refactor: introduce explicit query error seam contracts

This commit is contained in:
Tom Boucher
2026-05-04 20:12:11 -04:00
parent c7d3f83b8b
commit b9e3979fc1
5 changed files with 55 additions and 33 deletions

View File

@@ -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<typeof createRegistry>;
@@ -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, {

View File

@@ -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);

View File

@@ -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<QueryResult>;
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<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(
this.deps.createTimeoutError(
this.deps.createNativeTimeoutError(
timeoutMessage(legacyCommand, legacyArgs, this.deps.timeoutMs),
legacyCommand,
legacyArgs,

View File

@@ -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 {

View File

@@ -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;
}