tests/iteration-cli.test.ts
Rama main · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import test from 'node:test';
import { main } from '../src/cli/bootstrap.js';
import { parseCli } from '../src/cli/args.js';
import { executeGoals } from '../src/cli/commands/goals.js';
import { configSchema } from '../src/domain/config.js';
import type { AutomationDependencies } from '../src/application/automation.js';
import { FileIterationStore } from '../src/adapters/storage/iteration-store.js';
import { FileCandidateStore } from '../src/adapters/storage/candidate-store.js';
import { candidateFromSignal } from '../src/domain/candidate.js';
import { Store } from '../src/adapters/storage/file-store.js';
import type { GoalState } from '../src/domain/iteration.js';
test('goal inspection and pause work under a controller lock and enforce repository isolation', async () => {
await mkdir('.cache/tests', { recursive: true });
const root = await mkdtemp(resolve('.cache/tests/iteration-cli-')), config = resolve(root, 'config.json');
await writeFile(config, JSON.stringify({ repository: 'owner/repo', dataDir: root }));
const goals = new FileIterationStore(root), id = 'a'.repeat(24);
const state: GoalState = { schemaVersion: 1, id, repository: 'owner/repo', configHash: 'old-config',
spec: { title: 'Example goal', objective: 'Implement a feature', mode: 'feature', acceptance: [{ id: 'accept', text: 'Expected behavior' }], allowedPaths: ['src'],
evaluation: { suite: 'core-suite', case: 'example-case', profile: 'codex-default' } },
branch: 'main', sha: 'a'.repeat(40), status: 'planned', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
steps: [], completed: [], changes: [], reports: [], rounds: 0, calls: 0, tokens: 0, elapsedMs: 0, notes: [] };
await goals.save(state); await goals.save({ ...state, id: 'b'.repeat(24), repository: 'other/repo' });
const candidates = new FileCandidateStore(root), candidate = candidateFromSignal({
kind: 'issue', repository: 'owner/repo', identity: [7], evidence: ['issue'], commit: 'a'.repeat(40),
branch: 'main', issue: 7, title: 'Reject negative quantities', detail: 'Negative quantities must fail.',
priority: 70, priorityReasons: ['trusted'] });
await candidates.save(candidate);
await goals.remember({ id, goalId: id, repository: state.repository, sha: state.sha, configHash: state.configHash,
outcome: state.status, completed: [], reportIds: [], notes: [], createdAt: state.createdAt });
const lines: string[] = [], output = { write: (line: string) => lines.push(line), error: (_line: string) => {} };
const release = await new Store(root).acquire();
try {
assert.equal(await main(['goals', 'list', '--config', config], output), 0);
assert.equal(JSON.parse(lines.at(-1)!).total, 1);
assert.equal(await main(['goals', 'show', id, '--config', config], output), 0);
assert.equal(JSON.parse(lines.at(-1)!).configHash, 'old-config');
assert.equal(await main(['goals', 'graph', id, '--config', config], output), 0);
assert.deepEqual(JSON.parse(lines.at(-1)!).steps, []);
assert.deepEqual(JSON.parse(lines.at(-1)!).waveHistory, []);
assert.deepEqual(JSON.parse(lines.at(-1)!).serialReplay, []);
assert.equal(await main(['goals', 'pause', id, '--config', config], output), 0);
assert.equal(await goals.paused(id), true);
assert.equal((await goals.read(id))!.status, 'planned');
await assert.rejects(main(['goals', 'pause', 'b'.repeat(24), '--config', config], output), /this repository/);
assert.equal(await goals.paused('b'.repeat(24)), false);
assert.equal(await main(['experiences', '--config', config], output), 0);
assert.equal(JSON.parse(lines.at(-1)!).experiences[0].goalId, id);
assert.equal(await main(['evals', '--suite', 'core-suite', '--config', config], output), 0);
assert.equal(JSON.parse(lines.at(-1)!).records[0].case, 'example-case');
assert.equal(await main(['evals', 'gate', '--suite', 'core-suite', '--baseline', 'baseline',
'--candidate', 'codex-default', '--config', config], output), 1);
assert.equal(JSON.parse(lines.at(-1)!).eligible, false);
assert.equal(await main(['candidates', 'list', '--config', config], output), 0);
assert.equal(JSON.parse(lines.at(-1)!).candidates[0].id, candidate.id);
assert.equal(await main(['candidates', 'show', candidate.id, '--config', config], output), 0);
assert.equal(JSON.parse(lines.at(-1)!).evidenceDigest, candidate.evidenceDigest);
await assert.rejects(main(['candidates', 'approve', candidate.id, '--config', config], output), /--expected/);
await assert.rejects(main(['candidates', 'show', 'b'.repeat(24), '--config', config], output), /this repository/);
await assert.rejects(main(['goals', 'list', '--limit', '0', '--config', config], output), /pagination/);
await assert.rejects(main(['goals', 'list', '--status', 'unknown', '--config', config], output), /status/);
await assert.rejects(main(['goals', 'plan', '--config', config], output), /--spec/);
await assert.rejects(main(['goals', 'run', '--config', config], output), /ID is required/);
await assert.rejects(main(['discover', '--apply', '--config', config], output), /--expected/);
await assert.rejects(main(['discover', '--expected', 'token', '--config', config], output), /--apply/);
} finally { await release(); }
});
test('goal specification and queue flags are parsed without ambiguity', () => {
const result = parseCli(['goals', 'plan', '--spec', 'goal.json', '--config', 'local.json']);
assert.equal(result.values.spec, 'goal.json'); assert.deepEqual(result.positionals, ['goals', 'plan']);
assert.equal(parseCli(['iterate', '--once']).values.once, true);
assert.deepEqual(parseCli(['goals', 'graph', 'a'.repeat(24)]).positionals, ['goals', 'graph', 'a'.repeat(24)]);
assert.equal(parseCli(['evals', 'replay', '--profile', 'new-strategy']).values.profile, 'new-strategy');
});
test('iteration polling exits on cancellation and refuses an unconfigured loop', async () => {
const abort = new AbortController(), output = { write: (_line: string) => {}, error: (_line: string) => {} };
// No adapter may be touched before validating the polling configuration or cancellation.
const deps = { signal: abort.signal, config: configSchema.parse({ repository: 'owner/repo' }) } as AutomationDependencies;
await assert.rejects(executeGoals('iterate', undefined, undefined, { once: true }, deps, output), /iteration.queue or iteration.maintenance/);
abort.abort();
assert.equal(await executeGoals('iterate', undefined, undefined, {}, deps, output), 0);
});
test('strategy gate defers new Issue candidates without bypassing their state machine', async () => {
await mkdir('.cache/tests', { recursive: true });
const root = await mkdtemp(resolve('.cache/tests/gated-intake-'));
const lines: string[] = [], output = { write: (line: string) => lines.push(line), error: (_line: string) => {} };
const config = configSchema.parse({ repository: 'owner/repo', iteration: {
queue: { labels: ['agent-ready'], trustedAuthors: ['maintainer'], allowedPaths: ['src'] },
strategyGate: { suite: 'core-suite', baselineProfile: 'baseline', candidateProfile: 'candidate' }
} });
const deps = { signal: new AbortController().signal, config, goals: { list: async () => [] },
candidates: new FileCandidateStore(root), github: { target: async () => ({ branch: 'main', sha: 'a'.repeat(40) }),
issues: async () => [{ number: 7, title: 'Reject negative quantities', body: 'Negative quantities must fail.',
state: 'open', user: { login: 'maintainer' }, labels: [{ name: 'agent-ready' }] }] }
} as unknown as AutomationDependencies & { candidates: FileCandidateStore };
assert.equal(await executeGoals('iterate', undefined, undefined, { once: true }, deps, output), 1);
assert.equal(JSON.parse(lines.at(-1)!).strategyGate.eligible, false);
assert.equal((await deps.candidates.list())[0]?.status, 'observed');
});