import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; import { Command } from 'commander'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { registerMacpCommand } from './cli.js'; describe('registerMacpCommand', () => { function buildProgram(): Command { const program = new Command(); program.exitOverride(); // prevent process.exit in tests registerMacpCommand(program); return program; } it('registers a "macp" command on the parent', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp'); expect(macpCmd).toBeDefined(); }); it('registers "macp tasks" subcommand group', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp')!; const tasksCmd = macpCmd.commands.find((c) => c.name() === 'tasks'); expect(tasksCmd).toBeDefined(); }); it('registers "macp tasks list" subcommand with --status and --type flags', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp')!; const tasksCmd = macpCmd.commands.find((c) => c.name() === 'tasks')!; const listCmd = tasksCmd.commands.find((c) => c.name() === 'list'); expect(listCmd).toBeDefined(); const optionNames = listCmd!.options.map((o) => o.long); expect(optionNames).toContain('--status'); expect(optionNames).toContain('--type'); }); it('registers "macp submit" subcommand', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp')!; const submitCmd = macpCmd.commands.find((c) => c.name() === 'submit'); expect(submitCmd).toBeDefined(); }); it('registers "macp gate" subcommand with --fail-on flag', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp')!; const gateCmd = macpCmd.commands.find((c) => c.name() === 'gate'); expect(gateCmd).toBeDefined(); const optionNames = gateCmd!.options.map((o) => o.long); expect(optionNames).toContain('--fail-on'); }); it('registers "macp events" subcommand group', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp')!; const eventsCmd = macpCmd.commands.find((c) => c.name() === 'events'); expect(eventsCmd).toBeDefined(); }); it('registers "macp events tail" subcommand', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp')!; const eventsCmd = macpCmd.commands.find((c) => c.name() === 'events')!; const tailCmd = eventsCmd.commands.find((c) => c.name() === 'tail'); expect(tailCmd).toBeDefined(); }); it('has all required top-level subcommands', () => { const program = buildProgram(); const macpCmd = program.commands.find((c) => c.name() === 'macp')!; const topLevel = macpCmd.commands.map((c) => c.name()); expect(topLevel).toContain('tasks'); expect(topLevel).toContain('submit'); expect(topLevel).toContain('gate'); expect(topLevel).toContain('events'); }); }); /** * RI-N2 fail-closed CLI behavior: an unimplemented capability is a failure, * never a success. Every stub exits nonzero with a typed message, and the * implemented `macp gate` mirrors the typed gate-runner states. */ describe('registerMacpCommand fail-closed (RI-N2)', () => { let tmpDir: string; function buildProgram(): Command { const program = new Command(); program.exitOverride(); program.configureOutput({ writeErr: () => {} }); registerMacpCommand(program); return program; } beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'macp-cli-failclosed-')); process.exitCode = 0; }); afterEach(() => { process.exitCode = 0; fs.rmSync(tmpDir, { recursive: true, force: true }); }); it('macp tasks list exits nonzero (unimplemented capability)', async () => { const program = buildProgram(); await program.parseAsync(['macp', 'tasks', 'list'], { from: 'user' }); expect(process.exitCode).not.toBe(0); }); it('macp submit exits nonzero with a typed MACP_NOT_IMPLEMENTED message', async () => { const program = buildProgram(); const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); try { await program.parseAsync(['macp', 'submit', 'spec.json'], { from: 'user' }); expect(process.exitCode).not.toBe(0); const errText = errSpy.mock.calls.map((c) => String(c[0])).join('\n'); expect(errText).toContain('MACP_NOT_IMPLEMENTED'); } finally { errSpy.mockRestore(); } }); it('macp events tail exits nonzero (unimplemented capability)', async () => { const program = buildProgram(); await program.parseAsync(['macp', 'events', 'tail'], { from: 'user' }); expect(process.exitCode).not.toBe(0); }); it('macp gate runs a green inline command and exits 0', async () => { const program = buildProgram(); await program.parseAsync( [ 'macp', 'gate', 'exit 0', '--cwd', tmpDir, '--log', path.join(tmpDir, 'g.log'), '--timeout', '10', ], { from: 'user' }, ); expect(process.exitCode).toBe(0); }); it('macp gate exits nonzero on a failing command', async () => { const program = buildProgram(); await program.parseAsync( [ 'macp', 'gate', 'exit 9', '--cwd', tmpDir, '--log', path.join(tmpDir, 'g.log'), '--timeout', '10', ], { from: 'user' }, ); expect(process.exitCode).not.toBe(0); }); it('macp gate with an unimplemented ci-pipeline capability exits nonzero', async () => { const program = buildProgram(); const specPath = path.join(tmpDir, 'gates.json'); fs.writeFileSync(specPath, JSON.stringify([{ type: 'ci-pipeline' }])); await program.parseAsync( [ 'macp', 'gate', specPath, '--cwd', tmpDir, '--log', path.join(tmpDir, 'g.log'), '--timeout', '10', ], { from: 'user' }, ); expect(process.exitCode).not.toBe(0); }); it('macp gate --simulate completes (exit 0) but reports simulated results', async () => { const program = buildProgram(); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); try { await program.parseAsync( [ 'macp', 'gate', 'exit 0', '--simulate', '--cwd', tmpDir, '--log', path.join(tmpDir, 'g.log'), '--timeout', '10', ], { from: 'user' }, ); // completes only because the caller explicitly asked to simulate expect(process.exitCode).toBe(0); const outText = logSpy.mock.calls.map((c) => String(c[0])).join('\n'); expect(outText).toContain('simulated'); expect(outText).toContain('SIMULATED'); } finally { logSpy.mockRestore(); } }); it('macp gate with an empty spec exits nonzero with a typed error', async () => { const program = buildProgram(); await program.parseAsync( [ 'macp', 'gate', ' ', '--cwd', tmpDir, '--log', path.join(tmpDir, 'g.log'), '--timeout', '10', ], { from: 'user' }, ); expect(process.exitCode).not.toBe(0); }); });