fix(sdk): align hotpath observability with actual dispatch mode

This commit is contained in:
Tom Boucher
2026-05-05 20:22:03 -04:00
parent 8ad2e3877f
commit fe16143e29
3 changed files with 73 additions and 5 deletions

View File

@@ -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",
);
});

View File

@@ -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',
}),
);
});
});

View File

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