| author | |
| committer | |
| log | fc2cdeceb9ddb28c0de834b9fcb38abf68d99148 |
| tree | 82c06d2faf1c657d442e28e4803a0e56a6a80144 |
| parent | c637dfc454342c156dbe61ee8513f5e0ac0c4691 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>4 files changed, 470 insertions(+), 305 deletions(-)
apps/web/src/components/preview/PreviewAutomationOwner.tsx+161-238| ... | @@ -3,19 +3,13 @@ | ... | @@ -3,19 +3,13 @@ |
| 3 | import { useAtomValue } from "@effect/atom-react"; | 3 | import { useAtomValue } from "@effect/atom-react"; |
| 4 | import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; | 4 | import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; |
| 5 | import { | 5 | import { |
| 6 | EnvironmentId, | ||
| 7 | type PreviewAutomationNavigateInput, | 6 | type PreviewAutomationNavigateInput, |
| 8 | type PreviewAutomationOpenInput, | 7 | type PreviewAutomationOpenInput, |
| 9 | PreviewAutomationOperation, | ||
| 10 | type PreviewAutomationOwner as PreviewAutomationOwnerState, | 8 | type PreviewAutomationOwner as PreviewAutomationOwnerState, |
| 11 | type PreviewAutomationRequest, | 9 | type PreviewAutomationRequest, |
| 12 | type PreviewAutomationStatus, | 10 | type PreviewAutomationStatus, |
| 13 | PreviewTabId, | ||
| 14 | type ScopedThreadRef, | 11 | type ScopedThreadRef, |
| 15 | ThreadId, | ||
| 16 | TrimmedNonEmptyString, | ||
| 17 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 18 | import * as Schema from "effect/Schema"; | ||
| 19 | import { useCallback, useEffect, useEffectEvent, useId, useMemo, useRef, useState } from "react"; | 13 | import { useCallback, useEffect, useEffectEvent, useId, useMemo, useRef, useState } from "react"; |
| 20 | 14 | ||
| 21 | import { | 15 | import { |
| ... | @@ -31,105 +25,19 @@ import { useEnvironmentConnectionState } from "~/state/environments"; | ... | @@ -31,105 +25,19 @@ import { useEnvironmentConnectionState } from "~/state/environments"; |
| 31 | import { useAtomCommand } from "~/state/use-atom-command"; | 25 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 32 | 26 | ||
| 33 | import { previewBridge } from "./previewBridge"; | 27 | import { previewBridge } from "./previewBridge"; |
| 28 | import { | ||
| 29 | PreviewAutomationNavigationTimeoutError, | ||
| 30 | PreviewAutomationOperationError, | ||
| 31 | PreviewAutomationOverlayTimeoutError, | ||
| 32 | PreviewAutomationRecordingNotActiveError, | ||
| 33 | PreviewAutomationStaleOwnerError, | ||
| 34 | PreviewAutomationTargetUnavailableError, | ||
| 35 | } from "./previewAutomationErrors"; | ||
| 34 | import { | 36 | import { |
| 35 | createLatestPreviewAutomationRequestHandler, | 37 | createLatestPreviewAutomationRequestHandler, |
| 36 | createPreviewAutomationRequestConsumerAtom, | 38 | createPreviewAutomationRequestConsumerAtom, |
| 37 | } from "./previewAutomationRequestConsumer"; | 39 | } from "./previewAutomationRequestConsumer"; |
| 38 | 40 | ||
| 39 | export class PreviewAutomationOverlayTimeoutError extends Schema.TaggedErrorClass<PreviewAutomationOverlayTimeoutError>()( | ||
| 40 | "PreviewAutomationOverlayTimeoutError", | ||
| 41 | { | ||
| 42 | requestId: TrimmedNonEmptyString, | ||
| 43 | environmentId: EnvironmentId, | ||
| 44 | threadId: ThreadId, | ||
| 45 | timeoutMs: Schema.Int, | ||
| 46 | }, | ||
| 47 | ) { | ||
| 48 | get responseTag() { | ||
| 49 | return "PreviewAutomationTimeoutError"; | ||
| 50 | } | ||
| 51 | |||
| 52 | override get message(): string { | ||
| 53 | return `Preview webview for request ${this.requestId} on environment ${this.environmentId} thread ${this.threadId} did not register within ${this.timeoutMs}ms.`; | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | export class PreviewAutomationNavigationTimeoutError extends Schema.TaggedErrorClass<PreviewAutomationNavigationTimeoutError>()( | ||
| 58 | "PreviewAutomationNavigationTimeoutError", | ||
| 59 | { | ||
| 60 | requestId: TrimmedNonEmptyString, | ||
| 61 | environmentId: EnvironmentId, | ||
| 62 | threadId: ThreadId, | ||
| 63 | tabId: PreviewTabId, | ||
| 64 | readiness: Schema.Literals(["domContentLoaded", "load"]), | ||
| 65 | timeoutMs: Schema.Int, | ||
| 66 | }, | ||
| 67 | ) { | ||
| 68 | get responseTag() { | ||
| 69 | return "PreviewAutomationTimeoutError"; | ||
| 70 | } | ||
| 71 | |||
| 72 | override get message(): string { | ||
| 73 | return `Preview navigation for request ${this.requestId} on environment ${this.environmentId} thread ${this.threadId} tab ${this.tabId} did not reach ${this.readiness} readiness within ${this.timeoutMs}ms.`; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | export class PreviewAutomationStaleOwnerError extends Schema.TaggedErrorClass<PreviewAutomationStaleOwnerError>()( | ||
| 78 | "PreviewAutomationStaleOwnerError", | ||
| 79 | { | ||
| 80 | requestId: TrimmedNonEmptyString, | ||
| 81 | environmentId: EnvironmentId, | ||
| 82 | expectedThreadId: ThreadId, | ||
| 83 | requestedThreadId: ThreadId, | ||
| 84 | }, | ||
| 85 | ) { | ||
| 86 | get responseTag() { | ||
| 87 | return "PreviewAutomationUnavailableError"; | ||
| 88 | } | ||
| 89 | |||
| 90 | override get message(): string { | ||
| 91 | return `Preview automation request ${this.requestId} targeted thread ${this.requestedThreadId}, but the owner for environment ${this.environmentId} is attached to thread ${this.expectedThreadId}.`; | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | export class PreviewAutomationTargetUnavailableError extends Schema.TaggedErrorClass<PreviewAutomationTargetUnavailableError>()( | ||
| 96 | "PreviewAutomationTargetUnavailableError", | ||
| 97 | { | ||
| 98 | requestId: TrimmedNonEmptyString, | ||
| 99 | operation: PreviewAutomationOperation, | ||
| 100 | environmentId: EnvironmentId, | ||
| 101 | threadId: ThreadId, | ||
| 102 | tabId: Schema.NullOr(PreviewTabId), | ||
| 103 | bridgeAvailable: Schema.Boolean, | ||
| 104 | }, | ||
| 105 | ) { | ||
| 106 | get responseTag() { | ||
| 107 | return "PreviewAutomationTabNotFoundError"; | ||
| 108 | } | ||
| 109 | |||
| 110 | override get message(): string { | ||
| 111 | return `Preview automation target for ${this.operation} request ${this.requestId} is unavailable on environment ${this.environmentId} thread ${this.threadId} (tab ${this.tabId ?? "unassigned"}, bridge ${this.bridgeAvailable ? "available" : "unavailable"}).`; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | export class PreviewAutomationRecordingNotActiveError extends Schema.TaggedErrorClass<PreviewAutomationRecordingNotActiveError>()( | ||
| 116 | "PreviewAutomationRecordingNotActiveError", | ||
| 117 | { | ||
| 118 | requestId: TrimmedNonEmptyString, | ||
| 119 | environmentId: EnvironmentId, | ||
| 120 | threadId: ThreadId, | ||
| 121 | tabId: PreviewTabId, | ||
| 122 | }, | ||
| 123 | ) { | ||
| 124 | get responseTag() { | ||
| 125 | return "PreviewAutomationExecutionError"; | ||
| 126 | } | ||
| 127 | |||
| 128 | override get message(): string { | ||
| 129 | return `Preview automation request ${this.requestId} found no active recording for tab ${this.tabId} on environment ${this.environmentId} thread ${this.threadId}.`; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | export function observeAutomationOwnerConnectedGeneration( | 41 | export function observeAutomationOwnerConnectedGeneration( |
| 134 | previousGeneration: number | null, | 42 | previousGeneration: number | null, |
| 135 | connectedGeneration: number | null, | 43 | connectedGeneration: number | null, |
| ... | @@ -287,152 +195,166 @@ export function PreviewAutomationOwner(props: { | ... | @@ -287,152 +195,166 @@ export function PreviewAutomationOwner(props: { |
| 287 | 195 | ||
| 288 | const handleRequest = useCallback( | 196 | const handleRequest = useCallback( |
| 289 | async (request: PreviewAutomationRequest): Promise<unknown> => { | 197 | async (request: PreviewAutomationRequest): Promise<unknown> => { |
| 290 | if (request.threadId !== threadRef.threadId) { | 198 | let tabId = request.tabId ?? null; |
| 291 | throw new PreviewAutomationStaleOwnerError({ | 199 | try { |
| 200 | if (request.threadId !== threadRef.threadId) { | ||
| 201 | throw new PreviewAutomationStaleOwnerError({ | ||
| 202 | requestId: request.requestId, | ||
| 203 | environmentId: threadRef.environmentId, | ||
| 204 | expectedThreadId: threadRef.threadId, | ||
| 205 | requestedThreadId: request.threadId, | ||
| 206 | }); | ||
| 207 | } | ||
| 208 | const state = readThreadPreviewState(threadRef); | ||
| 209 | tabId = request.tabId ?? state.snapshot?.tabId ?? null; | ||
| 210 | const unavailableTarget = { | ||
| 292 | requestId: request.requestId, | 211 | requestId: request.requestId, |
| 212 | operation: request.operation, | ||
| 293 | environmentId: threadRef.environmentId, | 213 | environmentId: threadRef.environmentId, |
| 294 | expectedThreadId: threadRef.threadId, | 214 | threadId: threadRef.threadId, |
| 295 | requestedThreadId: request.threadId, | 215 | tabId, |
| 296 | }); | 216 | bridgeAvailable: Boolean(previewBridge), |
| 297 | } | 217 | }; |
| 298 | const state = readThreadPreviewState(threadRef); | 218 | switch (request.operation) { |
| 299 | const tabId = request.tabId ?? state.snapshot?.tabId ?? null; | 219 | case "status": |
| 300 | const unavailableTarget = { | 220 | return await currentStatus(threadRef, visible); |
| 301 | requestId: request.requestId, | 221 | case "open": { |
| 302 | operation: request.operation, | 222 | const input = request.input as PreviewAutomationOpenInput; |
| 303 | environmentId: threadRef.environmentId, | 223 | let activeTabId = |
| 304 | threadId: threadRef.threadId, | 224 | (input.reuseExistingTab ?? true) ? (state.snapshot?.tabId ?? null) : null; |
| 305 | tabId, | 225 | tabId = activeTabId; |
| 306 | bridgeAvailable: Boolean(previewBridge), | 226 | if (!activeTabId) { |
| 307 | }; | 227 | const result = await open({ |
| 308 | switch (request.operation) { | 228 | environmentId: threadRef.environmentId, |
| 309 | case "status": | 229 | input: { |
| 310 | return currentStatus(threadRef, visible); | 230 | threadId: threadRef.threadId, |
| 311 | case "open": { | 231 | ...(input.url ? { url: input.url } : {}), |
| 312 | const input = request.input as PreviewAutomationOpenInput; | 232 | }, |
| 313 | let activeTabId = | 233 | }); |
| 314 | (input.reuseExistingTab ?? true) ? (state.snapshot?.tabId ?? null) : null; | 234 | if (result._tag === "Failure") { |
| 315 | if (!activeTabId) { | 235 | throw squashAtomCommandFailure(result); |
| 316 | const result = await open({ | 236 | } |
| 317 | environmentId: threadRef.environmentId, | 237 | const snapshot = result.value; |
| 318 | input: { | 238 | applyPreviewServerSnapshot(threadRef, snapshot); |
| 319 | threadId: threadRef.threadId, | 239 | activeTabId = snapshot.tabId; |
| 320 | ...(input.url ? { url: input.url } : {}), | 240 | tabId = activeTabId; |
| 321 | }, | 241 | } else if (input.url && previewBridge) { |
| 322 | }); | 242 | await previewBridge.navigate(activeTabId, input.url); |
| 323 | if (result._tag === "Failure") { | ||
| 324 | throw squashAtomCommandFailure(result); | ||
| 325 | } | 243 | } |
| 326 | const snapshot = result.value; | 244 | if (input.show ?? true) { |
| 327 | applyPreviewServerSnapshot(threadRef, snapshot); | 245 | useRightPanelStore.getState().openBrowser(threadRef, activeTabId); |
| 328 | activeTabId = snapshot.tabId; | 246 | } |
| 329 | } else if (input.url && previewBridge) { | 247 | await waitForDesktopOverlay(threadRef, request.requestId, request.timeoutMs); |
| 330 | await previewBridge.navigate(activeTabId, input.url); | 248 | return await currentStatus(threadRef, input.show ?? true); |
| 331 | } | ||
| 332 | if (input.show ?? true) { | ||
| 333 | useRightPanelStore.getState().openBrowser(threadRef, activeTabId); | ||
| 334 | } | ||
| 335 | await waitForDesktopOverlay(threadRef, request.requestId, request.timeoutMs); | ||
| 336 | return currentStatus(threadRef, input.show ?? true); | ||
| 337 | } | ||
| 338 | case "navigate": { | ||
| 339 | if (!previewBridge || !tabId) { | ||
| 340 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 341 | } | ||
| 342 | const input = request.input as PreviewAutomationNavigateInput; | ||
| 343 | const resolution = resolveBrowserNavigationTarget( | ||
| 344 | threadRef.environmentId, | ||
| 345 | input.target ?? { kind: "url", url: input.url! }, | ||
| 346 | ); | ||
| 347 | await previewBridge.navigate(tabId, resolution.resolvedUrl); | ||
| 348 | await waitForNavigationReadiness( | ||
| 349 | threadRef, | ||
| 350 | request.requestId, | ||
| 351 | tabId, | ||
| 352 | input.readiness ?? "load", | ||
| 353 | input.timeoutMs ?? request.timeoutMs, | ||
| 354 | ); | ||
| 355 | return currentStatus(threadRef, visible); | ||
| 356 | } | ||
| 357 | case "snapshot": | ||
| 358 | if (!previewBridge || !tabId) { | ||
| 359 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 360 | } | ||
| 361 | return previewBridge.automation.snapshot(tabId); | ||
| 362 | case "click": | ||
| 363 | if (!previewBridge || !tabId) { | ||
| 364 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 365 | } | ||
| 366 | return previewBridge.automation.click( | ||
| 367 | tabId, | ||
| 368 | request.input as Parameters<typeof previewBridge.automation.click>[1], | ||
| 369 | ); | ||
| 370 | case "type": | ||
| 371 | if (!previewBridge || !tabId) { | ||
| 372 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 373 | } | ||
| 374 | return previewBridge.automation.type( | ||
| 375 | tabId, | ||
| 376 | request.input as Parameters<typeof previewBridge.automation.type>[1], | ||
| 377 | ); | ||
| 378 | case "press": | ||
| 379 | if (!previewBridge || !tabId) { | ||
| 380 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 381 | } | ||
| 382 | return previewBridge.automation.press( | ||
| 383 | tabId, | ||
| 384 | request.input as Parameters<typeof previewBridge.automation.press>[1], | ||
| 385 | ); | ||
| 386 | case "scroll": | ||
| 387 | if (!previewBridge || !tabId) { | ||
| 388 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 389 | } | ||
| 390 | return previewBridge.automation.scroll( | ||
| 391 | tabId, | ||
| 392 | request.input as Parameters<typeof previewBridge.automation.scroll>[1], | ||
| 393 | ); | ||
| 394 | case "evaluate": | ||
| 395 | if (!previewBridge || !tabId) { | ||
| 396 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 397 | } | ||
| 398 | return previewBridge.automation.evaluate( | ||
| 399 | tabId, | ||
| 400 | request.input as Parameters<typeof previewBridge.automation.evaluate>[1], | ||
| 401 | ); | ||
| 402 | case "waitFor": | ||
| 403 | if (!previewBridge || !tabId) { | ||
| 404 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 405 | } | ||
| 406 | return previewBridge.automation.waitFor( | ||
| 407 | tabId, | ||
| 408 | request.input as Parameters<typeof previewBridge.automation.waitFor>[1], | ||
| 409 | ); | ||
| 410 | case "recordingStart": { | ||
| 411 | if (!tabId) { | ||
| 412 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 413 | } | 249 | } |
| 414 | const startedAt = await startBrowserRecording(tabId); | 250 | case "navigate": { |
| 415 | return { | 251 | if (!previewBridge || !tabId) { |
| 416 | tabId, | 252 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); |
| 417 | recording: true, | 253 | } |
| 418 | startedAt, | 254 | const input = request.input as PreviewAutomationNavigateInput; |
| 419 | }; | 255 | const resolution = resolveBrowserNavigationTarget( |
| 420 | } | 256 | threadRef.environmentId, |
| 421 | case "recordingStop": { | 257 | input.target ?? { kind: "url", url: input.url! }, |
| 422 | if (!tabId) { | 258 | ); |
| 423 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | 259 | await previewBridge.navigate(tabId, resolution.resolvedUrl); |
| 260 | await waitForNavigationReadiness( | ||
| 261 | threadRef, | ||
| 262 | request.requestId, | ||
| 263 | tabId, | ||
| 264 | input.readiness ?? "load", | ||
| 265 | input.timeoutMs ?? request.timeoutMs, | ||
| 266 | ); | ||
| 267 | return await currentStatus(threadRef, visible); | ||
| 424 | } | 268 | } |
| 425 | const artifact = await stopBrowserRecording(tabId); | 269 | case "snapshot": |
| 426 | if (!artifact) { | 270 | if (!previewBridge || !tabId) { |
| 427 | throw new PreviewAutomationRecordingNotActiveError({ | 271 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); |
| 428 | requestId: request.requestId, | 272 | } |
| 429 | environmentId: threadRef.environmentId, | 273 | return await previewBridge.automation.snapshot(tabId); |
| 430 | threadId: threadRef.threadId, | 274 | case "click": |
| 275 | if (!previewBridge || !tabId) { | ||
| 276 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 277 | } | ||
| 278 | return await previewBridge.automation.click( | ||
| 279 | tabId, | ||
| 280 | request.input as Parameters<typeof previewBridge.automation.click>[1], | ||
| 281 | ); | ||
| 282 | case "type": | ||
| 283 | if (!previewBridge || !tabId) { | ||
| 284 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 285 | } | ||
| 286 | return await previewBridge.automation.type( | ||
| 287 | tabId, | ||
| 288 | request.input as Parameters<typeof previewBridge.automation.type>[1], | ||
| 289 | ); | ||
| 290 | case "press": | ||
| 291 | if (!previewBridge || !tabId) { | ||
| 292 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 293 | } | ||
| 294 | return await previewBridge.automation.press( | ||
| 295 | tabId, | ||
| 296 | request.input as Parameters<typeof previewBridge.automation.press>[1], | ||
| 297 | ); | ||
| 298 | case "scroll": | ||
| 299 | if (!previewBridge || !tabId) { | ||
| 300 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 301 | } | ||
| 302 | return await previewBridge.automation.scroll( | ||
| 303 | tabId, | ||
| 304 | request.input as Parameters<typeof previewBridge.automation.scroll>[1], | ||
| 305 | ); | ||
| 306 | case "evaluate": | ||
| 307 | if (!previewBridge || !tabId) { | ||
| 308 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 309 | } | ||
| 310 | return await previewBridge.automation.evaluate( | ||
| 311 | tabId, | ||
| 312 | request.input as Parameters<typeof previewBridge.automation.evaluate>[1], | ||
| 313 | ); | ||
| 314 | case "waitFor": | ||
| 315 | if (!previewBridge || !tabId) { | ||
| 316 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 317 | } | ||
| 318 | return await previewBridge.automation.waitFor( | ||
| 319 | tabId, | ||
| 320 | request.input as Parameters<typeof previewBridge.automation.waitFor>[1], | ||
| 321 | ); | ||
| 322 | case "recordingStart": { | ||
| 323 | if (!tabId) { | ||
| 324 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 325 | } | ||
| 326 | const startedAt = await startBrowserRecording(tabId); | ||
| 327 | return { | ||
| 431 | tabId, | 328 | tabId, |
| 432 | }); | 329 | recording: true, |
| 330 | startedAt, | ||
| 331 | }; | ||
| 332 | } | ||
| 333 | case "recordingStop": { | ||
| 334 | if (!tabId) { | ||
| 335 | throw new PreviewAutomationTargetUnavailableError(unavailableTarget); | ||
| 336 | } | ||
| 337 | const artifact = await stopBrowserRecording(tabId); | ||
| 338 | if (!artifact) { | ||
| 339 | throw new PreviewAutomationRecordingNotActiveError({ | ||
| 340 | requestId: request.requestId, | ||
| 341 | environmentId: threadRef.environmentId, | ||
| 342 | threadId: threadRef.threadId, | ||
| 343 | tabId, | ||
| 344 | }); | ||
| 345 | } | ||
| 346 | return artifact; | ||
| 433 | } | 347 | } |
| 434 | return artifact; | ||
| 435 | } | 348 | } |
| 349 | } catch (cause) { | ||
| 350 | throw PreviewAutomationOperationError.fromCause({ | ||
| 351 | requestId: request.requestId, | ||
| 352 | operation: request.operation, | ||
| 353 | environmentId: threadRef.environmentId, | ||
| 354 | threadId: threadRef.threadId, | ||
| 355 | tabId, | ||
| 356 | cause, | ||
| 357 | }); | ||
| 436 | } | 358 | } |
| 437 | }, | 359 | }, |
| 438 | [open, threadRef, visible], | 360 | [open, threadRef, visible], |
| ... | @@ -448,6 +370,7 @@ export function PreviewAutomationOwner(props: { | ... | @@ -448,6 +370,7 @@ export function PreviewAutomationOwner(props: { |
| 448 | () => | 370 | () => |
| 449 | createPreviewAutomationRequestConsumerAtom({ | 371 | createPreviewAutomationRequestConsumerAtom({ |
| 450 | requestsAtom: automationRequestsAtom, | 372 | requestsAtom: automationRequestsAtom, |
| 373 | environmentId: threadRef.environmentId, | ||
| 451 | handleRequest: requestHandler.handle, | 374 | handleRequest: requestHandler.handle, |
| 452 | respond: (response) => | 375 | respond: (response) => |
| 453 | respondToAutomation({ | 376 | respondToAutomation({ |
apps/web/src/components/preview/previewAutomationErrors.ts created+169| ... | @@ -0,0 +1,169 @@ | ||
| 1 | import { | ||
| 2 | EnvironmentId, | ||
| 3 | type PreviewAutomationOwner, | ||
| 4 | PreviewAutomationOperation, | ||
| 5 | type PreviewAutomationRequest, | ||
| 6 | type PreviewAutomationResponse, | ||
| 7 | PreviewTabId, | ||
| 8 | ThreadId, | ||
| 9 | TrimmedNonEmptyString, | ||
| 10 | } from "@t3tools/contracts"; | ||
| 11 | import * as Schema from "effect/Schema"; | ||
| 12 | |||
| 13 | export interface PreviewAutomationOperationContext { | ||
| 14 | readonly requestId: PreviewAutomationRequest["requestId"]; | ||
| 15 | readonly operation: PreviewAutomationRequest["operation"]; | ||
| 16 | readonly environmentId: PreviewAutomationOwner["environmentId"]; | ||
| 17 | readonly threadId: PreviewAutomationRequest["threadId"]; | ||
| 18 | readonly tabId: Exclude<PreviewAutomationRequest["tabId"], undefined> | null; | ||
| 19 | } | ||
| 20 | |||
| 21 | export class PreviewAutomationOverlayTimeoutError extends Schema.TaggedErrorClass<PreviewAutomationOverlayTimeoutError>()( | ||
| 22 | "PreviewAutomationOverlayTimeoutError", | ||
| 23 | { | ||
| 24 | requestId: TrimmedNonEmptyString, | ||
| 25 | environmentId: EnvironmentId, | ||
| 26 | threadId: ThreadId, | ||
| 27 | timeoutMs: Schema.Int, | ||
| 28 | }, | ||
| 29 | ) { | ||
| 30 | get responseTag() { | ||
| 31 | return "PreviewAutomationTimeoutError" as const; | ||
| 32 | } | ||
| 33 | |||
| 34 | override get message(): string { | ||
| 35 | return `Preview webview for request ${this.requestId} on environment ${this.environmentId} thread ${this.threadId} did not register within ${this.timeoutMs}ms.`; | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | export class PreviewAutomationNavigationTimeoutError extends Schema.TaggedErrorClass<PreviewAutomationNavigationTimeoutError>()( | ||
| 40 | "PreviewAutomationNavigationTimeoutError", | ||
| 41 | { | ||
| 42 | requestId: TrimmedNonEmptyString, | ||
| 43 | environmentId: EnvironmentId, | ||
| 44 | threadId: ThreadId, | ||
| 45 | tabId: PreviewTabId, | ||
| 46 | readiness: Schema.Literals(["domContentLoaded", "load"]), | ||
| 47 | timeoutMs: Schema.Int, | ||
| 48 | }, | ||
| 49 | ) { | ||
| 50 | get responseTag() { | ||
| 51 | return "PreviewAutomationTimeoutError" as const; | ||
| 52 | } | ||
| 53 | |||
| 54 | override get message(): string { | ||
| 55 | return `Preview navigation for request ${this.requestId} on environment ${this.environmentId} thread ${this.threadId} tab ${this.tabId} did not reach ${this.readiness} readiness within ${this.timeoutMs}ms.`; | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | export class PreviewAutomationStaleOwnerError extends Schema.TaggedErrorClass<PreviewAutomationStaleOwnerError>()( | ||
| 60 | "PreviewAutomationStaleOwnerError", | ||
| 61 | { | ||
| 62 | requestId: TrimmedNonEmptyString, | ||
| 63 | environmentId: EnvironmentId, | ||
| 64 | expectedThreadId: ThreadId, | ||
| 65 | requestedThreadId: ThreadId, | ||
| 66 | }, | ||
| 67 | ) { | ||
| 68 | get responseTag() { | ||
| 69 | return "PreviewAutomationUnavailableError" as const; | ||
| 70 | } | ||
| 71 | |||
| 72 | override get message(): string { | ||
| 73 | return `Preview automation request ${this.requestId} targeted thread ${this.requestedThreadId}, but the owner for environment ${this.environmentId} is attached to thread ${this.expectedThreadId}.`; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | export class PreviewAutomationTargetUnavailableError extends Schema.TaggedErrorClass<PreviewAutomationTargetUnavailableError>()( | ||
| 78 | "PreviewAutomationTargetUnavailableError", | ||
| 79 | { | ||
| 80 | requestId: TrimmedNonEmptyString, | ||
| 81 | operation: PreviewAutomationOperation, | ||
| 82 | environmentId: EnvironmentId, | ||
| 83 | threadId: ThreadId, | ||
| 84 | tabId: Schema.NullOr(PreviewTabId), | ||
| 85 | bridgeAvailable: Schema.Boolean, | ||
| 86 | }, | ||
| 87 | ) { | ||
| 88 | get responseTag() { | ||
| 89 | return "PreviewAutomationTabNotFoundError" as const; | ||
| 90 | } | ||
| 91 | |||
| 92 | override get message(): string { | ||
| 93 | return `Preview automation target for ${this.operation} request ${this.requestId} is unavailable on environment ${this.environmentId} thread ${this.threadId} (tab ${this.tabId ?? "unassigned"}, bridge ${this.bridgeAvailable ? "available" : "unavailable"}).`; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | export class PreviewAutomationRecordingNotActiveError extends Schema.TaggedErrorClass<PreviewAutomationRecordingNotActiveError>()( | ||
| 98 | "PreviewAutomationRecordingNotActiveError", | ||
| 99 | { | ||
| 100 | requestId: TrimmedNonEmptyString, | ||
| 101 | environmentId: EnvironmentId, | ||
| 102 | threadId: ThreadId, | ||
| 103 | tabId: PreviewTabId, | ||
| 104 | }, | ||
| 105 | ) { | ||
| 106 | get responseTag() { | ||
| 107 | return "PreviewAutomationExecutionError" as const; | ||
| 108 | } | ||
| 109 | |||
| 110 | override get message(): string { | ||
| 111 | return `Preview automation request ${this.requestId} found no active recording for tab ${this.tabId} on environment ${this.environmentId} thread ${this.threadId}.`; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | export class PreviewAutomationOperationError extends Schema.TaggedErrorClass<PreviewAutomationOperationError>()( | ||
| 116 | "PreviewAutomationOperationError", | ||
| 117 | { | ||
| 118 | requestId: TrimmedNonEmptyString, | ||
| 119 | operation: PreviewAutomationOperation, | ||
| 120 | environmentId: EnvironmentId, | ||
| 121 | threadId: ThreadId, | ||
| 122 | tabId: Schema.NullOr(PreviewTabId), | ||
| 123 | cause: Schema.Defect(), | ||
| 124 | }, | ||
| 125 | ) { | ||
| 126 | static fromCause( | ||
| 127 | input: PreviewAutomationOperationContext & { readonly cause: unknown }, | ||
| 128 | ): PreviewAutomationOwnerError { | ||
| 129 | return isPreviewAutomationOwnerError(input.cause) | ||
| 130 | ? input.cause | ||
| 131 | : new PreviewAutomationOperationError(input); | ||
| 132 | } | ||
| 133 | |||
| 134 | get responseTag() { | ||
| 135 | return "PreviewAutomationExecutionError" as const; | ||
| 136 | } | ||
| 137 | |||
| 138 | override get message(): string { | ||
| 139 | return `Preview automation ${this.operation} request ${this.requestId} failed on environment ${this.environmentId} thread ${this.threadId} (tab ${this.tabId ?? "unassigned"}).`; | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | export const PreviewAutomationOwnerError = Schema.Union([ | ||
| 144 | PreviewAutomationOverlayTimeoutError, | ||
| 145 | PreviewAutomationNavigationTimeoutError, | ||
| 146 | PreviewAutomationStaleOwnerError, | ||
| 147 | PreviewAutomationTargetUnavailableError, | ||
| 148 | PreviewAutomationRecordingNotActiveError, | ||
| 149 | PreviewAutomationOperationError, | ||
| 150 | ]); | ||
| 151 | export type PreviewAutomationOwnerError = typeof PreviewAutomationOwnerError.Type; | ||
| 152 | |||
| 153 | export const isPreviewAutomationOwnerError = Schema.is(PreviewAutomationOwnerError); | ||
| 154 | |||
| 155 | export function serializePreviewAutomationOwnerError( | ||
| 156 | error: PreviewAutomationOwnerError, | ||
| 157 | ): NonNullable<PreviewAutomationResponse["error"]> { | ||
| 158 | const detail = Object.fromEntries( | ||
| 159 | Object.entries(error).filter( | ||
| 160 | ([key]) => | ||
| 161 | key !== "_tag" && key !== "cause" && key !== "name" && key !== "message" && key !== "stack", | ||
| 162 | ), | ||
| 163 | ); | ||
| 164 | return { | ||
| 165 | _tag: error.responseTag, | ||
| 166 | message: error.message, | ||
| 167 | ...(Object.keys(detail).length === 0 ? {} : { detail }), | ||
| 168 | }; | ||
| 169 | } | ||
apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts+117-22| ... | @@ -1,19 +1,33 @@ | ... | @@ -1,19 +1,33 @@ |
| 1 | import type { PreviewAutomationRequest, PreviewAutomationResponse } from "@t3tools/contracts"; | 1 | import { |
| 2 | import { ThreadId } from "@t3tools/contracts"; | 2 | EnvironmentId, |
| 3 | type PreviewAutomationRequest, | ||
| 4 | type PreviewAutomationResponse, | ||
| 5 | PreviewTabId, | ||
| 6 | ThreadId, | ||
| 7 | } from "@t3tools/contracts"; | ||
| 3 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; | 8 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; |
| 4 | import { describe, expect, it, vi } from "vite-plus/test"; | 9 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 5 | 10 | ||
| 11 | import { PreviewAutomationTargetUnavailableError } from "./previewAutomationErrors"; | ||
| 6 | import { | 12 | import { |
| 7 | createPreviewAutomationRequestConsumerAtom, | 13 | createPreviewAutomationRequestConsumerAtom, |
| 8 | serializePreviewAutomationError, | 14 | serializePreviewAutomationError, |
| 9 | } from "./previewAutomationRequestConsumer"; | 15 | } from "./previewAutomationRequestConsumer"; |
| 10 | 16 | ||
| 11 | const request = (requestId: string): PreviewAutomationRequest => ({ | 17 | const environmentId = EnvironmentId.make("environment-1"); |
| 18 | const threadId = ThreadId.make("thread-1"); | ||
| 19 | const tabId = PreviewTabId.make("tab-1"); | ||
| 20 | |||
| 21 | const request = ( | ||
| 22 | requestId: string, | ||
| 23 | overrides: Partial<PreviewAutomationRequest> = {}, | ||
| 24 | ): PreviewAutomationRequest => ({ | ||
| 12 | requestId, | 25 | requestId, |
| 13 | threadId: ThreadId.make("thread-1"), | 26 | threadId, |
| 14 | operation: "status", | 27 | operation: "status", |
| 15 | input: {}, | 28 | input: {}, |
| 16 | timeoutMs: 15_000, | 29 | timeoutMs: 15_000, |
| 30 | ...overrides, | ||
| 17 | }); | 31 | }); |
| 18 | 32 | ||
| 19 | describe("previewAutomationRequestConsumer", () => { | 33 | describe("previewAutomationRequestConsumer", () => { |
| ... | @@ -30,6 +44,7 @@ describe("previewAutomationRequestConsumer", () => { | ... | @@ -30,6 +44,7 @@ describe("previewAutomationRequestConsumer", () => { |
| 30 | }); | 44 | }); |
| 31 | const consumerAtom = createPreviewAutomationRequestConsumerAtom({ | 45 | const consumerAtom = createPreviewAutomationRequestConsumerAtom({ |
| 32 | requestsAtom, | 46 | requestsAtom, |
| 47 | environmentId, | ||
| 33 | handleRequest, | 48 | handleRequest, |
| 34 | respond, | 49 | respond, |
| 35 | label: "test:preview-automation-consumer", | 50 | label: "test:preview-automation-consumer", |
| ... | @@ -56,6 +71,7 @@ describe("previewAutomationRequestConsumer", () => { | ... | @@ -56,6 +71,7 @@ describe("previewAutomationRequestConsumer", () => { |
| 56 | const respond = vi.fn(async (_response: PreviewAutomationResponse) => undefined); | 71 | const respond = vi.fn(async (_response: PreviewAutomationResponse) => undefined); |
| 57 | const consumerAtom = createPreviewAutomationRequestConsumerAtom({ | 72 | const consumerAtom = createPreviewAutomationRequestConsumerAtom({ |
| 58 | requestsAtom, | 73 | requestsAtom, |
| 74 | environmentId, | ||
| 59 | handleRequest: async () => undefined, | 75 | handleRequest: async () => undefined, |
| 60 | respond, | 76 | respond, |
| 61 | label: "test:preview-automation-initial-request", | 77 | label: "test:preview-automation-initial-request", |
| ... | @@ -69,33 +85,112 @@ describe("previewAutomationRequestConsumer", () => { | ... | @@ -69,33 +85,112 @@ describe("previewAutomationRequestConsumer", () => { |
| 69 | registry.dispose(); | 85 | registry.dispose(); |
| 70 | }); | 86 | }); |
| 71 | 87 | ||
| 72 | it("preserves typed automation errors in responses", () => { | 88 | it("preserves tagged automation errors and their structured diagnostics", () => { |
| 73 | const error = new Error("No preview tab"); | 89 | const error = new PreviewAutomationTargetUnavailableError({ |
| 74 | error.name = "PreviewAutomationTabNotFoundError"; | 90 | requestId: "request-1", |
| 91 | operation: "click", | ||
| 92 | environmentId, | ||
| 93 | threadId, | ||
| 94 | tabId, | ||
| 95 | bridgeAvailable: false, | ||
| 96 | }); | ||
| 75 | 97 | ||
| 76 | expect(serializePreviewAutomationError(error)).toEqual({ | 98 | expect( |
| 99 | serializePreviewAutomationError(error, { | ||
| 100 | requestId: "request-1", | ||
| 101 | operation: "click", | ||
| 102 | environmentId, | ||
| 103 | threadId, | ||
| 104 | tabId, | ||
| 105 | }), | ||
| 106 | ).toEqual({ | ||
| 77 | _tag: "PreviewAutomationTabNotFoundError", | 107 | _tag: "PreviewAutomationTabNotFoundError", |
| 78 | message: "No preview tab", | 108 | message: |
| 109 | "Preview automation target for click request request-1 is unavailable on environment environment-1 thread thread-1 (tab tab-1, bridge unavailable).", | ||
| 110 | detail: { | ||
| 111 | requestId: "request-1", | ||
| 112 | operation: "click", | ||
| 113 | environmentId: "environment-1", | ||
| 114 | threadId: "thread-1", | ||
| 115 | tabId: "tab-1", | ||
| 116 | bridgeAvailable: false, | ||
| 117 | }, | ||
| 79 | }); | 118 | }); |
| 80 | }); | 119 | }); |
| 81 | 120 | ||
| 82 | it("serializes structured automation context without leaking causes", () => { | 121 | it("correlates unexpected failures without exposing cause details", () => { |
| 83 | const error = Object.assign(new Error("Preview target unavailable"), { | 122 | const cause = new Error("private bridge token: preview-secret"); |
| 84 | name: "PreviewAutomationTargetUnavailableError", | 123 | const context = { |
| 85 | _tag: "PreviewAutomationTargetUnavailableError", | 124 | requestId: "request-2", |
| 86 | responseTag: "PreviewAutomationTabNotFoundError", | 125 | operation: "snapshot" as const, |
| 87 | requestId: "request-1", | 126 | environmentId, |
| 88 | threadId: "thread-1", | 127 | threadId, |
| 89 | cause: new Error("private bridge failure"), | 128 | tabId, |
| 90 | }); | 129 | }; |
| 130 | const response = serializePreviewAutomationError(cause, context); | ||
| 91 | 131 | ||
| 92 | expect(serializePreviewAutomationError(error)).toEqual({ | 132 | expect(response).toEqual({ |
| 93 | _tag: "PreviewAutomationTabNotFoundError", | 133 | _tag: "PreviewAutomationExecutionError", |
| 94 | message: "Preview target unavailable", | 134 | message: |
| 135 | "Preview automation snapshot request request-2 failed on environment environment-1 thread thread-1 (tab tab-1).", | ||
| 95 | detail: { | 136 | detail: { |
| 96 | requestId: "request-1", | 137 | requestId: "request-2", |
| 138 | operation: "snapshot", | ||
| 139 | environmentId: "environment-1", | ||
| 97 | threadId: "thread-1", | 140 | threadId: "thread-1", |
| 141 | tabId: "tab-1", | ||
| 98 | }, | 142 | }, |
| 99 | }); | 143 | }); |
| 144 | expect(JSON.stringify(response)).not.toContain("preview-secret"); | ||
| 145 | }); | ||
| 146 | |||
| 147 | it("sanitizes unexpected handler failures at the response boundary", async () => { | ||
| 148 | const requestsAtom = Atom.make<AsyncResult.AsyncResult<PreviewAutomationRequest, Error>>( | ||
| 149 | AsyncResult.initial<PreviewAutomationRequest, Error>(false), | ||
| 150 | ); | ||
| 151 | const responses: PreviewAutomationResponse[] = []; | ||
| 152 | const consumerAtom = createPreviewAutomationRequestConsumerAtom({ | ||
| 153 | requestsAtom, | ||
| 154 | environmentId, | ||
| 155 | handleRequest: async () => { | ||
| 156 | throw new Error("desktop IPC secret: do-not-return"); | ||
| 157 | }, | ||
| 158 | respond: async (response) => { | ||
| 159 | responses.push(response); | ||
| 160 | }, | ||
| 161 | label: "test:preview-automation-failure-boundary", | ||
| 162 | }); | ||
| 163 | const registry = AtomRegistry.make(); | ||
| 164 | registry.mount(consumerAtom); | ||
| 165 | |||
| 166 | registry.set( | ||
| 167 | requestsAtom, | ||
| 168 | AsyncResult.success( | ||
| 169 | request("request-failed", { | ||
| 170 | operation: "click", | ||
| 171 | tabId, | ||
| 172 | }), | ||
| 173 | ), | ||
| 174 | ); | ||
| 175 | |||
| 176 | await vi.waitFor(() => expect(responses).toHaveLength(1)); | ||
| 177 | expect(responses[0]).toEqual({ | ||
| 178 | requestId: "request-failed", | ||
| 179 | ok: false, | ||
| 180 | error: { | ||
| 181 | _tag: "PreviewAutomationExecutionError", | ||
| 182 | message: | ||
| 183 | "Preview automation click request request-failed failed on environment environment-1 thread thread-1 (tab tab-1).", | ||
| 184 | detail: { | ||
| 185 | requestId: "request-failed", | ||
| 186 | operation: "click", | ||
| 187 | environmentId: "environment-1", | ||
| 188 | threadId: "thread-1", | ||
| 189 | tabId: "tab-1", | ||
| 190 | }, | ||
| 191 | }, | ||
| 192 | }); | ||
| 193 | expect(JSON.stringify(responses[0])).not.toContain("do-not-return"); | ||
| 194 | registry.dispose(); | ||
| 100 | }); | 195 | }); |
| 101 | }); | 196 | }); |
apps/web/src/components/preview/previewAutomationRequestConsumer.ts+23-45| ... | @@ -1,6 +1,16 @@ | ... | @@ -1,6 +1,16 @@ |
| 1 | import type { PreviewAutomationRequest, PreviewAutomationResponse } from "@t3tools/contracts"; | 1 | import type { |
| 2 | PreviewAutomationOwner, | ||
| 3 | PreviewAutomationRequest, | ||
| 4 | PreviewAutomationResponse, | ||
| 5 | } from "@t3tools/contracts"; | ||
| 2 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; | 6 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 3 | 7 | ||
| 8 | import { | ||
| 9 | PreviewAutomationOperationError, | ||
| 10 | type PreviewAutomationOperationContext, | ||
| 11 | serializePreviewAutomationOwnerError, | ||
| 12 | } from "./previewAutomationErrors"; | ||
| 13 | |||
| 4 | type AutomationRequestResult<E> = AsyncResult.AsyncResult<PreviewAutomationRequest, E>; | 14 | type AutomationRequestResult<E> = AsyncResult.AsyncResult<PreviewAutomationRequest, E>; |
| 5 | type AutomationRequestHandler = (request: PreviewAutomationRequest) => Promise<unknown>; | 15 | type AutomationRequestHandler = (request: PreviewAutomationRequest) => Promise<unknown>; |
| 6 | 16 | ||
| ... | @@ -19,54 +29,16 @@ export function createLatestPreviewAutomationRequestHandler(initial: AutomationR | ... | @@ -19,54 +29,16 @@ export function createLatestPreviewAutomationRequestHandler(initial: AutomationR |
| 19 | 29 | ||
| 20 | export function serializePreviewAutomationError( | 30 | export function serializePreviewAutomationError( |
| 21 | error: unknown, | 31 | error: unknown, |
| 32 | context: PreviewAutomationOperationContext, | ||
| 22 | ): NonNullable<PreviewAutomationResponse["error"]> { | 33 | ): NonNullable<PreviewAutomationResponse["error"]> { |
| 23 | if (error instanceof Error) { | 34 | return serializePreviewAutomationOwnerError( |
| 24 | const explicitDetail = | 35 | PreviewAutomationOperationError.fromCause({ ...context, cause: error }), |
| 25 | "detail" in error && (error as { detail?: unknown }).detail !== undefined | 36 | ); |
| 26 | ? (error as { detail?: unknown }).detail | ||
| 27 | : undefined; | ||
| 28 | const structuralDetail = | ||
| 29 | "_tag" in error && | ||
| 30 | typeof (error as { _tag?: unknown })._tag === "string" && | ||
| 31 | (error as { _tag: string })._tag.startsWith("PreviewAutomation") | ||
| 32 | ? Object.fromEntries( | ||
| 33 | Object.entries(error).filter( | ||
| 34 | ([key]) => | ||
| 35 | key !== "_tag" && | ||
| 36 | key !== "cause" && | ||
| 37 | key !== "name" && | ||
| 38 | key !== "message" && | ||
| 39 | key !== "stack" && | ||
| 40 | key !== "detail" && | ||
| 41 | key !== "responseTag", | ||
| 42 | ), | ||
| 43 | ) | ||
| 44 | : undefined; | ||
| 45 | const detail = explicitDetail ?? structuralDetail; | ||
| 46 | const responseTag = | ||
| 47 | "responseTag" in error && | ||
| 48 | typeof (error as { responseTag?: unknown }).responseTag === "string" && | ||
| 49 | (error as { responseTag: string }).responseTag.startsWith("PreviewAutomation") | ||
| 50 | ? (error as { responseTag: string }).responseTag | ||
| 51 | : undefined; | ||
| 52 | return { | ||
| 53 | _tag: | ||
| 54 | responseTag ?? | ||
| 55 | (error.name.startsWith("PreviewAutomation") | ||
| 56 | ? error.name | ||
| 57 | : "PreviewAutomationExecutionError"), | ||
| 58 | message: error.message, | ||
| 59 | ...(detail === undefined ? {} : { detail }), | ||
| 60 | }; | ||
| 61 | } | ||
| 62 | return { | ||
| 63 | _tag: "PreviewAutomationExecutionError", | ||
| 64 | message: String(error), | ||
| 65 | }; | ||
| 66 | } | 37 | } |
| 67 | 38 | ||
| 68 | export function createPreviewAutomationRequestConsumerAtom<E>(options: { | 39 | export function createPreviewAutomationRequestConsumerAtom<E>(options: { |
| 69 | readonly requestsAtom: Atom.Atom<AutomationRequestResult<E>>; | 40 | readonly requestsAtom: Atom.Atom<AutomationRequestResult<E>>; |
| 41 | readonly environmentId: PreviewAutomationOwner["environmentId"]; | ||
| 70 | readonly handleRequest: (request: PreviewAutomationRequest) => Promise<unknown>; | 42 | readonly handleRequest: (request: PreviewAutomationRequest) => Promise<unknown>; |
| 71 | readonly respond: (response: PreviewAutomationResponse) => Promise<unknown>; | 43 | readonly respond: (response: PreviewAutomationResponse) => Promise<unknown>; |
| 72 | readonly label: string; | 44 | readonly label: string; |
| ... | @@ -89,7 +61,13 @@ export function createPreviewAutomationRequestConsumerAtom<E>(options: { | ... | @@ -89,7 +61,13 @@ export function createPreviewAutomationRequestConsumerAtom<E>(options: { |
| 89 | options.respond({ | 61 | options.respond({ |
| 90 | requestId: request.requestId, | 62 | requestId: request.requestId, |
| 91 | ok: false, | 63 | ok: false, |
| 92 | error: serializePreviewAutomationError(error), | 64 | error: serializePreviewAutomationError(error, { |
| 65 | requestId: request.requestId, | ||
| 66 | operation: request.operation, | ||
| 67 | environmentId: options.environmentId, | ||
| 68 | threadId: request.threadId, | ||
| 69 | tabId: request.tabId ?? null, | ||
| 70 | }), | ||
| 93 | }), | 71 | }), |
| 94 | ); | 72 | ); |
| 95 | }; | 73 | }; |