From fe16143e2920a8790f65c298590ec8bb48508a72 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 5 May 2026 20:22:03 -0400 Subject: [PATCH] fix(sdk): align hotpath observability with actual dispatch mode --- sdk/src/gsd-tools.test.ts | 4 +-- sdk/src/query-runtime-bridge.test.ts | 46 ++++++++++++++++++++++++++++ sdk/src/query-runtime-bridge.ts | 28 +++++++++++++++-- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/sdk/src/gsd-tools.test.ts b/sdk/src/gsd-tools.test.ts index 55163cdb8..035fdbe50 100644 --- a/sdk/src/gsd-tools.test.ts +++ b/sdk/src/gsd-tools.test.ts @@ -195,8 +195,8 @@ describe('GSDTools', () => { allowFallbackToSubprocess: true, }); - await expect(tools.exec('nonexistent-command', [])).rejects.toThrow( - "Strict SDK mode: command 'nonexistent-command' has no native adapter", + await expect(tools.exec('graphify', [])).rejects.toThrow( + "Strict SDK mode: command 'graphify' has no native adapter", ); }); diff --git a/sdk/src/query-runtime-bridge.test.ts b/sdk/src/query-runtime-bridge.test.ts index d7dfe494a..903ec3f2b 100644 --- a/sdk/src/query-runtime-bridge.test.ts +++ b/sdk/src/query-runtime-bridge.test.ts @@ -99,4 +99,50 @@ describe('QueryRuntimeBridge observability', () => { }), ); }); + + it('emits subprocess hotpath event when native query is disabled', async () => { + const onDispatchEvent = vi.fn(); + const bridge = new QueryRuntimeBridge( + { has: () => true } as never, + { execute: vi.fn() } as never, + { dispatch: vi.fn(async () => 'ok') } as never, + () => false, + { onDispatchEvent, allowFallbackToSubprocess: true }, + ); + + await bridge.dispatchHotpath('commit', ['msg'], 'commit', ['msg'], 'raw'); + + expect(onDispatchEvent).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'query_hotpath_dispatch', + command: 'commit', + dispatchMode: 'subprocess', + outcome: 'success', + }), + ); + }); + + it('blocks subprocess hotpath when fallback is disabled', async () => { + const onDispatchEvent = vi.fn(); + const bridge = new QueryRuntimeBridge( + { has: () => true } as never, + { execute: vi.fn() } as never, + { dispatch: vi.fn(async () => 'ok') } as never, + () => false, + { onDispatchEvent, allowFallbackToSubprocess: false }, + ); + + await expect( + bridge.dispatchHotpath('commit', ['msg'], 'commit', ['msg'], 'raw'), + ).rejects.toThrow("Subprocess fallback disabled"); + + expect(onDispatchEvent).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'query_hotpath_dispatch', + command: 'commit', + dispatchMode: 'subprocess', + outcome: 'error', + }), + ); + }); }); diff --git a/sdk/src/query-runtime-bridge.ts b/sdk/src/query-runtime-bridge.ts index 5011d4421..ea2611bde 100644 --- a/sdk/src/query-runtime-bridge.ts +++ b/sdk/src/query-runtime-bridge.ts @@ -34,7 +34,7 @@ export interface RuntimeBridgeHotpathEvent { command: string; legacyCommand: string; mode: TransportMode; - dispatchMode: 'native_hotpath'; + dispatchMode: 'native_hotpath' | 'subprocess'; durationMs: number; outcome: 'success' | 'error'; errorKind?: 'timeout' | 'failure'; @@ -153,6 +153,28 @@ export class QueryRuntimeBridge { mode: TransportMode, ): Promise { const startedAt = Date.now(); + const useNative = this.shouldUseNativeQuery(); + + if (!useNative && this.options?.allowFallbackToSubprocess === false) { + const error = GSDToolsError.failure( + `Subprocess fallback disabled: command '${registryCommand}' cannot run without native dispatch`, + legacyCommand, + legacyArgs, + null, + ); + this.emit({ + type: 'query_hotpath_dispatch', + command: registryCommand, + legacyCommand, + mode, + dispatchMode: 'subprocess', + durationMs: Date.now() - startedAt, + outcome: 'error', + errorKind: 'failure', + }); + throw error; + } + try { const result = await this.nativeHotpathAdapter.dispatch( legacyCommand, @@ -166,7 +188,7 @@ export class QueryRuntimeBridge { command: registryCommand, legacyCommand, mode, - dispatchMode: 'native_hotpath', + dispatchMode: useNative ? 'native_hotpath' : 'subprocess', durationMs: Date.now() - startedAt, outcome: 'success', }); @@ -178,7 +200,7 @@ export class QueryRuntimeBridge { command: registryCommand, legacyCommand, mode, - dispatchMode: 'native_hotpath', + dispatchMode: useNative ? 'native_hotpath' : 'subprocess', durationMs: Date.now() - startedAt, outcome: 'error', errorKind: kind,