import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdir, writeFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { QuerySubprocessAdapter } from './query-subprocess-adapter.js'; class FakeToolsError extends Error { constructor( message: string, public readonly command: string, public readonly args: string[], public readonly exitCode: number | null, public readonly stderr: string, ) { super(message); } } describe('QuerySubprocessAdapter', () => { let dir: string; let fixtures: string; beforeEach(async () => { dir = join(tmpdir(), `query-subprocess-adapter-${Date.now()}-${Math.random().toString(36).slice(2)}`); fixtures = join(dir, 'fixtures'); await mkdir(fixtures, { recursive: true }); }); afterEach(async () => { await rm(dir, { recursive: true, force: true }); }); async function createScript(name: string, code: string): Promise { const scriptPath = join(fixtures, name); await writeFile(scriptPath, code, { mode: 0o755 }); return scriptPath; } function createAdapter(gsdToolsPath: string): QuerySubprocessAdapter { return new QuerySubprocessAdapter({ projectDir: dir, gsdToolsPath, timeoutMs: 2_000, createTimeoutError: (message, command, args, stderr) => new FakeToolsError(message, command, args, null, stderr) as never, createFailureError: (message, command, args, exitCode, stderr) => new FakeToolsError(message, command, args, exitCode, stderr) as never, }); } it('execJson parses JSON', async () => { const script = await createScript('json.cjs', `process.stdout.write(JSON.stringify({ ok: true }));`); const adapter = createAdapter(script); await expect(adapter.execJson('state', ['load'])).resolves.toEqual({ ok: true }); }); it('execJson resolves @file output', async () => { const outFile = join(fixtures, 'out.json'); await writeFile(outFile, JSON.stringify({ from: 'file' })); const script = await createScript('file.cjs', `process.stdout.write('@file:${outFile.replace(/\\/g, '\\\\')}');`); const adapter = createAdapter(script); await expect(adapter.execJson('state', ['load'])).resolves.toEqual({ from: 'file' }); }); it('execJson resolves relative @file output against projectDir', async () => { const relDir = join(dir, '.planning'); await mkdir(relDir, { recursive: true }); const relFile = join(relDir, 'out.json'); await writeFile(relFile, JSON.stringify({ from: 'relative-file' })); const script = await createScript('file-relative.cjs', `process.stdout.write('@file:.planning/out.json');`); const adapter = createAdapter(script); await expect(adapter.execJson('state', ['load'])).resolves.toEqual({ from: 'relative-file' }); }); it('execRaw returns trimmed stdout', async () => { const script = await createScript('raw.cjs', `process.stdout.write(' hello ');`); const adapter = createAdapter(script); await expect(adapter.execRaw('config-set', ['x', 'y'])).resolves.toBe('hello'); }); });