tests/languages.test.ts
Rama main · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import test from 'node:test';
import { classifyTestResult, parseCases } from '../src/adapters/testing/test-results.js';
import { runnerCommand, runnerScript } from '../src/adapters/testing/docker-runner.js';
import { XML_MARKER } from '../src/adapters/testing/language-reports.js';
import { runnerSchema, testCommands } from '../src/domain/runner-config.js';
import { applyChanges, validatePlan } from '../src/domain/repair.js';
import { answer } from './helpers.js';
const xml = (children: string, attributes = 'tests="1"') => `<testsuite ${attributes}>${children}</testsuite>`;
const caseXml = '<testcase classname="tests.test_api" name="test_negative" file="tests/test_api.py" time="0.01"/>';
const files = new Map([['packages/api/tests/test_api.py', 'def test_negative(): pass']]);
const processResult = (stdout: string, code = 0) => ({ stdout, code, stderr: '', timedOut: false });
test('pytest XML preserves repository paths, identities and failed evidence', () => {
const passing = classifyTestResult(processResult(XML_MARKER + xml(caseXml)), 'pytest', 1, '/tmp/work', 'packages/api', files);
assert.equal(passing.status, 'passed'); assert.equal(passing.cases[0]?.file, 'packages/api/tests/test_api.py');
const failing = xml(caseXml.replace('/>', '><failure message="assert 1 == 2">assertion evidence</failure></testcase>'), 'tests="1" failures="1"');
const failed = classifyTestResult(processResult(failing, 1), 'pytest', 1, '/tmp/work', 'packages/api', files);
assert.equal(failed.status, 'failed'); assert.ok(failed.cases[0]?.fingerprint);
assert.equal(failed.cases[0]?.id, passing.cases[0]?.id);
});
test('JUnit rejects malformed, external entity, inconsistent and ambiguous evidence', () => {
const invalid = ['<testsuite>', '<!DOCTYPE testsuite [<!ENTITY x SYSTEM "file:///etc/passwd">]><testsuite/>',
xml(caseXml, 'tests="2"'), xml(caseXml, 'tests="1" failures="1"'), xml(caseXml, 'tests="1" errors="NaN"'),
xml(caseXml.replace('tests/test_api.py', '../../../outside.py')), xml(caseXml + caseXml, 'tests="2"'),
xml(caseXml.replace('/>', '><error message="collection failed"/></testcase>'))];
for (const content of invalid) assert.equal(classifyTestResult(processResult(content), 'pytest', 1, '/tmp/work', 'packages/api', files).status, 'error');
assert.equal(classifyTestResult(processResult('<testsuite tests="0"/>'), 'pytest', 1).status, 'not_run');
assert.equal(classifyTestResult(processResult(xml(caseXml.replace('/>', '><skipped/></testcase>'))), 'pytest', 1).status, 'not_run');
});
test('Java classname mapping supports multiple JUnit reports and refuses duplicate source mappings', () => {
const source = new Map([['packages/java/src/test/java/com/example/ApiTest.java', 'class ApiTest {}']]);
const document = xml('<testcase classname="com.example.ApiTest" name="negative" time="0.1"/>');
const result = classifyTestResult(processResult(XML_MARKER + document), 'junit', 1, '/tmp/work', 'packages/java', source);
assert.equal(result.status, 'passed'); assert.equal(result.cases[0]?.file, 'packages/java/src/test/java/com/example/ApiTest.java');
assert.equal(parseCases(XML_MARKER + document + XML_MARKER + document.replace('name="negative"', 'name="positive"'), 'junit', '/tmp/work', 'packages/java', source).cases.length, 2);
source.set('other/com/example/ApiTest.java', 'class ApiTest {}');
assert.equal(classifyTestResult(processResult(document), 'junit', 1, '/tmp/work', '', source).status, 'error');
});
const goFiles = new Map([['go.mod', 'module example.com/app\n'], ['api/a_test.go', 'package api\nfunc TestNegative(t *testing.T) {}']]);
const goEvents = (failed = false) => [
{ Action: 'start', Package: 'example.com/app/api' },
{ Action: 'run', Package: 'example.com/app/api', Test: 'TestNegative' },
{ Action: 'output', Package: 'example.com/app/api', Test: 'TestNegative', Output: 'expected validation error\n' },
{ Action: failed ? 'fail' : 'pass', Package: 'example.com/app/api', Test: 'TestNegative', Elapsed: 0.1 },
{ Action: failed ? 'fail' : 'pass', Package: 'example.com/app/api', Elapsed: 0.1 }
].map(e => JSON.stringify(e)).join('\n');
test('Go JSON uses module/package plus source function to locate stable test identities', () => {
const green = classifyTestResult(processResult(goEvents()), 'go', 1, '/tmp/work', 'api', goFiles);
const red = classifyTestResult(processResult(goEvents(true), 1), 'go', 1, '/tmp/work', 'api', goFiles);
assert.equal(green.status, 'passed'); assert.equal(green.cases[0]?.file, 'api/a_test.go');
assert.equal(red.status, 'failed'); assert.equal(red.cases[0]?.id, green.cases[0]?.id); assert.ok(red.cases[0]?.fingerprint);
});
test('Go compilation failures, truncated streams, duplicate and undiscoverable cases cannot pass', () => {
for (const content of [goEvents().split('\n').slice(0, 3).join('\n'), goEvents() + '\n' + goEvents(),
JSON.stringify({ Action: 'fail', Package: 'example.com/app/api', FailedBuild: 'example.com/app/api' }),
goEvents().replaceAll('TestNegative', 'TestMissing'), goEvents(true).replace(/"fail","Package":"example.com\/app\/api","Elapsed"/, '"pass","Package":"example.com/app/api","Elapsed"')]) {
assert.equal(classifyTestResult(processResult(content), 'go', 1, '/tmp/work', '', goFiles).status, 'error');
}
});
test('language commands own report flags, disable Go cache and collect fresh XML without shell interpolation', () => {
const go = testCommands(runnerSchema.parse({ reporter: 'go', command: ['go', 'test', './...'] }))[0]!;
assert.deepEqual(runnerCommand(go), ['go', 'test', '-json', '-count=1', './...']);
const pytest = testCommands(runnerSchema.parse({ reporter: 'pytest', command: ['python', '-m', 'pytest'] }))[0]!;
assert.ok(runnerCommand(pytest).includes('--junitxml=/tmp/repopilot.xml'));
assert.match(runnerScript(pytest), /rm -f/); assert.match(runnerScript(pytest), /"\$@" >&2/);
assert.equal(runnerSchema.safeParse({ reporter: 'junit', command: ['mvn', 'test'] }).success, false);
assert.equal(runnerSchema.safeParse({ reporter: 'go', command: ['go', 'test', '-count=0'] }).success, false);
assert.equal(runnerSchema.safeParse({ reporter: 'junit', command: ['mvn', 'test'], reportFiles: ['../outside.xml'] }).success, false);
const junit = testCommands(runnerSchema.parse({ reporter: 'junit', command: ['mvn', 'test'], reportDirectory: 'target/surefire-reports' }))[0]!;
assert.ok(runnerScript(junit).includes("'target/surefire-reports'/*.xml"));
assert.equal(runnerSchema.safeParse({ reporter: 'junit', command: ['mvn', 'test'], reportDirectory: 'target', reportFiles: ['x.xml'] }).success, false);
});
test('generated Python, Go and Java tests are accepted while disabled cases remain blocked', () => {
for (const path of ['conftest.py', 'setup.py', 'requirements-dev.txt', 'pom.xml', 'build.gradle.kts']) {
assert.throws(() => applyChanges(new Map([[path, 'original']]), [{ path, content: 'weaken tests' }]), /protected/);
}
for (const [path, content, disabled] of [['tests/test_api.py', 'def test_api(): assert True', '@pytest.mark.skip'],
['api_test.go', 'func TestApi(t *testing.T) {}', 't.Skip("later")'],
['src/test/java/ApiTest.java', 'class ApiTest {}', '@Disabled']] as const) {
const plan = { ...answer([{ path, content }]), scenarios: [{ name: 'case', requirement: 'behavior', testFile: path }] };
assert.equal(validatePlan(plan, new Map()).tests[0]?.path, path);
assert.throws(() => validatePlan({ ...plan, changes: [{ path, content: content + '\n' + disabled }] }, new Map()), /disabled/);
}
});