mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-15 04:18:05 +00:00
284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { reconcileTestReports } from '../../scripts/test-completeness.js';
|
|
|
|
const report = (name: string, statuses: string[]) => ({
|
|
success: !statuses.includes('failed'),
|
|
executionErrors: 0,
|
|
executionReason: 'passed',
|
|
testResults: [
|
|
{
|
|
name,
|
|
status: statuses.includes('failed') ? 'failed' : 'passed',
|
|
assertionResults: statuses.map((status, i) => ({
|
|
ancestorTitles: ['contract'],
|
|
title: `case ${i}`,
|
|
fullName: `contract case ${i}`,
|
|
status,
|
|
location: { line: i + 1, column: 1 },
|
|
})),
|
|
},
|
|
],
|
|
});
|
|
|
|
describe('required test execution across CI jobs', () => {
|
|
it('fails when a collected test never executes, including todo', () => {
|
|
const result = reconcileTestReports([
|
|
report('/home/runner/work/GitNexus/GitNexus/gitnexus/test/unit/a.test.ts', [
|
|
'passed',
|
|
'pending',
|
|
'todo',
|
|
]),
|
|
]);
|
|
expect(result.total).toBe(3);
|
|
expect(result.passed).toBe(1);
|
|
expect(result.unverified).toHaveLength(2);
|
|
});
|
|
|
|
it('accepts a platform skip only with an actual pass from another job', () => {
|
|
const result = reconcileTestReports([
|
|
report('/home/runner/work/GitNexus/GitNexus/gitnexus/test/unit/a.test.ts', [
|
|
'passed',
|
|
'pending',
|
|
]),
|
|
report('D:\\a\\GitNexus\\GitNexus\\gitnexus\\test\\unit\\a.test.ts', ['pending', 'passed']),
|
|
]);
|
|
expect(result.total).toBe(2);
|
|
expect(result.passed).toBe(2);
|
|
expect(result.unverified).toEqual([]);
|
|
expect(result.failures).toEqual([]);
|
|
});
|
|
|
|
it('never lets a passing job erase a failure on another platform', () => {
|
|
const result = reconcileTestReports([
|
|
report('/repo/gitnexus/test/unit/a.test.ts', ['failed']),
|
|
report('/repo/gitnexus/test/unit/a.test.ts', ['passed']),
|
|
]);
|
|
expect(result.failures.some((message) => message.includes('contract case 0'))).toBe(true);
|
|
});
|
|
|
|
it('counts failed tests separately from tests that never executed', () => {
|
|
const result = reconcileTestReports([
|
|
report('/repo/gitnexus/test/unit/a.test.ts', ['passed', 'failed', 'pending']),
|
|
]);
|
|
expect(result.report.numPassedTests).toBe(1);
|
|
expect(result.report.numFailedTests).toBe(1);
|
|
expect(result.report.numPendingTests).toBe(1);
|
|
expect(result.unverified).toHaveLength(1);
|
|
expect(result.report.success).toBe(false);
|
|
});
|
|
|
|
it('does not collapse identical titles from different files or repeated cases', () => {
|
|
const repeated = report('/repo/gitnexus/test/unit/a.test.ts', ['passed', 'pending']);
|
|
repeated.testResults[0].assertionResults[1].title = 'case 0';
|
|
repeated.testResults[0].assertionResults[1].fullName = 'contract case 0';
|
|
const result = reconcileTestReports([
|
|
repeated,
|
|
report('/repo/gitnexus/test/unit/b.test.ts', ['passed']),
|
|
]);
|
|
expect(result.total).toBe(3);
|
|
expect(result.unverified).toHaveLength(1);
|
|
});
|
|
|
|
it('matches source locations when repeated titles are collected in a different order', () => {
|
|
const first = report('/repo/gitnexus/test/unit/a.test.ts', ['pending', 'passed']);
|
|
first.testResults[0].assertionResults[1].title = 'case 0';
|
|
first.testResults[0].assertionResults[1].fullName = 'contract case 0';
|
|
const second = structuredClone(first);
|
|
second.testResults[0].assertionResults.reverse();
|
|
const result = reconcileTestReports([first, second]);
|
|
expect(result.total).toBe(2);
|
|
expect(result.unverified).toHaveLength(1);
|
|
});
|
|
|
|
it('rejects ambiguous parameterized cases with the same name and location', () => {
|
|
const input = report('/repo/gitnexus/test/unit/a.test.ts', ['passed']);
|
|
input.testResults[0].assertionResults.push({ ...input.testResults[0].assertionResults[0] });
|
|
expect(() => reconcileTestReports([input])).toThrow(/ambiguous/i);
|
|
});
|
|
|
|
it('requires an unambiguous title and a real pass for helper-generated tests without locations', () => {
|
|
const input = report('/repo/gitnexus/test/unit/a.test.ts', ['pending']);
|
|
Reflect.deleteProperty(input.testResults[0].assertionResults[0], 'location');
|
|
expect(reconcileTestReports([input]).unverified).toHaveLength(1);
|
|
const passing = structuredClone(input);
|
|
passing.testResults[0].assertionResults[0].status = 'passed';
|
|
expect(reconcileTestReports([input, passing]).report.success).toBe(true);
|
|
passing.testResults[0].assertionResults.push({ ...passing.testResults[0].assertionResults[0] });
|
|
expect(() => reconcileTestReports([passing])).toThrow(/ambiguous/i);
|
|
});
|
|
|
|
it('reconciles source-adjacent web tests on Linux and Windows', () => {
|
|
const result = reconcileTestReports(
|
|
[
|
|
report('/repo/gitnexus-web/src/lib/upload-filter.test.ts', ['passed']),
|
|
report('C:/repo/gitnexus-web/src/lib/upload-filter.test.ts', ['passed']),
|
|
],
|
|
['src/lib/upload-filter.test.ts'],
|
|
'gitnexus-web',
|
|
);
|
|
expect(result.report.success).toBe(true);
|
|
expect(result.total).toBe(1);
|
|
expect(() =>
|
|
reconcileTestReports(
|
|
[report('/repo/gitnexus-web/test/unit/a.test.ts', ['passed'])],
|
|
['test/unit/a.test.ts', 'src/lib/upload-filter.test.ts'],
|
|
'gitnexus-web',
|
|
),
|
|
).toThrow(/never collected/);
|
|
});
|
|
|
|
it('requires every shard receipt and the full web inventory through the CLI', () => {
|
|
const temp = mkdtempSync(path.join(tmpdir(), 'gitnexus-completeness-cli-'));
|
|
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
const root = path.join(temp, 'gitnexus');
|
|
const webRoot = path.join(temp, 'gitnexus-web');
|
|
const receipts = path.join(temp, 'receipts');
|
|
const output = path.join(temp, 'combined.json');
|
|
for (const directory of [
|
|
path.join(root, 'scripts'),
|
|
path.join(root, 'test/unit'),
|
|
path.join(webRoot, 'src/lib'),
|
|
receipts,
|
|
]) {
|
|
mkdirSync(directory, { recursive: true });
|
|
}
|
|
const script = path.join(root, 'scripts/test-completeness.ts');
|
|
copyFileSync(path.join(packageRoot, 'scripts/test-completeness.ts'), script);
|
|
writeFileSync(path.join(temp, 'package.json'), '{"type":"module"}');
|
|
writeFileSync(path.join(root, 'test/unit/a.test.ts'), '// inventory fixture');
|
|
writeFileSync(path.join(webRoot, 'src/lib/a.test.ts'), '// inventory fixture');
|
|
const core = report(path.join(root, 'test/unit/a.test.ts'), ['passed']);
|
|
const web = report(path.join(webRoot, 'src/lib/a.test.ts'), ['passed']);
|
|
const webReport = path.join(webRoot, 'web-test-results.json');
|
|
for (const name of [
|
|
'coverage',
|
|
'benchmarks',
|
|
'preflight',
|
|
'windows-latest-1',
|
|
'windows-latest-2',
|
|
'macos-latest-1',
|
|
'macos-latest-2',
|
|
]) {
|
|
writeFileSync(path.join(receipts, `${name}.json`), JSON.stringify(core));
|
|
}
|
|
const run = () => {
|
|
writeFileSync(webReport, JSON.stringify(web));
|
|
return spawnSync(process.execPath, ['--import', 'tsx', script, receipts, output, '2'], {
|
|
cwd: packageRoot,
|
|
encoding: 'utf8',
|
|
timeout: 30_000,
|
|
});
|
|
};
|
|
try {
|
|
const complete = run();
|
|
expect(complete.error, complete.stderr).toBeUndefined();
|
|
expect(complete.status, complete.stderr).toBe(0);
|
|
expect(JSON.parse(readFileSync(output, 'utf8')).numPendingTests).toBe(0);
|
|
writeFileSync(path.join(webRoot, 'src/lib/missing.test.ts'), '// missing receipt');
|
|
const missingSource = run();
|
|
expect(missingSource.status).not.toBe(0);
|
|
expect(missingSource.stderr).toContain(
|
|
'Test file was never collected: src/lib/missing.test.ts',
|
|
);
|
|
rmSync(path.join(webRoot, 'src/lib/missing.test.ts'));
|
|
rmSync(path.join(receipts, 'windows-latest-2.json'));
|
|
const missingShard = run();
|
|
expect(missingShard.status).not.toBe(0);
|
|
expect(missingShard.stderr).toContain('windows-latest-2.json');
|
|
} finally {
|
|
rmSync(temp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('rejects test files missing from the collected inventory', () => {
|
|
expect(() =>
|
|
reconcileTestReports(
|
|
[report('/repo/gitnexus/test/unit/a.test.ts', ['passed'])],
|
|
['test/unit/a.test.ts', 'test/unit/b.test.ts'],
|
|
),
|
|
).toThrow(/b.test.ts/);
|
|
});
|
|
|
|
it('rejects missing, empty or malformed reports instead of reporting zero skips', () => {
|
|
expect(() => reconcileTestReports([])).toThrow();
|
|
expect(() => reconcileTestReports([{ testResults: [] }])).toThrow();
|
|
expect(() =>
|
|
reconcileTestReports([{ testResults: [{ name: 'a', assertionResults: [] }] }]),
|
|
).toThrow();
|
|
expect(() =>
|
|
reconcileTestReports([report('/repo/gitnexus/test/unit/a.test.ts', ['unknown'])]),
|
|
).toThrow();
|
|
});
|
|
|
|
it('keeps failed collection and unhandled runner errors fatal', () => {
|
|
const broken = report('/repo/gitnexus/test/unit/a.test.ts', ['passed']);
|
|
broken.success = false;
|
|
expect(reconcileTestReports([broken]).failures).not.toHaveLength(0);
|
|
broken.success = true;
|
|
broken.executionErrors = 1;
|
|
expect(reconcileTestReports([broken]).failures).not.toHaveLength(0);
|
|
broken.executionErrors = 0;
|
|
broken.testResults[0].status = 'failed';
|
|
expect(reconcileTestReports([broken]).failures).not.toHaveLength(0);
|
|
expect(reconcileTestReports([broken]).report.numFailedTestSuites).toBe(1);
|
|
expect(reconcileTestReports([broken]).report.testResults[0].status).toBe('failed');
|
|
});
|
|
|
|
it('captures a real unhandled rejection even when Vitest exits successfully', () => {
|
|
const temp = mkdtempSync(path.join(tmpdir(), 'gitnexus-receipt-'));
|
|
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
const root = path.join(temp, 'gitnexus');
|
|
const output = path.join(temp, 'receipt.json');
|
|
mkdirSync(path.join(root, 'test/unit'), { recursive: true });
|
|
const vitestImport = pathToFileURL(
|
|
path.join(packageRoot, 'node_modules/vitest/dist/index.js'),
|
|
).href;
|
|
writeFileSync(
|
|
path.join(root, 'test/unit/rejection.test.ts'),
|
|
`
|
|
import { it, expect } from ${JSON.stringify(vitestImport)};
|
|
it('executes its assertion but leaks a rejection', async () => {
|
|
expect(2 + 2).toBe(4);
|
|
setTimeout(() => { void Promise.reject(new Error('receipt rejection probe')); }, 0);
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
});
|
|
`,
|
|
);
|
|
const config = path.join(root, 'vitest.config.mjs');
|
|
writeFileSync(
|
|
config,
|
|
'export default { test: { include: ["test/**/*.test.ts"], includeTaskLocation: true, dangerouslyIgnoreUnhandledErrors: true } };',
|
|
);
|
|
try {
|
|
const child = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.join(packageRoot, 'node_modules/vitest/vitest.mjs'),
|
|
'run',
|
|
'--root',
|
|
root,
|
|
'--config',
|
|
config,
|
|
'--reporter',
|
|
path.join(packageRoot, 'scripts/execution-reporter.ts'),
|
|
'--outputFile',
|
|
output,
|
|
],
|
|
{ cwd: packageRoot, encoding: 'utf8', timeout: 30_000 },
|
|
);
|
|
expect(child.error, child.stderr).toBeUndefined();
|
|
expect(child.status, child.stderr).toBe(0);
|
|
const receipt = JSON.parse(readFileSync(output, 'utf8'));
|
|
expect(receipt.numPassedTests).toBe(1);
|
|
expect(receipt.executionErrors).toBeGreaterThan(0);
|
|
expect(reconcileTestReports([receipt]).report.success).toBe(false);
|
|
} finally {
|
|
rmSync(temp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|