authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 12:00:54-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 12:00:54-07:00
log336d176027d1079fb98554a9439b6a2412c7e2fe
tree0f7a4f9c2fd79f2e9c571372fa1f7f19ab263eca
parent204984bf8cc4c9b85ef4d100b355c6c0caf57a21
signature Signed by PGP key B5690EEEBB952194

[codex] Structure preview automation errors (#3272)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 441 insertions(+), 70 deletions(-)

apps/server/src/mcp/PreviewAutomationBroker.test.ts+110-2
...@@ -1,8 +1,10 @@...@@ -1,8 +1,10 @@
1import { expect, it } from "@effect/vitest";1import { expect, it } from "@effect/vitest";
2import {2import {
3 EnvironmentId,3 EnvironmentId,
4 PreviewAutomationClientDisconnectedError,
5 PreviewAutomationInvalidSelectorError,
6 PreviewAutomationMalformedResponseError,
4 PreviewAutomationNoFocusedOwnerError,7 PreviewAutomationNoFocusedOwnerError,
5 PreviewAutomationUnavailableError,
6 ProviderInstanceId,8 ProviderInstanceId,
7 ThreadId,9 ThreadId,
8 type PreviewAutomationOwner,10 type PreviewAutomationOwner,
...@@ -59,6 +61,95 @@ it.effect("atomically registers a connected owner and correlates its response",...@@ -59,6 +61,95 @@ it.effect("atomically registers a connected owner and correlates its response",
59 ),61 ),
60);62);
6163
64it.effect("preserves bounded request and remote selector diagnostics", () => {
65 const locator = "role=button[name='request-secret']";
66 const remoteMessage = "Unexpected token near remote-secret.";
67 const remoteError = {
68 _tag: "PreviewAutomationInvalidSelectorError",
69 message: remoteMessage,
70 detail: { selector: "role=button[name='remote-secret']" },
71 } as const;
72
73 return Effect.scoped(
74 Effect.gen(function* () {
75 const broker = yield* PreviewAutomationBroker.make;
76 const requests = yield* broker.connect(makeOwner({ tabId: "tab-1" }));
77 yield* Stream.runForEach(requests, (request) =>
78 broker.respond({
79 requestId: request.requestId,
80 ok: false,
81 error: remoteError,
82 }),
83 ).pipe(Effect.forkScoped);
84 yield* Effect.yieldNow;
85
86 const error = yield* broker
87 .invoke<void>({
88 scope,
89 operation: "click",
90 input: { locator },
91 timeoutMs: 1_234,
92 })
93 .pipe(Effect.flip);
94
95 expect(error).toBeInstanceOf(PreviewAutomationInvalidSelectorError);
96 expect(error).toMatchObject({
97 operation: "click",
98 environmentId: scope.environmentId,
99 threadId: scope.threadId,
100 providerSessionId: scope.providerSessionId,
101 providerInstanceId: scope.providerInstanceId,
102 clientId: "client-1",
103 requestId: "preview-0",
104 tabId: "tab-1",
105 timeoutMs: 1_234,
106 selectorKind: "locator",
107 selectorLength: locator.length,
108 remoteTag: "PreviewAutomationInvalidSelectorError",
109 remoteMessageLength: remoteMessage.length,
110 remoteDetailKind: "object",
111 });
112 expect(error.message).toBe(
113 `Preview automation click received an invalid locator (${locator.length} characters).`,
114 );
115 expect(error.message).not.toContain("secret");
116 expect(error.cause).toBe(remoteError);
117 expect("selector" in error).toBe(false);
118 expect("remoteMessage" in error).toBe(false);
119 expect("remoteDetail" in error).toBe(false);
120 }),
121 );
122});
123
124it.effect("distinguishes malformed remote failures", () =>
125 Effect.scoped(
126 Effect.gen(function* () {
127 const broker = yield* PreviewAutomationBroker.make;
128 const requests = yield* broker.connect(makeOwner());
129 yield* Stream.runForEach(requests, (request) =>
130 broker.respond({ requestId: request.requestId, ok: false }),
131 ).pipe(Effect.forkScoped);
132 yield* Effect.yieldNow;
133
134 const error = yield* broker
135 .invoke<void>({ scope, operation: "status", input: {}, timeoutMs: 2_000 })
136 .pipe(Effect.flip);
137
138 expect(error).toBeInstanceOf(PreviewAutomationMalformedResponseError);
139 expect(error).toMatchObject({
140 operation: "status",
141 environmentId: scope.environmentId,
142 threadId: scope.threadId,
143 providerSessionId: scope.providerSessionId,
144 providerInstanceId: scope.providerInstanceId,
145 clientId: "client-1",
146 requestId: "preview-0",
147 timeoutMs: 2_000,
148 });
149 }),
150 ),
151);
152
62it.effect("rejects calls when no focused owner exists", () =>153it.effect("rejects calls when no focused owner exists", () =>
63 Effect.gen(function* () {154 Effect.gen(function* () {
64 const broker = yield* PreviewAutomationBroker.make;155 const broker = yield* PreviewAutomationBroker.make;
...@@ -66,6 +157,13 @@ it.effect("rejects calls when no focused owner exists", () =>...@@ -66,6 +157,13 @@ it.effect("rejects calls when no focused owner exists", () =>
66 .invoke<void>({ scope, operation: "status", input: {} })157 .invoke<void>({ scope, operation: "status", input: {} })
67 .pipe(Effect.flip);158 .pipe(Effect.flip);
68 expect(error).toBeInstanceOf(PreviewAutomationNoFocusedOwnerError);159 expect(error).toBeInstanceOf(PreviewAutomationNoFocusedOwnerError);
160 expect(error).toMatchObject({
161 operation: "status",
162 environmentId: scope.environmentId,
163 threadId: scope.threadId,
164 providerSessionId: scope.providerSessionId,
165 providerInstanceId: scope.providerInstanceId,
166 });
69 }),167 }),
70);168);
71169
...@@ -162,7 +260,17 @@ it.effect("fails requests assigned to a browser stream when that stream reconnec...@@ -162,7 +260,17 @@ it.effect("fails requests assigned to a browser stream when that stream reconnec
162 const _replacementRequests = yield* broker.connect(makeOwner());260 const _replacementRequests = yield* broker.connect(makeOwner());
163261
164 const error = yield* Fiber.join(pending);262 const error = yield* Fiber.join(pending);
165 expect(error).toBeInstanceOf(PreviewAutomationUnavailableError);263 expect(error).toBeInstanceOf(PreviewAutomationClientDisconnectedError);
264 expect(error).toMatchObject({
265 operation: "status",
266 environmentId: scope.environmentId,
267 threadId: scope.threadId,
268 providerSessionId: scope.providerSessionId,
269 providerInstanceId: scope.providerInstanceId,
270 clientId: "client-1",
271 requestId: "preview-0",
272 timeoutMs: 15_000,
273 });
166 }),274 }),
167 ),275 ),
168);276);
apps/server/src/mcp/PreviewAutomationBroker.ts+149-52
...@@ -1,12 +1,16 @@...@@ -1,12 +1,16 @@
1import {1import {
2 PreviewAutomationClientDisconnectedError,
2 PreviewAutomationControlInterruptedError,3 PreviewAutomationControlInterruptedError,
3 PreviewAutomationExecutionError,4 PreviewAutomationExecutionError,
5 PreviewAutomationHostNotConnectedError,
4 PreviewAutomationInvalidSelectorError,6 PreviewAutomationInvalidSelectorError,
7 PreviewAutomationMalformedResponseError,
5 PreviewAutomationNoFocusedOwnerError,8 PreviewAutomationNoFocusedOwnerError,
9 PreviewAutomationRemoteUnavailableError,
10 PreviewAutomationRequestQueueClosedError,
6 PreviewAutomationResultTooLargeError,11 PreviewAutomationResultTooLargeError,
7 PreviewAutomationTabNotFoundError,12 PreviewAutomationTabNotFoundError,
8 PreviewAutomationTimeoutError,13 PreviewAutomationTimeoutError,
9 PreviewAutomationUnavailableError,
10 PreviewAutomationUnsupportedClientError,14 PreviewAutomationUnsupportedClientError,
11 type PreviewAutomationError,15 type PreviewAutomationError,
12 type PreviewAutomationOperation,16 type PreviewAutomationOperation,
...@@ -62,6 +66,21 @@ interface ClientConnection {...@@ -62,6 +66,21 @@ interface ClientConnection {
62interface PendingRequest {66interface PendingRequest {
63 readonly queue: ClientConnection["queue"];67 readonly queue: ClientConnection["queue"];
64 readonly deferred: Deferred.Deferred<unknown, PreviewAutomationError>;68 readonly deferred: Deferred.Deferred<unknown, PreviewAutomationError>;
69 readonly context: PreviewAutomationRequestErrorContext;
70}
71
72interface PreviewAutomationRequestErrorContext {
73 readonly operation: PreviewAutomationOperation;
74 readonly environmentId: McpInvocationContext.McpInvocationScope["environmentId"];
75 readonly threadId: McpInvocationContext.McpInvocationScope["threadId"];
76 readonly providerSessionId: string;
77 readonly providerInstanceId: McpInvocationContext.McpInvocationScope["providerInstanceId"];
78 readonly clientId: string;
79 readonly requestId: string;
80 readonly tabId?: PreviewTabId;
81 readonly timeoutMs: number;
82 readonly selectorKind?: "locator" | "selector";
83 readonly selectorLength?: number;
65}84}
6685
67interface BrokerState {86interface BrokerState {
...@@ -71,48 +90,104 @@ interface BrokerState {...@@ -71,48 +90,104 @@ interface BrokerState {
71 readonly requestSequence: number;90 readonly requestSequence: number;
72}91}
7392
74const makeResponseError = (93const selectorDiagnosticsFromInput = (
94 input: unknown,
95): Pick<PreviewAutomationRequestErrorContext, "selectorKind" | "selectorLength"> => {
96 if (typeof input !== "object" || input === null) return {};
97 if ("locator" in input && typeof input.locator === "string") {
98 return { selectorKind: "locator", selectorLength: input.locator.length };
99 }
100 if ("selector" in input && typeof input.selector === "string") {
101 return { selectorKind: "selector", selectorLength: input.selector.length };
102 }
103 return {};
104};
105
106type RemoteDetailKind = "null" | "array" | "object" | "string" | "number" | "boolean";
107
108function remoteDetailKind(detail: unknown): RemoteDetailKind {
109 if (detail === null) return "null";
110 if (Array.isArray(detail)) return "array";
111 switch (typeof detail) {
112 case "string":
113 return "string";
114 case "number":
115 return "number";
116 case "boolean":
117 return "boolean";
118 default:
119 return "object";
120 }
121}
122
123const classifyResponseError = (
124 context: PreviewAutomationRequestErrorContext,
75 error: NonNullable<PreviewAutomationResponse["error"]>,125 error: NonNullable<PreviewAutomationResponse["error"]>,
76): PreviewAutomationError => {126): PreviewAutomationError => {
127 const remoteDiagnostics = {
128 remoteTag: error._tag,
129 remoteMessageLength: error.message.length,
130 ...(error.detail === undefined ? {} : { remoteDetailKind: remoteDetailKind(error.detail) }),
131 cause: error,
132 };
77 switch (error._tag) {133 switch (error._tag) {
78 case "PreviewAutomationNoFocusedOwnerError":134 case "PreviewAutomationNoFocusedOwnerError":
79 return new PreviewAutomationNoFocusedOwnerError({ message: error.message });135 return new PreviewAutomationNoFocusedOwnerError({
136 ...context,
137 ...remoteDiagnostics,
138 });
80 case "PreviewAutomationUnsupportedClientError":139 case "PreviewAutomationUnsupportedClientError":
81 return new PreviewAutomationUnsupportedClientError({ message: error.message });140 return new PreviewAutomationUnsupportedClientError({
141 ...context,
142 ...remoteDiagnostics,
143 });
82 case "PreviewAutomationTabNotFoundError":144 case "PreviewAutomationTabNotFoundError":
83 return new PreviewAutomationTabNotFoundError({ message: error.message });145 return new PreviewAutomationTabNotFoundError({
146 ...context,
147 ...remoteDiagnostics,
148 });
84 case "PreviewAutomationTimeoutError":149 case "PreviewAutomationTimeoutError":
85 return new PreviewAutomationTimeoutError({ message: error.message });150 return new PreviewAutomationTimeoutError({
151 ...context,
152 ...remoteDiagnostics,
153 });
86 case "PreviewAutomationControlInterruptedError":154 case "PreviewAutomationControlInterruptedError":
87 return new PreviewAutomationControlInterruptedError({ message: error.message });155 return new PreviewAutomationControlInterruptedError({
156 ...context,
157 ...remoteDiagnostics,
158 });
88 case "PreviewAutomationInvalidSelectorError": {159 case "PreviewAutomationInvalidSelectorError": {
89 const detail =
90 typeof error.detail === "object" && error.detail !== null ? error.detail : undefined;
91 return new PreviewAutomationInvalidSelectorError({160 return new PreviewAutomationInvalidSelectorError({
92 message: error.message,161 ...context,
93 selector:162 ...remoteDiagnostics,
94 detail && "selector" in detail && typeof detail.selector === "string"
95 ? detail.selector
96 : "",
97 });163 });
98 }164 }
99 case "PreviewAutomationResultTooLargeError": {165 case "PreviewAutomationResultTooLargeError": {
100 const detail =166 const detail =
101 typeof error.detail === "object" && error.detail !== null ? error.detail : undefined;167 typeof error.detail === "object" && error.detail !== null ? error.detail : undefined;
168 const maximumBytes =
169 detail &&
170 "maximumBytes" in detail &&
171 typeof detail.maximumBytes === "number" &&
172 Number.isInteger(detail.maximumBytes) &&
173 detail.maximumBytes > 0
174 ? detail.maximumBytes
175 : undefined;
102 return new PreviewAutomationResultTooLargeError({176 return new PreviewAutomationResultTooLargeError({
103 message: error.message,177 ...context,
104 maximumBytes:178 ...remoteDiagnostics,
105 detail && "maximumBytes" in detail && typeof detail.maximumBytes === "number"179 ...(maximumBytes === undefined ? {} : { maximumBytes }),
106 ? detail.maximumBytes
107 : 64_000,
108 });180 });
109 }181 }
110 case "PreviewAutomationUnavailableError":182 case "PreviewAutomationUnavailableError":
111 return new PreviewAutomationUnavailableError({ message: error.message });183 return new PreviewAutomationRemoteUnavailableError({
184 ...context,
185 ...remoteDiagnostics,
186 });
112 default:187 default:
113 return new PreviewAutomationExecutionError({188 return new PreviewAutomationExecutionError({
114 message: error.message,189 ...context,
115 detail: error.detail,190 ...remoteDiagnostics,
116 });191 });
117 }192 }
118};193};
...@@ -148,13 +223,8 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {...@@ -148,13 +223,8 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
148 });223 });
149 yield* Effect.forEach(224 yield* Effect.forEach(
150 toFail,225 toFail,
151 ({ deferred }) =>226 ({ deferred, context }) =>
152 Deferred.fail(227 Deferred.fail(deferred, new PreviewAutomationClientDisconnectedError(context)),
153 deferred,
154 new PreviewAutomationUnavailableError({
155 message: "The preview automation client disconnected.",
156 }),
157 ),
158 { discard: true },228 { discard: true },
159 );229 );
160 yield* Queue.shutdown(queue);230 yield* Queue.shutdown(queue);
...@@ -228,10 +298,8 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {...@@ -228,10 +298,8 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
228 yield* Deferred.fail(298 yield* Deferred.fail(
229 pending.deferred,299 pending.deferred,
230 response.error300 response.error
231 ? makeResponseError(response.error)301 ? classifyResponseError(pending.context, response.error)
232 : new PreviewAutomationExecutionError({302 : new PreviewAutomationMalformedResponseError(pending.context),
233 message: "Preview automation failed without an error payload.",
234 }),
235 );303 );
236 }304 }
237 });305 });
...@@ -250,28 +318,60 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {...@@ -250,28 +318,60 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
250 .sort((left, right) => right.focusedAt.localeCompare(left.focusedAt));318 .sort((left, right) => right.focusedAt.localeCompare(left.focusedAt));
251 const owner = candidates.find((candidate) => current.clients.has(candidate.clientId));319 const owner = candidates.find((candidate) => current.clients.has(candidate.clientId));
252 if (!owner) {320 if (!owner) {
253 if (candidates.length > 0) {321 const disconnectedOwner = candidates[0];
254 return yield* new PreviewAutomationUnavailableError({322 if (disconnectedOwner) {
255 message: "The browser host is not connected.",323 return yield* new PreviewAutomationHostNotConnectedError({
324 operation: input.operation,
325 environmentId: input.scope.environmentId,
326 threadId: input.scope.threadId,
327 providerSessionId: input.scope.providerSessionId,
328 providerInstanceId: input.scope.providerInstanceId,
329 clientId: disconnectedOwner.clientId,
256 });330 });
257 }331 }
258 return yield* new PreviewAutomationNoFocusedOwnerError({332 return yield* new PreviewAutomationNoFocusedOwnerError({
259 message: "No desktop browser host is available for this thread.",333 operation: input.operation,
334 environmentId: input.scope.environmentId,
335 threadId: input.scope.threadId,
336 providerSessionId: input.scope.providerSessionId,
337 providerInstanceId: input.scope.providerInstanceId,
260 });338 });
261 }339 }
262 const connection = current.clients.get(owner.clientId);340 const connection = current.clients.get(owner.clientId);
263 if (!connection) {341 if (!connection) {
264 return yield* new PreviewAutomationUnavailableError({342 return yield* new PreviewAutomationHostNotConnectedError({
265 message: "The browser host is not connected.",343 operation: input.operation,
344 environmentId: input.scope.environmentId,
345 threadId: input.scope.threadId,
346 providerSessionId: input.scope.providerSessionId,
347 providerInstanceId: input.scope.providerInstanceId,
348 clientId: owner.clientId,
266 });349 });
267 }350 }
268 const timeoutMs = input.timeoutMs ?? 15_000;351 const timeoutMs = input.timeoutMs ?? 15_000;
269 const deferred = yield* Deferred.make<unknown, PreviewAutomationError>();352 const deferred = yield* Deferred.make<unknown, PreviewAutomationError>();
270 const requestId = yield* SynchronizedRef.modify(state, (next) => {353 const [requestId, requestContext] = yield* SynchronizedRef.modify(state, (next) => {
271 const requestId = `preview-${next.requestSequence}`;354 const requestId = `preview-${next.requestSequence}`;
355 const tabId = input.tabId ?? owner.tabId ?? undefined;
356 const selectorDiagnostics = selectorDiagnosticsFromInput(input.input);
357 const context: PreviewAutomationRequestErrorContext = {
358 operation: input.operation,
359 environmentId: input.scope.environmentId,
360 threadId: input.scope.threadId,
361 providerSessionId: input.scope.providerSessionId,
362 providerInstanceId: input.scope.providerInstanceId,
363 clientId: owner.clientId,
364 requestId,
365 ...(tabId === undefined ? {} : { tabId }),
366 timeoutMs,
367 ...selectorDiagnostics,
368 };
272 const pending = new Map(next.pending);369 const pending = new Map(next.pending);
273 pending.set(requestId, { queue: connection.queue, deferred });370 pending.set(requestId, { queue: connection.queue, deferred, context });
274 return [requestId, { ...next, pending, requestSequence: next.requestSequence + 1 }] as const;371 return [
372 [requestId, context] as const,
373 { ...next, pending, requestSequence: next.requestSequence + 1 },
374 ] as const;
275 });375 });
276 const removePending = SynchronizedRef.update(state, (next) => {376 const removePending = SynchronizedRef.update(state, (next) => {
277 if (!next.pending.has(requestId)) return next;377 if (!next.pending.has(requestId)) return next;
...@@ -283,24 +383,21 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {...@@ -283,24 +383,21 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
283 const offered = yield* Queue.offer(connection.queue, {383 const offered = yield* Queue.offer(connection.queue, {
284 requestId,384 requestId,
285 threadId: input.scope.threadId,385 threadId: input.scope.threadId,
286 tabId: input.tabId ?? owner.tabId ?? undefined,386 tabId: requestContext.tabId,
287 operation: input.operation,387 operation: input.operation,
288 input: input.input,388 input: input.input,
289 timeoutMs,389 timeoutMs,
290 });390 });
291 if (!offered) {391 if (!offered) {
292 return yield* new PreviewAutomationUnavailableError({392 const completion = yield* Deferred.poll(deferred);
293 message: "The preview automation client is no longer accepting requests.",393 if (Option.isSome(completion)) {
294 });394 return (yield* completion.value) as A;
395 }
396 return yield* new PreviewAutomationRequestQueueClosedError(requestContext);
295 }397 }
296 const result = yield* Deferred.await(deferred).pipe(Effect.timeoutOption(timeoutMs));398 const result = yield* Deferred.await(deferred).pipe(Effect.timeoutOption(timeoutMs));
297 return yield* Option.match(result, {399 return yield* Option.match(result, {
298 onNone: () =>400 onNone: () => Effect.fail(new PreviewAutomationTimeoutError(requestContext)),
299 Effect.fail(
300 new PreviewAutomationTimeoutError({
301 message: `Preview automation timed out after ${timeoutMs}ms.`,
302 }),
303 ),
304 onSome: (value) => Effect.succeed(value as A),401 onSome: (value) => Effect.succeed(value as A),
305 });402 });
306 });403 });
packages/contracts/src/previewAutomation.ts+182-16
...@@ -2,6 +2,7 @@ import { Schema } from "effect";...@@ -2,6 +2,7 @@ import { Schema } from "effect";
22
3import { EnvironmentId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts";3import { EnvironmentId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts";
4import { PreviewTabId } from "./preview.ts";4import { PreviewTabId } from "./preview.ts";
5import { ProviderInstanceId } from "./providerInstance.ts";
56
6const BoundedUrl = Schema.String.check(Schema.isTrimmed())7const BoundedUrl = Schema.String.check(Schema.isTrimmed())
7 .check(8 .check(
...@@ -455,45 +456,205 @@ export class PreviewAutomationUnavailableError extends Schema.TaggedErrorClass<P...@@ -455,45 +456,205 @@ export class PreviewAutomationUnavailableError extends Schema.TaggedErrorClass<P
455 { message: Schema.String },456 { message: Schema.String },
456) {}457) {}
457458
459const PreviewAutomationScopeErrorFields = {
460 operation: PreviewAutomationOperation,
461 environmentId: EnvironmentId,
462 threadId: ThreadId,
463 providerSessionId: TrimmedNonEmptyString,
464 providerInstanceId: ProviderInstanceId,
465};
466
467const PreviewAutomationRequestErrorFields = {
468 ...PreviewAutomationScopeErrorFields,
469 clientId: TrimmedNonEmptyString,
470 requestId: TrimmedNonEmptyString,
471 tabId: Schema.optional(PreviewTabId),
472 timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)),
473};
474
475const PreviewAutomationRemoteDiagnosticFields = {
476 remoteTag: TrimmedNonEmptyString,
477 remoteMessageLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
478 remoteDetailKind: Schema.optional(
479 Schema.Literals(["null", "array", "object", "string", "number", "boolean"]),
480 ),
481 cause: Schema.Defect(),
482};
483
484const PreviewAutomationOptionalRemoteDiagnosticFields = {
485 remoteTag: Schema.optional(TrimmedNonEmptyString),
486 remoteMessageLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
487 remoteDetailKind: Schema.optional(
488 Schema.Literals(["null", "array", "object", "string", "number", "boolean"]),
489 ),
490 cause: Schema.optional(Schema.Defect()),
491};
492
458export class PreviewAutomationNoFocusedOwnerError extends Schema.TaggedErrorClass<PreviewAutomationNoFocusedOwnerError>()(493export class PreviewAutomationNoFocusedOwnerError extends Schema.TaggedErrorClass<PreviewAutomationNoFocusedOwnerError>()(
459 "PreviewAutomationNoFocusedOwnerError",494 "PreviewAutomationNoFocusedOwnerError",
460 { message: Schema.String },495 {
461) {}496 ...PreviewAutomationScopeErrorFields,
497 clientId: Schema.optional(TrimmedNonEmptyString),
498 requestId: Schema.optional(TrimmedNonEmptyString),
499 tabId: Schema.optional(PreviewTabId),
500 timeoutMs: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
501 ...PreviewAutomationOptionalRemoteDiagnosticFields,
502 },
503) {
504 override get message(): string {
505 const summary = `No focused preview automation owner is available for ${this.operation} in thread ${this.threadId}.`;
506 return summary;
507 }
508}
462509
463export class PreviewAutomationUnsupportedClientError extends Schema.TaggedErrorClass<PreviewAutomationUnsupportedClientError>()(510export class PreviewAutomationUnsupportedClientError extends Schema.TaggedErrorClass<PreviewAutomationUnsupportedClientError>()(
464 "PreviewAutomationUnsupportedClientError",511 "PreviewAutomationUnsupportedClientError",
465 { message: Schema.String },512 {
466) {}513 ...PreviewAutomationRequestErrorFields,
514 ...PreviewAutomationRemoteDiagnosticFields,
515 },
516) {
517 override get message(): string {
518 return `Preview automation client ${this.clientId} does not support ${this.operation}.`;
519 }
520}
467521
468export class PreviewAutomationTabNotFoundError extends Schema.TaggedErrorClass<PreviewAutomationTabNotFoundError>()(522export class PreviewAutomationTabNotFoundError extends Schema.TaggedErrorClass<PreviewAutomationTabNotFoundError>()(
469 "PreviewAutomationTabNotFoundError",523 "PreviewAutomationTabNotFoundError",
470 { message: Schema.String },524 {
471) {}525 ...PreviewAutomationRequestErrorFields,
526 ...PreviewAutomationRemoteDiagnosticFields,
527 },
528) {
529 override get message(): string {
530 const summary = this.tabId
531 ? `Preview tab ${this.tabId} was not found for ${this.operation}.`
532 : `No active preview tab was found for ${this.operation}.`;
533 return summary;
534 }
535}
472536
473export class PreviewAutomationTimeoutError extends Schema.TaggedErrorClass<PreviewAutomationTimeoutError>()(537export class PreviewAutomationTimeoutError extends Schema.TaggedErrorClass<PreviewAutomationTimeoutError>()(
474 "PreviewAutomationTimeoutError",538 "PreviewAutomationTimeoutError",
475 { message: Schema.String },539 {
476) {}540 ...PreviewAutomationRequestErrorFields,
541 ...PreviewAutomationOptionalRemoteDiagnosticFields,
542 },
543) {
544 override get message(): string {
545 const summary = `Preview automation ${this.operation} timed out after ${this.timeoutMs}ms.`;
546 return summary;
547 }
548}
477549
478export class PreviewAutomationControlInterruptedError extends Schema.TaggedErrorClass<PreviewAutomationControlInterruptedError>()(550export class PreviewAutomationControlInterruptedError extends Schema.TaggedErrorClass<PreviewAutomationControlInterruptedError>()(
479 "PreviewAutomationControlInterruptedError",551 "PreviewAutomationControlInterruptedError",
480 { message: Schema.String },552 {
481) {}553 ...PreviewAutomationRequestErrorFields,
554 ...PreviewAutomationRemoteDiagnosticFields,
555 },
556) {
557 override get message(): string {
558 return `Preview automation ${this.operation} was interrupted on client ${this.clientId}.`;
559 }
560}
482561
483export class PreviewAutomationExecutionError extends Schema.TaggedErrorClass<PreviewAutomationExecutionError>()(562export class PreviewAutomationExecutionError extends Schema.TaggedErrorClass<PreviewAutomationExecutionError>()(
484 "PreviewAutomationExecutionError",563 "PreviewAutomationExecutionError",
485 { message: Schema.String, detail: Schema.optional(Schema.Unknown) },564 {
486) {}565 ...PreviewAutomationRequestErrorFields,
566 ...PreviewAutomationRemoteDiagnosticFields,
567 },
568) {
569 override get message(): string {
570 return `Preview automation ${this.operation} failed on client ${this.clientId}.`;
571 }
572}
487573
488export class PreviewAutomationInvalidSelectorError extends Schema.TaggedErrorClass<PreviewAutomationInvalidSelectorError>()(574export class PreviewAutomationInvalidSelectorError extends Schema.TaggedErrorClass<PreviewAutomationInvalidSelectorError>()(
489 "PreviewAutomationInvalidSelectorError",575 "PreviewAutomationInvalidSelectorError",
490 { message: Schema.String, selector: Schema.String },576 {
491) {}577 ...PreviewAutomationRequestErrorFields,
578 ...PreviewAutomationRemoteDiagnosticFields,
579 selectorKind: Schema.optional(Schema.Literals(["locator", "selector"])),
580 selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
581 },
582) {
583 override get message(): string {
584 if (this.selectorKind !== undefined && this.selectorLength !== undefined) {
585 return `Preview automation ${this.operation} received an invalid ${this.selectorKind} (${this.selectorLength} characters).`;
586 }
587 return `Preview automation ${this.operation} received an invalid selector.`;
588 }
589}
492590
493export class PreviewAutomationResultTooLargeError extends Schema.TaggedErrorClass<PreviewAutomationResultTooLargeError>()(591export class PreviewAutomationResultTooLargeError extends Schema.TaggedErrorClass<PreviewAutomationResultTooLargeError>()(
494 "PreviewAutomationResultTooLargeError",592 "PreviewAutomationResultTooLargeError",
495 { message: Schema.String, maximumBytes: Schema.Int },593 {
496) {}594 ...PreviewAutomationRequestErrorFields,
595 ...PreviewAutomationRemoteDiagnosticFields,
596 maximumBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
597 },
598) {
599 override get message(): string {
600 const summary =
601 this.maximumBytes === undefined
602 ? `Preview automation ${this.operation} produced a result that is too large.`
603 : `Preview automation ${this.operation} produced a result larger than ${this.maximumBytes} bytes.`;
604 return summary;
605 }
606}
607
608export class PreviewAutomationHostNotConnectedError extends Schema.TaggedErrorClass<PreviewAutomationHostNotConnectedError>()(
609 "PreviewAutomationHostNotConnectedError",
610 {
611 ...PreviewAutomationScopeErrorFields,
612 clientId: TrimmedNonEmptyString,
613 },
614) {
615 override get message(): string {
616 return `Preview automation host ${this.clientId} is not connected for ${this.operation}.`;
617 }
618}
619
620export class PreviewAutomationClientDisconnectedError extends Schema.TaggedErrorClass<PreviewAutomationClientDisconnectedError>()(
621 "PreviewAutomationClientDisconnectedError",
622 PreviewAutomationRequestErrorFields,
623) {
624 override get message(): string {
625 return `Preview automation client ${this.clientId} disconnected during ${this.operation}.`;
626 }
627}
628
629export class PreviewAutomationRequestQueueClosedError extends Schema.TaggedErrorClass<PreviewAutomationRequestQueueClosedError>()(
630 "PreviewAutomationRequestQueueClosedError",
631 PreviewAutomationRequestErrorFields,
632) {
633 override get message(): string {
634 return `Preview automation client ${this.clientId} stopped accepting ${this.operation} requests.`;
635 }
636}
637
638export class PreviewAutomationRemoteUnavailableError extends Schema.TaggedErrorClass<PreviewAutomationRemoteUnavailableError>()(
639 "PreviewAutomationRemoteUnavailableError",
640 {
641 ...PreviewAutomationRequestErrorFields,
642 ...PreviewAutomationRemoteDiagnosticFields,
643 },
644) {
645 override get message(): string {
646 return `Preview automation ${this.operation} is unavailable on client ${this.clientId}.`;
647 }
648}
649
650export class PreviewAutomationMalformedResponseError extends Schema.TaggedErrorClass<PreviewAutomationMalformedResponseError>()(
651 "PreviewAutomationMalformedResponseError",
652 PreviewAutomationRequestErrorFields,
653) {
654 override get message(): string {
655 return `Preview automation client ${this.clientId} returned a malformed response for ${this.operation}.`;
656 }
657}
497658
498export const PreviewAutomationError = Schema.Union([659export const PreviewAutomationError = Schema.Union([
499 PreviewAutomationUnavailableError,660 PreviewAutomationUnavailableError,
...@@ -505,6 +666,11 @@ export const PreviewAutomationError = Schema.Union([...@@ -505,6 +666,11 @@ export const PreviewAutomationError = Schema.Union([
505 PreviewAutomationExecutionError,666 PreviewAutomationExecutionError,
506 PreviewAutomationInvalidSelectorError,667 PreviewAutomationInvalidSelectorError,
507 PreviewAutomationResultTooLargeError,668 PreviewAutomationResultTooLargeError,
669 PreviewAutomationHostNotConnectedError,
670 PreviewAutomationClientDisconnectedError,
671 PreviewAutomationRequestQueueClosedError,
672 PreviewAutomationRemoteUnavailableError,
673 PreviewAutomationMalformedResponseError,
508]);674]);
509export type PreviewAutomationError = typeof PreviewAutomationError.Type;675export type PreviewAutomationError = typeof PreviewAutomationError.Type;
510676