From 009cfb15629e7956498a74d581be972fe6f2ee9c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 4 May 2026 20:06:30 -0400 Subject: [PATCH] refactor: split native adapter timeout and failure seams --- sdk/src/query-gsd-tools-runtime.ts | 2 ++ sdk/src/query-native-direct-adapter.test.ts | 35 +++++++++++++++++++++ sdk/src/query-native-direct-adapter.ts | 11 +++++-- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 sdk/src/query-native-direct-adapter.test.ts diff --git a/sdk/src/query-gsd-tools-runtime.ts b/sdk/src/query-gsd-tools-runtime.ts index 3ea879768..1db59bc82 100644 --- a/sdk/src/query-gsd-tools-runtime.ts +++ b/sdk/src/query-gsd-tools-runtime.ts @@ -44,6 +44,8 @@ export function createGSDToolsRuntime(opts: { dispatch: (registryCommand, registryArgs) => registry.dispatch(registryCommand, registryArgs, opts.projectDir), createTimeoutError: (message, command, args) => GSDToolsError.timeout(message, command, args, '', opts.timeoutMs), + createFailureError: (message, command, args, cause) => + GSDToolsError.failure(message, command, args, 1, '', { cause }), }); 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 new file mode 100644 index 000000000..aa06edf08 --- /dev/null +++ b/sdk/src/query-native-direct-adapter.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { GSDToolsError } from './gsd-tools-error.js'; +import { QueryNativeDirectAdapter } from './query-native-direct-adapter.js'; + +describe('QueryNativeDirectAdapter', () => { + it('wraps native failures as typed failure errors', async () => { + const adapter = new QueryNativeDirectAdapter({ + timeoutMs: 1000, + 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 }), + }); + + await expect(adapter.dispatchJson('state', ['load'], 'state.load', [])).rejects.toMatchObject({ + classification: { kind: 'failure' }, + command: 'state', + }); + }); + + it('preserves timeout errors', async () => { + const timeoutErr = GSDToolsError.timeout('timeout', 'state', ['load']); + const adapter = new QueryNativeDirectAdapter({ + timeoutMs: 1000, + 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 }), + }); + + 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 96a4982e5..673af1cc2 100644 --- a/sdk/src/query-native-direct-adapter.ts +++ b/sdk/src/query-native-direct-adapter.ts @@ -1,11 +1,13 @@ import { formatQueryRawOutput } from './query-raw-output-projection.js'; -import { timeoutMessage } from './query-failure-classification.js'; +import { GSDToolsError } from './gsd-tools-error.js'; +import { errorMessage, timeoutMessage } from './query-failure-classification.js'; import type { QueryResult } from './query/utils.js'; export interface QueryNativeDirectAdapterDeps { timeoutMs: number; dispatch: (registryCommand: string, registryArgs: string[]) => Promise; createTimeoutError: (message: string, command: string, args: string[]) => Error; + createFailureError: (message: string, command: string, args: string[], cause: unknown) => Error; } /** @@ -15,7 +17,12 @@ export class QueryNativeDirectAdapter { constructor(private readonly deps: QueryNativeDirectAdapterDeps) {} async dispatchResult(legacyCommand: string, legacyArgs: string[], registryCommand: string, registryArgs: string[]): Promise { - return this.withTimeout(legacyCommand, legacyArgs, this.deps.dispatch(registryCommand, registryArgs)); + try { + 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); + } } async dispatchJson(legacyCommand: string, legacyArgs: string[], registryCommand: string, registryArgs: string[]): Promise {