Files
msd-core/sdk/src/query/query-dispatch.test.ts
Tom Boucher 5975f06b6a refactor(query): extract command catalog seam for registry wiring (#3060)
* refactor(sdk): extract gsdtools transport seam with per-command policy

* refactor(query): centralize registry command catalog wiring

* refactor(query): unify command resolution seam across sdk callers

* fix(sdk): address CodeRabbit transport policy and timeout findings

* refactor(query): extract mutation event mapper seam

* refactor(query): converge mutation and transport policy data

* refactor(query): share fallback orchestration across cli and sdk

* refactor(query): split static registry catalog by domain clusters

* refactor(query): extract mutation event emission decorator seam

* refactor(query): extract alias-family handler catalog module

* refactor(query): extract cjs fallback execution adapter

* refactor(query): deepen command semantics seam

* refactor(query): extract deep dispatch seam

* refactor(query): deepen cjs fallback execution seam

* refactor(query): merge routing plan into dispatch seam

* fix(query): address CodeRabbit review findings on PR #3060

Critical: prevent double-execution race by checking timeout errors
before subprocess fallback (gsd-transport.ts).

Major: fix execRaw() to respect transport policy outputMode instead
of hardcoding 'raw' (gsd-tools.ts).

Major: add explicit 30s timeout to subprocess fallback execution
(query-fallback-executor.ts).

Major: remove raw args from stderr banner to prevent secret leakage
(query-fallback-executor.ts).

Minor: ensure native text output has trailing newline for CLI parity
(query-dispatch.ts).

Update gsd-tools.test.ts to match new execRaw() behavior.

* fix(tests): update CLI integration tests for catalog-based registration

The refactoring moved handler registration from inline registry.register()
calls to catalog-based registration (registerStaticCatalog/registerAliasCatalog).

- gsd-sdk-query-registry-integration.test.cjs: collectRegisteredNames() now
  also scans catalog files for handler names registered via the new system.
- bug-2492-context-coverage-gate.test.cjs: checks for catalog-based
  registration (DECISION_ROUTING_STATIC_CATALOG) instead of inline strings.
- bug-2524-sdk-query-ws-flag.test.cjs: checks for dispatchNative callback
  pattern instead of direct registry.dispatch() call.

* fix(query): address remaining CodeRabbit review findings

- query-command-semantics.ts: guard stats/progress rewrite so option
  tokens (e.g. --pick) are not turned into subcommands, preserving the
  top-level handler dispatch.

- query-dispatch.ts: formatOutput now skips --pick for text-format
  responses (matching CJS fallback behavior) and surfaces a proper error
  when extractField returns undefined instead of silently producing
  'undefined'.

- query-dispatch.ts: fix backwards error message — 'registered' is the
  restrictive policy that disables fallback, not enables it.

- tests/bug-2492-context-coverage-gate.test.cjs: check
  VERIFY_DECISION_STATIC_CATALOG (the correct catalog for plan-gate
  handlers) instead of DECISION_ROUTING_STATIC_CATALOG.

- tests/gsd-sdk-query-registry-integration.test.cjs: resolve catalog
  variable before loading entries so the drift guard checks each
  referenced catalog individually.

* refactor(query): deepen registry assembly module with strict invariants

- extract registry assembly into dedicated module
- split build vs mutation decoration internals
- add strict assembly invariants:
  1) no duplicate keys
  2) alias canonicals must have handlers
  3) mutation commands must be registered
  4) raw-output policy commands must be registered
- slim query index to thin re-export seam
- add focused registry assembly tests
- update drift-guard tests to target new seam

* test(query): add thin-seam coverage for query index re-exports

* fix(query): return structured native dispatch errors + tighten decisions.parse guard

- runQueryDispatch native path now catches adapter errors and returns
  QueryDispatchResult.error instead of throwing.
- preserve legacy CLI exit contract by using code=1 for native dispatch
  failures.
- strengthen bug-2492 guard: decisions.parse assertion now checks
  VERIFY_DECISION_STATIC_CATALOG OR explicit command token.
2026-05-03 13:57:32 -04:00

100 lines
3.3 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdir, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createRegistry } from './index.js';
import { runQueryDispatch } from './query-dispatch.js';
describe('runQueryDispatch', () => {
let tmpDir: string;
let fixtureDir: string;
beforeEach(async () => {
tmpDir = join(tmpdir(), `query-dispatch-${Date.now()}-${Math.random().toString(36).slice(2)}`);
fixtureDir = join(tmpDir, 'fixtures');
await mkdir(fixtureDir, { recursive: true });
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
async function createScript(name: string, code: string): Promise<string> {
const scriptPath = join(fixtureDir, name);
await writeFile(scriptPath, code, { mode: 0o755 });
return scriptPath;
}
it('runs native dispatch and formats json', async () => {
const registry = createRegistry();
const out = await runQueryDispatch({
registry,
projectDir: tmpDir,
cjsFallbackEnabled: true,
resolveGsdToolsPath: () => '',
dispatchNative: async () => ({ data: { ok: true } }),
}, ['state', 'json']);
expect(out.error).toBeUndefined();
expect(out.stdout).toBe('{\n "ok": true\n}\n');
});
it('applies --pick to native json output', async () => {
const registry = createRegistry();
const out = await runQueryDispatch({
registry,
projectDir: tmpDir,
cjsFallbackEnabled: true,
resolveGsdToolsPath: () => '',
dispatchNative: async () => ({ data: { nested: { value: 7 } } }),
}, ['state', 'json', '--pick', 'nested.value']);
expect(out.error).toBeUndefined();
expect(out.stdout).toBe('7\n');
});
it('returns structured error for unknown command when fallback disabled', async () => {
const registry = createRegistry();
const out = await runQueryDispatch({
registry,
projectDir: tmpDir,
cjsFallbackEnabled: false,
resolveGsdToolsPath: () => '',
dispatchNative: async () => ({ data: {} }),
}, ['unknown-cmd']);
expect(out.error?.code).toBe(10);
expect(out.error?.message).toContain('Unknown command: "unknown-cmd"');
expect(out.error?.message).toContain('Attempted dotted:');
});
it('runs cjs fallback and formats text mode', async () => {
const script = await createScript('text.cjs', "process.stdout.write('USAGE: help text');");
const registry = createRegistry();
const out = await runQueryDispatch({
registry,
projectDir: tmpDir,
cjsFallbackEnabled: true,
resolveGsdToolsPath: () => script,
dispatchNative: async () => ({ data: {} }),
}, ['unknown-cmd', '--help']);
expect(out.error).toBeUndefined();
expect(out.stdout).toBe('USAGE: help text\n');
expect(out.stderr[0]).toContain('falling back to gsd-tools.cjs');
});
it('returns requires-command error for empty argv', async () => {
const registry = createRegistry();
const out = await runQueryDispatch({
registry,
projectDir: tmpDir,
cjsFallbackEnabled: true,
resolveGsdToolsPath: () => '',
dispatchNative: async () => ({ data: {} }),
}, []);
expect(out.error?.code).toBe(10);
expect(out.error?.message).toContain('requires a command');
});
});