feat(sdk): add strict mode and explicit fallback policy to runtime bridge
This commit is contained in:
@@ -171,20 +171,46 @@ describe('GSDTools', () => {
|
||||
`process.stdout.write(JSON.stringify({ from: 'subprocess-fallback' }));`,
|
||||
);
|
||||
|
||||
const tools = new GSDTools({ projectDir: tmpDir, gsdToolsPath: scriptPath });
|
||||
const tools = new GSDTools({
|
||||
projectDir: tmpDir,
|
||||
gsdToolsPath: scriptPath,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
setTransportPolicy('verify.path-exists', { allowFallbackToSubprocess: true });
|
||||
|
||||
const result = await tools.exec('verify.path-exists', []);
|
||||
expect(result).toEqual({ from: 'subprocess-fallback' });
|
||||
});
|
||||
|
||||
it('fails fast in strictSdk mode when command has no native adapter', async () => {
|
||||
const scriptPath = await createScript(
|
||||
'strict-should-not-run.cjs',
|
||||
`process.stdout.write(JSON.stringify({ should: 'not-run' }));`,
|
||||
);
|
||||
|
||||
const tools = new GSDTools({
|
||||
projectDir: tmpDir,
|
||||
gsdToolsPath: scriptPath,
|
||||
strictSdk: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
|
||||
await expect(tools.exec('nonexistent-command', [])).rejects.toThrow(
|
||||
"Strict SDK mode: command 'nonexistent-command' has no native adapter",
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves GSDToolsError contract when native handler throws and fallback disabled', async () => {
|
||||
const scriptPath = await createScript(
|
||||
'should-not-run.cjs',
|
||||
`process.stdout.write(JSON.stringify({ should: 'not-run' }));`,
|
||||
);
|
||||
|
||||
const tools = new GSDTools({ projectDir: tmpDir, gsdToolsPath: scriptPath });
|
||||
const tools = new GSDTools({
|
||||
projectDir: tmpDir,
|
||||
gsdToolsPath: scriptPath,
|
||||
allowFallbackToSubprocess: false,
|
||||
});
|
||||
setTransportPolicy('verify.path-exists', { allowFallbackToSubprocess: false });
|
||||
|
||||
try {
|
||||
|
||||
@@ -53,6 +53,10 @@ export class GSDTools {
|
||||
* Set false in tests that substitute a mock `gsdToolsPath` script.
|
||||
*/
|
||||
preferNativeQuery?: boolean;
|
||||
/** When true, fail if a command has no native registry adapter. */
|
||||
strictSdk?: boolean;
|
||||
/** Explicit subprocess bridge policy. Default false for SDK-native mode. */
|
||||
allowFallbackToSubprocess?: boolean;
|
||||
}) {
|
||||
this.projectDir = opts.projectDir;
|
||||
this.gsdToolsPath =
|
||||
@@ -71,6 +75,8 @@ export class GSDTools {
|
||||
shouldUseNativeQuery: () => this.shouldUseNativeQuery(),
|
||||
execJsonFallback: (legacyCommand, legacyArgs) => this.exec(legacyCommand, legacyArgs),
|
||||
execRawFallback: (legacyCommand, legacyArgs) => this.execRaw(legacyCommand, legacyArgs),
|
||||
strictSdk: opts.strictSdk,
|
||||
allowFallbackToSubprocess: opts.allowFallbackToSubprocess ?? false,
|
||||
});
|
||||
|
||||
this.bridge = runtime.bridge;
|
||||
|
||||
@@ -28,4 +28,25 @@ describe('QueryExecutionPolicy', () => {
|
||||
expect(policyArg).toEqual({ preferNative: true, allowFallbackToSubprocess: false });
|
||||
|
||||
});
|
||||
|
||||
it('allows runtime override for allowFallbackToSubprocess', async () => {
|
||||
const run = vi.fn().mockResolvedValue({ ok: true });
|
||||
const policy = new QueryExecutionPolicy({ run } as never);
|
||||
|
||||
setTransportPolicy('verify.path-exists', { preferNative: true, allowFallbackToSubprocess: true });
|
||||
|
||||
await policy.execute({
|
||||
legacyCommand: 'verify.path-exists',
|
||||
legacyArgs: [],
|
||||
registryCommand: 'verify.path-exists',
|
||||
registryArgs: [],
|
||||
mode: 'json',
|
||||
projectDir: '/tmp/project',
|
||||
preferNativeQuery: true,
|
||||
allowFallbackToSubprocess: false,
|
||||
});
|
||||
|
||||
const [, policyArg] = run.mock.calls[0];
|
||||
expect(policyArg).toEqual({ preferNative: true, allowFallbackToSubprocess: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface QueryExecutionRequest {
|
||||
projectDir: string;
|
||||
workstream?: string;
|
||||
preferNativeQuery: boolean;
|
||||
allowFallbackToSubprocess?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +36,8 @@ export class QueryExecutionPolicy {
|
||||
},
|
||||
{
|
||||
preferNative: request.preferNativeQuery && policy.preferNative,
|
||||
allowFallbackToSubprocess: policy.allowFallbackToSubprocess,
|
||||
allowFallbackToSubprocess:
|
||||
request.allowFallbackToSubprocess ?? policy.allowFallbackToSubprocess,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export function createGSDToolsRuntime(opts: {
|
||||
shouldUseNativeQuery: () => boolean;
|
||||
execJsonFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<unknown>;
|
||||
execRawFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<string>;
|
||||
strictSdk?: boolean;
|
||||
allowFallbackToSubprocess?: boolean;
|
||||
}): GSDToolsRuntime {
|
||||
const registry = createRegistry(opts.eventStream, opts.sessionId);
|
||||
|
||||
@@ -69,6 +71,10 @@ export function createGSDToolsRuntime(opts: {
|
||||
executionPolicy,
|
||||
nativeHotpathAdapter,
|
||||
opts.shouldUseNativeQuery,
|
||||
{
|
||||
strictSdk: opts.strictSdk,
|
||||
allowFallbackToSubprocess: opts.allowFallbackToSubprocess,
|
||||
},
|
||||
);
|
||||
|
||||
return { bridge };
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { QueryCommandResolution } from './query/query-command-resolution-st
|
||||
import { resolveQueryCommand } from './query/query-command-resolution-strategy.js';
|
||||
import { QueryExecutionPolicy } from './query-execution-policy.js';
|
||||
import { QueryNativeHotpathAdapter } from './query-native-hotpath-adapter.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
export interface RuntimeBridgeExecuteInput {
|
||||
legacyCommand: string;
|
||||
@@ -25,6 +26,10 @@ export class QueryRuntimeBridge {
|
||||
private readonly executionPolicy: QueryExecutionPolicy,
|
||||
private readonly nativeHotpathAdapter: QueryNativeHotpathAdapter,
|
||||
private readonly shouldUseNativeQuery: () => boolean,
|
||||
private readonly options?: {
|
||||
strictSdk?: boolean;
|
||||
allowFallbackToSubprocess?: boolean;
|
||||
},
|
||||
) {}
|
||||
|
||||
getRegistry(): QueryRegistry {
|
||||
@@ -36,6 +41,15 @@ export class QueryRuntimeBridge {
|
||||
}
|
||||
|
||||
async execute(input: RuntimeBridgeExecuteInput): Promise<unknown> {
|
||||
if (this.options?.strictSdk && !this.registry.has(input.registryCommand)) {
|
||||
throw GSDToolsError.failure(
|
||||
`Strict SDK mode: command '${input.registryCommand}' has no native adapter`,
|
||||
input.legacyCommand,
|
||||
input.legacyArgs,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
return this.executionPolicy.execute({
|
||||
legacyCommand: input.legacyCommand,
|
||||
legacyArgs: input.legacyArgs,
|
||||
@@ -45,6 +59,7 @@ export class QueryRuntimeBridge {
|
||||
projectDir: input.projectDir,
|
||||
workstream: input.workstream,
|
||||
preferNativeQuery: this.shouldUseNativeQuery(),
|
||||
allowFallbackToSubprocess: this.options?.allowFallbackToSubprocess,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user