ci/woodpecker/pr/ci Pipeline was successful
RI-N2 / SDLC-D-035. Every GateResult now carries a typed status discriminator (passed|failed|simulated|waiting|capability_failure); passed:true remains true only for really-executed, really-green gates. - ci-pipeline without a CI provider → capability_failure (MACP_NO_CI_PIPELINE), never the placeholder pass - empty-command gate → capability_failure (MACP_NO_COMMAND / MACP_NO_REVIEWER), and runGates no longer silently skips it - manual gate type waits (MACP_AUTHORITY_REQUIRED) — neither pass nor fail - explicit simulation only (simulate option / --simulate): typed simulated results can never make the aggregate passed (RunGatesResult.state) - CLI stubs (tasks list, submit, events tail) exit nonzero with typed MACP_NOT_IMPLEMENTED; macp gate is now wired to runGates with --simulate - legacy __tests__/gate-runner.test.ts consolidated into src/gate-runner.spec.ts (root eslint project service does not cover packages/macp/__tests__)
240 lines
7.4 KiB
TypeScript
240 lines
7.4 KiB
TypeScript
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);
|
|
});
|
|
});
|