tests/implementation.test.ts
Rama main · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import test from 'node:test';
import { runPipeline } from '../src/application/pipeline.js';
import { configSchema } from '../src/domain/config.js';
import type { Agent } from '../src/ports/agent.js';
import type { Runner } from '../src/ports/runner.js';
import { answer, result, store, testCase } from './helpers.js';
const criterion = 'Negative quantities produce a validation error.';
const base = new Map([['src/a.ts', 'BROKEN']]);
const input = { base, head: base, baseSha: 'a'.repeat(40), headSha: 'a'.repeat(40), description: criterion,
implementation: { mode: 'feature' as const, acceptance: [criterion], allowedPaths: ['src', 'test'] } };
const config = configSchema.parse({ repository: 'owner/repo', agent: { enabled: true, repair: true, maxAttempts: 3 } });
function model(): Agent { return {
review: async () => answer(),
plan: async () => ({ ...answer([{ path: 'test/generated.test.js', content: 'test("works", () => {});' }]),
scenarios: [{ name: 'works', requirement: criterion, requirementQuote: criterion, kind: 'new_behavior', testFile: 'test/generated.test.js' }] }),
repair: async () => answer([{ path: 'src/a.ts', content: 'GOOD' }])
}; }
const runner: Runner = { run: async files => {
const cases = [testCase()];
if (files.has('test/generated.test.js')) cases.push(testCase('test/generated.test.js', files.get('src/a.ts') === 'GOOD' ? 'passed' : 'failed'));
return result(cases.some(c => c.status === 'failed') ? 'failed' : 'passed', cases);
} };
test('feature implementation demonstrates missing behavior before changing production and preserves frozen acceptance tests', async () => {
const report = await runPipeline(input, config, await store(), runner, model());
assert.equal(report.status, 'verified');
assert.equal(report.tests.base.status, 'passed');
assert.equal(report.evidence.find(e => e.phase === 'planned-base')?.result.status, 'failed');
assert.equal(report.testStability?.status, 'stable');
assert.equal(report.tests.repaired?.status, 'passed');
assert.deepEqual(report.changes.map(c => c.path).sort(), ['src/a.ts', 'test/generated.test.js']);
});
test('feature planning refuses uncited acceptance, omitted acceptance and tests outside allowed paths', async () => {
for (const variant of ['quote', 'coverage', 'scope']) {
const m = model();
if (variant === 'quote') { const plan = m.plan; m.plan = async (...args) => { const a = await plan(...args); a.scenarios[0]!.requirementQuote = 'Unrelated requirements'; return a; }; }
const implementation = { ...input.implementation,
acceptance: variant === 'coverage' ? [criterion, 'Zero quantities must also be rejected.'] : [criterion],
allowedPaths: variant === 'scope' ? ['src'] : ['src', 'test'] };
m.repair = async () => { assert.fail('Invalid plan must never reach repair'); };
const report = await runPipeline({ ...input, implementation }, config, await store(), runner, m);
assert.equal(report.status, 'error', variant); assert.equal(report.attempts, 0);
}
});
test('feature repairs cannot expand scope or modify frozen tests', async () => {
for (const path of ['other/a.ts', 'test/generated.test.js']) {
const m = model(); m.repair = async () => answer([{ path, content: 'GOOD' }]);
const report = await runPipeline(input, config, await store(), runner, m);
assert.equal(report.status, 'needs_attention'); assert.deepEqual(report.changes, []);
assert.match(report.repairs[0]!.reason ?? '', /allowed paths|frozen tests/);
}
});
test('identical failed patches terminate instead of consuming all repair attempts', async () => {
const m = model(); let calls = 0;
m.repair = async () => { calls++; return answer([{ path: 'src/a.ts', content: 'STILL_BROKEN' }]); };
const report = await runPipeline(input, config, await store(), runner, m);
assert.equal(calls, 2); assert.equal(report.status, 'needs_attention');
assert.equal(report.evidence.filter(e => e.phase === 'repair').length, 1);
assert.match(report.repairs[1]!.reason ?? '', /Repeated patch/);
});
test('already passing feature criteria verify new acceptance tests without production changes', async () => {
const m = model(); m.repair = async () => { assert.fail('Already passing tests cannot justify a repair'); };
const green: Runner = { run: async files => result('passed', [testCase(), ...(files.has('test/generated.test.js') ? [testCase('test/generated.test.js')] : [])]) };
const report = await runPipeline(input, config, await store(), green, m);
assert.equal(report.status, 'verified'); assert.equal(report.attempts, 0);
assert.deepEqual(report.changes.map(c => c.path), ['test/generated.test.js']);
assert.equal(report.tests.repaired?.status, 'passed');
});
test('already passing acceptance tests cannot bypass static policy review', async () => {
const m = model(), plan = m.plan;
m.plan = async (...args) => {
const planned = await plan(...args); planned.changes[0]!.content = 'test("works", () => { forbidden(); });'; return planned;
};
m.repair = async () => { assert.fail('Passing acceptance tests cannot authorize production repair'); };
const policyBase = new Map([...base, ['.repopilot/policy.json', JSON.stringify({ rules: [{ id: 'forbidden',
kind: 'forbid-call', callee: 'forbidden', message: 'Forbidden operation' }] })]]);
const green: Runner = { run: async files => result('passed', [testCase(), ...(files.has('test/generated.test.js') ? [testCase('test/generated.test.js')] : [])]) };
const report = await runPipeline({ ...input, base: policyBase, head: policyBase }, config, await store(), green, m);
assert.equal(report.status, 'needs_attention'); assert.deepEqual(report.changes, []); assert.equal(report.attempts, 0);
assert.match(report.notes.join('\n'), /policy review/);
});
test('bugfix mode cannot accept all-green tests as proof that a reported bug was reproduced', async () => {
const m = model(), plan = m.plan;
m.plan = async (...args) => { const planned = await plan(...args); planned.scenarios[0]!.kind = 'regression'; return planned; };
m.repair = async () => { assert.fail('Unreproduced bug must not request a repair'); };
const green: Runner = { run: async files => result('passed', [testCase(), ...(files.has('test/generated.test.js') ? [testCase('test/generated.test.js')] : [])]) };
const report = await runPipeline({ ...input, implementation: { ...input.implementation, mode: 'bugfix' } }, config, await store(), green, m);
assert.equal(report.status, 'needs_attention'); assert.deepEqual(report.changes, []); assert.equal(report.attempts, 0);
});