import { mkdirSync, mkdtempSync, writeFileSync, chmodSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Command } from 'commander'; import { afterEach, describe, expect, it } from 'vitest'; import { registerQCommand, resolveQuestionTool } from './q.js'; import { exitStatusFor, resolveBrainTool } from './brain-dispatch.js'; describe('resolveBrainTool', () => { const saved = process.env['MOSAIC_BRAIN_HOME']; afterEach(() => { if (saved === undefined) delete process.env['MOSAIC_BRAIN_HOME']; else process.env['MOSAIC_BRAIN_HOME'] = saved; }); it('joins brain home with the relative tool path', () => { const tmp = mkdtempSync(join(tmpdir(), 'dispatch-resolve-')); process.env['MOSAIC_BRAIN_HOME'] = tmp; expect(resolveBrainTool('/nonexistent/mosaic-home', 'tools/questions/q-new.sh')).toBe( join(tmp, 'tools', 'questions', 'q-new.sh'), ); }); }); describe('resolveQuestionTool', () => { const saved = process.env['MOSAIC_BRAIN_HOME']; afterEach(() => { if (saved === undefined) delete process.env['MOSAIC_BRAIN_HOME']; else process.env['MOSAIC_BRAIN_HOME'] = saved; }); it('maps new/render subcommands to their brain tools', () => { const tmp = mkdtempSync(join(tmpdir(), 'q-resolve-')); process.env['MOSAIC_BRAIN_HOME'] = tmp; expect(resolveQuestionTool(tmp, 'new')).toBe(join(tmp, 'tools', 'questions', 'q-new.sh')); expect(resolveQuestionTool(tmp, 'render')).toBe(join(tmp, 'tools', 'questions', 'render.py')); expect(resolveQuestionTool(tmp, 'bogus')).toBeUndefined(); }); }); describe('registerQCommand usage + dispatch', () => { const saved = process.env['MOSAIC_BRAIN_HOME']; afterEach(() => { if (saved === undefined) delete process.env['MOSAIC_BRAIN_HOME']; else process.env['MOSAIC_BRAIN_HOME'] = saved; process.exitCode = undefined; }); it('exit 2 with the subcommand list when no/unknown subcommand', async () => { process.env['MOSAIC_BRAIN_HOME'] = mkdtempSync(join(tmpdir(), 'q-usage-')); const program = new Command(); registerQCommand(program); await program.parseAsync(['q'], { from: 'user' }); expect(process.exitCode).toBe(2); process.exitCode = undefined; await program.parseAsync(['q', 'bogus'], { from: 'user' }); expect(process.exitCode).toBe(2); process.exitCode = undefined; // Reserved property names must not leak through the record lookup. await program.parseAsync(['q', 'toString'], { from: 'user' }); expect(process.exitCode).toBe(2); }); it('execs the brain tool with pass-through args and exit code', async () => { const tmp = mkdtempSync(join(tmpdir(), 'q-live-')); process.env['MOSAIC_BRAIN_HOME'] = tmp; mkdirSync(join(tmp, 'tools', 'questions'), { recursive: true }); const stub = join(tmp, 'tools', 'questions', 'q-new.sh'); writeFileSync(stub, '#!/usr/bin/env bash\necho "called with: $*"\nexit 7\n'); chmodSync(stub, 0o755); const program = new Command(); registerQCommand(program); await program.parseAsync(['q', 'new', '--slug', 'x', '--question', 'why'], { from: 'user' }); expect(process.exitCode).toBe(7); }); }); describe('exitStatusFor (shared dispatch contract)', () => { it('maps absent tool to 127', () => { expect(exitStatusFor({ status: 0 }, false)).toBe(127); }); it('passes tool status through', () => { expect(exitStatusFor({ status: 7 }, true)).toBe(7); }); it('maps signal death to 125', () => { expect(exitStatusFor({ status: null }, true)).toBe(125); }); });