refactor(query): deepen dispatch policy seam with structured result contract (#3066)

* refactor(query): deepen dispatch policy seam with structured result contract

Closes #3065.

- unify query dispatch outcome as typed success/failure union
- include error kind/details + final exit_code in failure path
- align native and fallback paths under one dispatch policy seam
- make CLI query path consume seam result (thin adapter)
- add ADR + context term for Dispatch Policy Module

* refactor(query): strengthen dispatch seam with shared error mapper and typed details

- add query-dispatch-error-mapper module shared by native/fallback paths
- remove ad-hoc inline mapping in dispatch/fallback executors
- lock error-details schema in mapper + dispatch tests
- document structured dispatch contract in QUERY-HANDLERS.md

* fix(query): return structured fallback failure when path resolution throws

- guard resolveGsdToolsPath in cjs dispatch path
- map thrown resolution errors to fallback_failure result
- add regression test for structured failure contract
This commit is contained in:
Tom Boucher
2026-05-03 14:30:27 -04:00
committed by GitHub
parent 5975f06b6a
commit f104dab332
12 changed files with 297 additions and 51 deletions

View File

@@ -0,0 +1,42 @@
import { describe, it, expect } from 'vitest';
import {
mapNativeDispatchError,
mapFallbackDispatchError,
toDispatchFailure,
} from './query-dispatch-error-mapper.js';
describe('query dispatch error mapper', () => {
it('maps native timeout errors', () => {
const err = mapNativeDispatchError(
new Error('gsd-tools timed out after 30000ms: state load'),
'state.load',
[],
);
expect(err.kind).toBe('native_timeout');
expect(err.code).toBe(1);
expect(err.details).toMatchObject({ command: 'state.load', args: [], timeout_ms: 30000 });
});
it('maps native non-timeout errors', () => {
const err = mapNativeDispatchError(new Error('boom'), 'state.json', []);
expect(err.kind).toBe('native_failure');
expect(err.code).toBe(1);
expect(err.details).toMatchObject({ command: 'state.json', args: [] });
});
it('maps fallback errors', () => {
const err = mapFallbackDispatchError(new Error('spawn ENOENT'), 'state', ['load']);
expect(err.kind).toBe('fallback_failure');
expect(err.code).toBe(1);
expect(err.details).toMatchObject({ command: 'state', args: ['load'], backend: 'cjs' });
});
it('builds failure result union', () => {
const out = toDispatchFailure({ kind: 'internal_error', code: 1, message: 'Error: x' }, ['warn']);
expect(out.ok).toBe(false);
if (out.ok) throw new Error('expected failure');
expect(out.exit_code).toBe(1);
expect(out.stderr).toEqual(['warn']);
expect(out.error.kind).toBe('internal_error');
});
});