From b9e3979fc152a7883f67b9bbaecc6da23f78cdd2 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 4 May 2026 20:12:11 -0400 Subject: [PATCH] refactor: introduce explicit query error seam contracts --- sdk/src/query-gsd-tools-runtime.ts | 25 ++++++++++++------ sdk/src/query-native-direct-adapter.test.ts | 8 +++--- sdk/src/query-native-direct-adapter.ts | 9 +++---- sdk/src/query-subprocess-adapter.ts | 18 ++----------- sdk/src/query-tools-error-seam.ts | 28 +++++++++++++++++++++ 5 files changed, 55 insertions(+), 33 deletions(-) create mode 100644 sdk/src/query-tools-error-seam.ts diff --git a/sdk/src/query-gsd-tools-runtime.ts b/sdk/src/query-gsd-tools-runtime.ts index d58b38e3f..22a00f4fb 100644 --- a/sdk/src/query-gsd-tools-runtime.ts +++ b/sdk/src/query-gsd-tools-runtime.ts @@ -8,6 +8,7 @@ import { QueryNativeDirectAdapter } from './query-native-direct-adapter.js'; import { QueryNativeHotpathAdapter } from './query-native-hotpath-adapter.js'; import { formatQueryRawOutput } from './query-raw-output-projection.js'; import { failureToolsError, timeoutToolsError } from './query-tools-error-factory.js'; +import type { QueryNativeErrorFactory, QueryToolsErrorFactory } from './query-tools-error-seam.js'; export interface GSDToolsRuntime { registry: ReturnType; @@ -28,24 +29,32 @@ export function createGSDToolsRuntime(opts: { }): GSDToolsRuntime { const registry = createRegistry(opts.eventStream, opts.sessionId); + const queryToolsErrorFactory: QueryToolsErrorFactory = { + createTimeoutError: (message, command, args, stderr, timeoutMs) => + timeoutToolsError(message, command, args, stderr, timeoutMs), + createFailureError: (message, command, args, exitCode, stderr) => + failureToolsError(message, command, args, exitCode, stderr), + }; + const subprocessAdapter = new QuerySubprocessAdapter({ projectDir: opts.projectDir, gsdToolsPath: opts.gsdToolsPath, timeoutMs: opts.timeoutMs, workstream: opts.workstream, - createTimeoutError: (message, command, args, stderr, timeoutMs) => - timeoutToolsError(message, command, args, stderr, timeoutMs), - createFailureError: (message, command, args, exitCode, stderr) => - failureToolsError(message, command, args, exitCode, stderr), + ...queryToolsErrorFactory, }); + const nativeErrorFactory: QueryNativeErrorFactory = { + createNativeTimeoutError: (message, command, args) => + timeoutToolsError(message, command, args, '', opts.timeoutMs), + createNativeFailureError: (message, command, args, cause) => + failureToolsError(message, command, args, 1, '', cause), + }; + const nativeDirectAdapter = new QueryNativeDirectAdapter({ timeoutMs: opts.timeoutMs, dispatch: (registryCommand, registryArgs) => registry.dispatch(registryCommand, registryArgs, opts.projectDir), - createTimeoutError: (message, command, args) => - timeoutToolsError(message, command, args, '', opts.timeoutMs), - createFailureError: (message, command, args, cause) => - failureToolsError(message, command, args, 1, '', cause), + ...nativeErrorFactory, }); const transport = new GSDTransport(registry, { diff --git a/sdk/src/query-native-direct-adapter.test.ts b/sdk/src/query-native-direct-adapter.test.ts index a8dd2c5b9..d966fc737 100644 --- a/sdk/src/query-native-direct-adapter.test.ts +++ b/sdk/src/query-native-direct-adapter.test.ts @@ -9,8 +9,8 @@ describe('QueryNativeDirectAdapter', () => { dispatch: async () => { throw new Error('boom'); }, - createTimeoutError: (message, command, args) => GSDToolsError.timeout(message, command, args), - createFailureError: (message, command, args, cause) => GSDToolsError.failure(message, command, args, 1, '', { cause }), + createNativeTimeoutError: (message, command, args) => GSDToolsError.timeout(message, command, args), + createNativeFailureError: (message, command, args, cause) => GSDToolsError.failure(message, command, args, 1, '', { cause }), }); await expect(adapter.dispatchJson('state', ['load'], 'state.load', [])).rejects.toMatchObject({ @@ -26,8 +26,8 @@ describe('QueryNativeDirectAdapter', () => { dispatch: async () => { throw timeoutErr; }, - createTimeoutError: (message, command, args) => GSDToolsError.timeout(message, command, args), - createFailureError: (message, command, args, cause) => GSDToolsError.failure(message, command, args, 1, '', { cause }), + createNativeTimeoutError: (message, command, args) => GSDToolsError.timeout(message, command, args), + createNativeFailureError: (message, command, args, cause) => GSDToolsError.failure(message, command, args, 1, '', { cause }), }); await expect(adapter.dispatchJson('state', ['load'], 'state.load', [])).rejects.toBe(timeoutErr); diff --git a/sdk/src/query-native-direct-adapter.ts b/sdk/src/query-native-direct-adapter.ts index ba6e2060d..0aa4a6ac8 100644 --- a/sdk/src/query-native-direct-adapter.ts +++ b/sdk/src/query-native-direct-adapter.ts @@ -1,13 +1,12 @@ import { formatQueryRawOutput } from './query-raw-output-projection.js'; import { GSDToolsError } from './gsd-tools-error.js'; import { errorMessage, timeoutMessage } from './query-failure-classification.js'; +import type { QueryNativeErrorFactory } from './query-tools-error-seam.js'; import type { QueryResult } from './query/utils.js'; -export interface QueryNativeDirectAdapterDeps { +export interface QueryNativeDirectAdapterDeps extends QueryNativeErrorFactory { timeoutMs: number; dispatch: (registryCommand: string, registryArgs: string[]) => Promise; - createTimeoutError: (message: string, command: string, args: string[]) => GSDToolsError; - createFailureError: (message: string, command: string, args: string[], cause: unknown) => GSDToolsError; } /** @@ -21,7 +20,7 @@ export class QueryNativeDirectAdapter { return await this.withTimeout(legacyCommand, legacyArgs, this.deps.dispatch(registryCommand, registryArgs)); } catch (error) { if (error instanceof GSDToolsError) throw error; - throw this.deps.createFailureError(errorMessage(error), legacyCommand, legacyArgs, error); + throw this.deps.createNativeFailureError(errorMessage(error), legacyCommand, legacyArgs, error); } } @@ -40,7 +39,7 @@ export class QueryNativeDirectAdapter { const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject( - this.deps.createTimeoutError( + this.deps.createNativeTimeoutError( timeoutMessage(legacyCommand, legacyArgs, this.deps.timeoutMs), legacyCommand, legacyArgs, diff --git a/sdk/src/query-subprocess-adapter.ts b/sdk/src/query-subprocess-adapter.ts index ce940212d..8fac76050 100644 --- a/sdk/src/query-subprocess-adapter.ts +++ b/sdk/src/query-subprocess-adapter.ts @@ -1,27 +1,13 @@ import { execFile } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { timeoutMessage } from './query-failure-classification.js'; -import type { GSDToolsError } from './gsd-tools-error.js'; +import type { QueryToolsErrorFactory } from './query-tools-error-seam.js'; -export interface QuerySubprocessAdapterDeps { +export interface QuerySubprocessAdapterDeps extends QueryToolsErrorFactory { projectDir: string; gsdToolsPath: string; timeoutMs: number; workstream?: string; - createTimeoutError: ( - message: string, - command: string, - args: string[], - stderr: string, - timeoutMs: number, - ) => GSDToolsError; - createFailureError: ( - message: string, - command: string, - args: string[], - exitCode: number | null, - stderr: string, - ) => GSDToolsError; } export class QuerySubprocessAdapter { diff --git a/sdk/src/query-tools-error-seam.ts b/sdk/src/query-tools-error-seam.ts new file mode 100644 index 000000000..def5697a3 --- /dev/null +++ b/sdk/src/query-tools-error-seam.ts @@ -0,0 +1,28 @@ +import type { GSDToolsError } from './gsd-tools-error.js'; + +export interface QueryTimeoutErrorFactory { + createTimeoutError: ( + message: string, + command: string, + args: string[], + stderr: string, + timeoutMs: number, + ) => GSDToolsError; +} + +export interface QueryFailureErrorFactory { + createFailureError: ( + message: string, + command: string, + args: string[], + exitCode: number | null, + stderr: string, + ) => GSDToolsError; +} + +export type QueryToolsErrorFactory = QueryTimeoutErrorFactory & QueryFailureErrorFactory; + +export interface QueryNativeErrorFactory { + createNativeTimeoutError: (message: string, command: string, args: string[]) => GSDToolsError; + createNativeFailureError: (message: string, command: string, args: string[], cause: unknown) => GSDToolsError; +}