tests/test-assessment.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 { validatePlan } from '../src/domain/repair.js';
import { assessPlan } from '../src/domain/test-assessment.js';
import type { TestCase, TestPlan } from '../src/domain/types.js';
import { agent, answer, result, store, testCase } from './helpers.js';
const file = 'test/feature.test.js', description = 'Add a new export that returns the greeting.';
const plan: TestPlan = { summary: 'New greeting', tests: [{ path: file, content: 'test("works", () => {});' }],
scenarios: [{ name: 'works', requirement: 'Greeting', testFile: file, kind: 'new_behavior', requirementQuote: description }] };
const original = result();
function augmented(status: TestCase['status']) {
return result(status === 'failed' ? 'failed' : 'passed', [testCase(), testCase(file, status)]);
}
test('classification distinguishes preserved behavior, regressions, new behavior and ambiguous failures', () => {
for (const [base, head, outcome, eligible] of [
['passed', 'passed', 'preserved', true], ['passed', 'failed', 'regression', true],
['failed', 'passed', 'new_behavior_verified', true], ['failed', 'failed', 'unresolved', false],
['skipped', 'passed', 'unresolved', false], ['passed', 'skipped', 'unresolved', false]
] as const) {
const assessment = assessPlan(plan, original, original, augmented(base), augmented(head));
assert.equal(assessment.cases[0]?.outcome, outcome); assert.equal(assessment.eligible, eligible);
}
});
test('regression labels cannot exempt failing baselines', () => {
const regression: TestPlan = { ...plan, scenarios: plan.scenarios.map(s => ({ ...s, kind: 'regression' })) };
assert.equal(assessPlan(regression, original, original, augmented('failed'), augmented('passed')).eligible, false);
});
test('discovery changes and infrastructure errors remain blocked', () => {
assert.equal(assessPlan(plan, original, original, result(), augmented('passed')).eligible, false);
assert.equal(assessPlan(plan, original, original, { ...augmented('failed'), status: 'error' }, augmented('passed')).eligible, false);
const renamed = augmented('passed'); renamed.cases[1] = { ...testCase(file), id: 'renamed', name: 'renamed' };
assert.equal(assessPlan(plan, original, original, augmented('failed'), renamed).eligible, false);
});
test('generated tests cannot repair or break original tests through side effects', () => {
const failedOriginal = result('failed');
assert.equal(assessPlan(plan, original, failedOriginal, augmented('passed'), augmented('passed')).eligible, false);
const brokenBase = augmented('failed'); brokenBase.cases[0] = testCase(undefined, 'failed');
assert.equal(assessPlan(plan, original, original, brokenBase, augmented('passed')).eligible, false);
});
test('new-behavior plans require exact request evidence and separate files', () => {
const response = { ...answer(plan.tests), scenarios: plan.scenarios };
assert.throws(() => validatePlan(response, new Map(), 'Different request'), /exact requirement quote/);
assert.equal(validatePlan(response, new Map(), description).scenarios[0]?.kind, 'new_behavior');
assert.throws(() => validatePlan({ ...response, scenarios: [...response.scenarios,
{ name: 'regression', requirement: 'preserve', testFile: file, kind: 'regression' }] }, new Map(), description), /Separate/);
});
const config = configSchema.parse({ repository: 'example/project', agent: { enabled: true, repair: true } });
test('a new feature can pass without requiring generated tests to pass on base', async () => {
const a = agent(); a.plan = async () => ({ ...answer(plan.tests), scenarios: plan.scenarios });
a.repair = async () => { throw new Error('No repair expected'); };
const report = await runPipeline({ base: new Map([['src/a.ts', 'old']]), head: new Map([['src/a.ts', 'new']]),
baseSha: 'a'.repeat(40), headSha: 'b'.repeat(40), description }, config, await store(),
{ run: async (_files, phase) => phase === 'planned-base' ? augmented('failed') : phase === 'planned-head' ? augmented('passed') : original }, a);
assert.equal(report.status, 'passed'); assert.equal(report.testAssessment?.cases[0]?.outcome, 'new_behavior_verified');
assert.equal(report.attempts, 0);
});
test('mixed new behavior and regression repairs retain both frozen tests', async () => {
const regressionFile = 'test/regression.test.js';
const a = agent();
a.plan = async () => ({ ...answer([...plan.tests, { path: regressionFile, content: 'test("works", () => {});' }]),
scenarios: [...plan.scenarios, { name: 'works', requirement: 'Keep original behavior', testFile: regressionFile, kind: 'regression' }] });
const report = await runPipeline({ base: new Map([['src/a.ts', 'BASE']]), head: new Map([['src/a.ts', 'BAD']]),
baseSha: 'a'.repeat(40), headSha: 'b'.repeat(40), description }, config, await store(), {
run: async files => {
if (!files.has(file)) return original;
const cases = [testCase(), testCase(file, files.get('src/a.ts') === 'BASE' ? 'failed' : 'passed'),
testCase(regressionFile, files.get('src/a.ts') === 'BAD' ? 'failed' : 'passed')];
return result(cases.some(c => c.status === 'failed') ? 'failed' : 'passed', cases);
}
}, a);
assert.equal(report.status, 'verified');
assert.deepEqual(report.testAssessment?.cases.map(c => c.outcome), ['new_behavior_verified', 'regression']);
assert.equal(report.changes.length, 3);
});
test('new behavior that fails on both revisions requires review without repair', async () => {
const a = agent(); a.plan = async () => ({ ...answer(plan.tests), scenarios: plan.scenarios });
a.repair = async () => { throw new Error('Ambiguous evidence must not trigger repair'); };
const report = await runPipeline({ base: new Map(), head: new Map(), baseSha: 'a'.repeat(40), headSha: 'b'.repeat(40), description },
config, await store(), { run: async (_files, phase) => phase.startsWith('planned-') ? augmented('failed') : original }, a);
assert.equal(report.status, 'needs_attention'); assert.equal(report.attempts, 0);
});