From 00ba404b60aaad5b1890f96796cd3873cb2163a3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 5 May 2026 19:31:03 -0400 Subject: [PATCH] test(sdk): enforce runtime bridge seam and explicit no-fallback behavior --- sdk/src/gsd-transport.test.ts | 26 +++++++++++++++++++++ sdk/src/gsd-transport.ts | 12 +++++++++- sdk/src/query-runtime-seam-coverage.test.ts | 20 ++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 sdk/src/query-runtime-seam-coverage.test.ts diff --git a/sdk/src/gsd-transport.test.ts b/sdk/src/gsd-transport.test.ts index af5a71669..2a0f3a2b0 100644 --- a/sdk/src/gsd-transport.test.ts +++ b/sdk/src/gsd-transport.test.ts @@ -234,6 +234,32 @@ describe('GSDTransport', () => { expect(adapters.execSubprocessJson).toHaveBeenCalledOnce(); }); + it('fails when command is unregistered and subprocess fallback is disabled', async () => { + const registry = new QueryRegistry(); + + const adapters = { + dispatchNative: vi.fn(async () => ({ data: { ok: true } })), + execSubprocessJson: vi.fn(async () => ({ ok: 'fallback' })), + execSubprocessRaw: vi.fn(async () => 'fallback-raw'), + }; + + const transport = new GSDTransport(registry, adapters); + + await expect(transport.run({ + legacyCommand: 'unknown', + legacyArgs: [], + registryCommand: 'unknown', + registryArgs: [], + mode: 'json', + projectDir: '/tmp', + }, { + preferNative: true, + allowFallbackToSubprocess: false, + })).rejects.toThrow("Subprocess fallback disabled"); + + expect(adapters.execSubprocessJson).not.toHaveBeenCalled(); + }); + it('forces raw subprocess path when workstream present and mode is raw', async () => { const registry = new QueryRegistry(); registry.register('commit', async () => ({ data: { hash: 'abc' } })); diff --git a/sdk/src/gsd-transport.ts b/sdk/src/gsd-transport.ts index 92409dbbb..3857d2886 100644 --- a/sdk/src/gsd-transport.ts +++ b/sdk/src/gsd-transport.ts @@ -2,6 +2,7 @@ import type { QueryResult } from './query/utils.js'; import type { QueryRegistry } from './query/registry.js'; import type { TransportMode } from './gsd-transport-policy.js'; import { toFailureSignal } from './query-failure-classification.js'; +import { GSDToolsError } from './gsd-tools-error.js'; export interface TransportRequest { legacyCommand: string; @@ -52,7 +53,16 @@ export class GSDTransport { onDecision?.({ dispatchMode: 'subprocess', reason: 'native_failure_fallback' }); } } else { - onDecision?.({ dispatchMode: 'subprocess', reason: this.subprocessReason(request, policy) }); + const reason = this.subprocessReason(request, policy); + onDecision?.({ dispatchMode: 'subprocess', reason }); + if (!policy.allowFallbackToSubprocess) { + throw GSDToolsError.failure( + `Subprocess fallback disabled: command '${request.registryCommand}' cannot run without native dispatch`, + request.legacyCommand, + request.legacyArgs, + null, + ); + } } return this.dispatchSubprocess(request); diff --git a/sdk/src/query-runtime-seam-coverage.test.ts b/sdk/src/query-runtime-seam-coverage.test.ts new file mode 100644 index 000000000..325a67ddf --- /dev/null +++ b/sdk/src/query-runtime-seam-coverage.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createGSDToolsRuntime } from './query-gsd-tools-runtime.js'; + +describe('SDK Runtime Bridge seam coverage', () => { + it('exposes bridge as the single runtime seam', () => { + const runtime = createGSDToolsRuntime({ + projectDir: '/tmp/project', + gsdToolsPath: '/tmp/gsd-tools.cjs', + timeoutMs: 1_000, + shouldUseNativeQuery: () => true, + execJsonFallback: vi.fn(async () => ({})), + execRawFallback: vi.fn(async () => ''), + }); + + expect(Object.keys(runtime)).toEqual(['bridge']); + expect(typeof runtime.bridge.resolve).toBe('function'); + expect(typeof runtime.bridge.execute).toBe('function'); + expect(typeof runtime.bridge.dispatchHotpath).toBe('function'); + }); +});