mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-08 12:47:51 +00:00
* feat: add governed file-backed work products * test: cover file work product delivery paths * test: cover work product artifact persistence * fix: harden artifact file handling * fix: harden work product artifact boundaries * test: cover artifact source replacement
351 lines
11 KiB
JavaScript
351 lines
11 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
evaluateCoverage,
|
|
executableChangedLineNumbers,
|
|
markdownSummary,
|
|
normalizeCoverageSummary,
|
|
normalizeDetailedCoverage,
|
|
parseChangedLineNumbers,
|
|
parseChangedLineSpans,
|
|
parseSelectedPackages,
|
|
} from './check-coverage-ratchets.mjs';
|
|
|
|
const metric = (covered, total) => ({ covered, total, skipped: 0, pct: (covered / total) * 100 });
|
|
const fileCoverage = (covered, total) => ({
|
|
lines: metric(covered, total),
|
|
branches: metric(covered, total),
|
|
functions: metric(covered, total),
|
|
statements: metric(covered, total),
|
|
});
|
|
|
|
const policy = {
|
|
schemaVersion: 'critical-path-coverage/v1',
|
|
longTermTarget: { lines: 80, branches: 80, functions: 80, statements: 80 },
|
|
packages: [
|
|
{
|
|
id: 'server',
|
|
runner: { testFiles: ['src/__tests__/auth.test.ts'] },
|
|
boundaries: [
|
|
{
|
|
id: 'auth',
|
|
description: 'Authentication boundary.',
|
|
include: ['server/src/middleware/**/*.ts'],
|
|
thresholds: { lines: 75, branches: 75, functions: 75, statements: 75 },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
test('normalizes absolute report paths to repository-relative POSIX paths', () => {
|
|
const summary = {
|
|
total: fileCoverage(1, 1),
|
|
'/repo/server/src/middleware/auth.ts': fileCoverage(3, 4),
|
|
};
|
|
|
|
assert.deepEqual(Object.keys(normalizeCoverageSummary(summary, '/repo')), [
|
|
'server/src/middleware/auth.ts',
|
|
]);
|
|
assert.deepEqual(
|
|
Object.keys(
|
|
normalizeDetailedCoverage(
|
|
{ '/repo/server/src/middleware/auth.ts': { statementMap: {}, s: {} } },
|
|
'/repo'
|
|
)
|
|
),
|
|
['server/src/middleware/auth.ts']
|
|
);
|
|
});
|
|
|
|
test('extracts added and modified line numbers from zero-context diffs', () => {
|
|
const lines = parseChangedLineNumbers(
|
|
'@@ -4 +4,2 @@\n-old\n+new\n+next\n@@ -10,2 +11 @@\n-old\n-old\n+new\n@@ -20 +21,0 @@\n-old\n'
|
|
);
|
|
assert.deepEqual([...lines], [4, 5, 11]);
|
|
});
|
|
|
|
test('keeps only executable changed lines and ignores comment-only or type-only edits', () => {
|
|
const previous = 'const value: OldType = run(\n input\n);\n';
|
|
const commentOnly = 'const value: OldType = run(\n // rationale\n input\n);\n';
|
|
assert.deepEqual([...executableChangedLineNumbers(commentOnly, new Set([2]), previous)], []);
|
|
|
|
const typeOnly = 'const value: NewType = run(\n input\n);\n';
|
|
assert.deepEqual([...executableChangedLineNumbers(typeOnly, new Set([1]), previous)], []);
|
|
|
|
const runtimeChange = 'const value: OldType = run(\n replacement\n);\n';
|
|
assert.deepEqual([...executableChangedLineNumbers(runtimeChange, new Set([2]), previous)], [2]);
|
|
});
|
|
|
|
test('ignores type-only tokens when the same file also changes runtime behavior', () => {
|
|
const previous = 'type Input = OldType;\nconst value = run(input);\n';
|
|
const current = 'type Input = NewType;\nconst value = run(replacement);\n';
|
|
const diff = [
|
|
'@@ -1,2 +1,2 @@',
|
|
'-type Input = OldType;',
|
|
'-const value = run(input);',
|
|
'+type Input = NewType;',
|
|
'+const value = run(replacement);',
|
|
].join('\n');
|
|
const changedLines = parseChangedLineNumbers(diff);
|
|
const changedSpans = parseChangedLineSpans(diff);
|
|
|
|
assert.deepEqual(
|
|
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
|
[2]
|
|
);
|
|
});
|
|
|
|
test('distinguishes added type punctuation from deletion-only runtime edits', () => {
|
|
const previous = [
|
|
'const value = run(input);',
|
|
'const result = keep(original);',
|
|
'verify(input);',
|
|
].join('\n');
|
|
const current = [
|
|
'const value: Input = run(input);',
|
|
'const result = keep(replacement);',
|
|
'verify();',
|
|
].join('\n');
|
|
const diff = [
|
|
'@@ -1,3 +1,3 @@',
|
|
'-const value = run(input);',
|
|
'-const result = keep(original);',
|
|
'-verify(input);',
|
|
'+const value: Input = run(input);',
|
|
'+const result = keep(replacement);',
|
|
'+verify();',
|
|
].join('\n');
|
|
const changedLines = parseChangedLineNumbers(diff);
|
|
const changedSpans = parseChangedLineSpans(diff);
|
|
|
|
assert.deepEqual(
|
|
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
|
[2, 3]
|
|
);
|
|
});
|
|
|
|
test('retains runtime-significant whitespace insertions', () => {
|
|
const previous = 'const kind = typeofx;\n';
|
|
const current = 'const kind = typeof x;\n';
|
|
const diff = '@@ -1 +1 @@\n-const kind = typeofx;\n+const kind = typeof x;\n';
|
|
const changedLines = parseChangedLineNumbers(diff);
|
|
const changedSpans = parseChangedLineSpans(diff);
|
|
|
|
assert.deepEqual(
|
|
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
|
[1]
|
|
);
|
|
});
|
|
|
|
test('treats line-count-only statement formatting as one runtime-neutral hunk', () => {
|
|
const previous = ['const value = run(input);', '', 'const result = keep(original);'].join('\n');
|
|
const current = [
|
|
'const value = run(',
|
|
' input',
|
|
');',
|
|
'',
|
|
'const result = keep(replacement);',
|
|
].join('\n');
|
|
const diff = [
|
|
'@@ -1 +1,3 @@',
|
|
'-const value = run(input);',
|
|
'+const value = run(',
|
|
'+ input',
|
|
'+);',
|
|
'@@ -3 +5 @@',
|
|
'-const result = keep(original);',
|
|
'+const result = keep(replacement);',
|
|
].join('\n');
|
|
const changedLines = parseChangedLineNumbers(diff);
|
|
const changedSpans = parseChangedLineSpans(diff);
|
|
|
|
assert.deepEqual(
|
|
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
|
[5]
|
|
);
|
|
});
|
|
|
|
test('treats equal-line-count statement formatting as one runtime-neutral hunk', () => {
|
|
const previous = [
|
|
'const value = run(input);',
|
|
'const other = use(input);',
|
|
'const result = keep(original);',
|
|
].join('\n');
|
|
const current = [
|
|
'const value = run(',
|
|
'input); const other = use(input);',
|
|
'const result = keep(replacement);',
|
|
].join('\n');
|
|
const diff = [
|
|
'@@ -1,3 +1,3 @@',
|
|
'-const value = run(input);',
|
|
'-const other = use(input);',
|
|
'-const result = keep(original);',
|
|
'+const value = run(',
|
|
'+input); const other = use(input);',
|
|
'+const result = keep(replacement);',
|
|
].join('\n');
|
|
const changedLines = parseChangedLineNumbers(diff);
|
|
const changedSpans = parseChangedLineSpans(diff);
|
|
|
|
assert.deepEqual(
|
|
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
|
[3]
|
|
);
|
|
});
|
|
|
|
test('aggregates a boundary and accepts its measured floor', () => {
|
|
const evaluation = evaluateCoverage(policy, {
|
|
server: {
|
|
'server/src/middleware/auth.ts': fileCoverage(3, 4),
|
|
'server/src/routes/tasks.ts': fileCoverage(0, 10),
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(evaluation.failures, []);
|
|
assert.equal(evaluation.results[0].metrics.lines.pct, 75);
|
|
assert.match(markdownSummary(evaluation, policy.longTermTarget), /server \| auth \| 75%/);
|
|
});
|
|
|
|
test('rejects a metric regression and a boundary without report entries', () => {
|
|
const regression = evaluateCoverage(policy, {
|
|
server: { 'server/src/middleware/auth.ts': fileCoverage(2, 4) },
|
|
});
|
|
assert.equal(regression.failures.length, 4);
|
|
|
|
const missing = evaluateCoverage(policy, {
|
|
server: { 'server/src/routes/tasks.ts': fileCoverage(4, 4) },
|
|
});
|
|
assert.deepEqual(missing.failures, ['server/auth matched no reported source files']);
|
|
assert.equal(missing.results[0].status, 'fail');
|
|
assert.equal(missing.results[0].metrics.lines.pct, 0);
|
|
});
|
|
|
|
test('requires bounded reviewed exceptions and omits active exceptions', () => {
|
|
const withException = globalThis.structuredClone(policy);
|
|
withException.packages[0].boundaries[0].exceptions = [
|
|
{
|
|
path: 'server/src/middleware/legacy.ts',
|
|
reason: 'Legacy adapter awaiting removal.',
|
|
owner: 'BradGroux',
|
|
trackingIssue: '#1169',
|
|
reviewBy: '2026-09-30',
|
|
},
|
|
];
|
|
const evaluation = evaluateCoverage(
|
|
withException,
|
|
{
|
|
server: {
|
|
'server/src/middleware/auth.ts': fileCoverage(4, 4),
|
|
'server/src/middleware/legacy.ts': fileCoverage(0, 4),
|
|
},
|
|
},
|
|
'2026-08-23'
|
|
);
|
|
assert.deepEqual(evaluation.failures, []);
|
|
assert.deepEqual(evaluation.results[0].files, ['server/src/middleware/auth.ts']);
|
|
|
|
withException.packages[0].boundaries[0].exceptions[0].reviewBy = 'never';
|
|
assert.throws(
|
|
() => evaluateCoverage(withException, { server: {} }, '2026-08-23'),
|
|
/reviewBy must be a real YYYY-MM-DD date/
|
|
);
|
|
});
|
|
|
|
test('rejects a changed critical file without covered executable lines', () => {
|
|
const uncovered = evaluateCoverage(
|
|
policy,
|
|
{
|
|
server: {
|
|
'server/src/middleware/auth.ts': fileCoverage(3, 4),
|
|
'server/src/middleware/new-auth.ts': fileCoverage(0, 4),
|
|
},
|
|
},
|
|
'2026-08-23',
|
|
['server/src/middleware/new-auth.ts']
|
|
);
|
|
assert.ok(
|
|
uncovered.failures.includes(
|
|
'server/auth changed critical file server/src/middleware/new-auth.ts has no covered lines'
|
|
)
|
|
);
|
|
assert.equal(uncovered.results[0].status, 'fail');
|
|
});
|
|
|
|
test('requires changed executable statements themselves to be covered', () => {
|
|
const summary = { server: { 'server/src/middleware/auth.ts': fileCoverage(3, 4) } };
|
|
const details = {
|
|
server: {
|
|
'server/src/middleware/auth.ts': {
|
|
statementMap: {
|
|
0: { start: { line: 10 }, end: { line: 10 } },
|
|
1: { start: { line: 20 }, end: { line: 22 } },
|
|
},
|
|
s: { 0: 1, 1: 0 },
|
|
},
|
|
},
|
|
};
|
|
const changedLines = new Map([['server/src/middleware/auth.ts', new Set([21])]]);
|
|
const uncovered = evaluateCoverage(
|
|
policy,
|
|
summary,
|
|
'2026-08-23',
|
|
['server/src/middleware/auth.ts'],
|
|
details,
|
|
changedLines
|
|
);
|
|
assert.match(uncovered.failures.join('\n'), /uncovered executable statements on line\(s\) 20/);
|
|
assert.equal(uncovered.results[0].status, 'fail');
|
|
|
|
details.server['server/src/middleware/auth.ts'].s[1] = 1;
|
|
const covered = evaluateCoverage(
|
|
policy,
|
|
summary,
|
|
'2026-08-23',
|
|
['server/src/middleware/auth.ts'],
|
|
details,
|
|
changedLines
|
|
);
|
|
assert.deepEqual(covered.failures, []);
|
|
assert.equal(covered.results[0].status, 'pass');
|
|
});
|
|
|
|
test('does not require executable coverage for a type-only changed critical file', () => {
|
|
const summary = { server: { 'server/src/middleware/auth.ts': fileCoverage(3, 4) } };
|
|
const covered = evaluateCoverage(
|
|
policy,
|
|
summary,
|
|
'2026-08-23',
|
|
['server/src/middleware/types.ts'],
|
|
{ server: {} },
|
|
new Map([['server/src/middleware/types.ts', new Set()]])
|
|
);
|
|
|
|
assert.deepEqual(covered.failures, []);
|
|
assert.equal(covered.results[0].status, 'pass');
|
|
});
|
|
|
|
test('rejects broad and untracked exceptions', () => {
|
|
const invalid = globalThis.structuredClone(policy);
|
|
invalid.packages[0].boundaries[0].exceptions = [
|
|
{
|
|
path: 'server/src/middleware/**',
|
|
reason: 'This reason is long enough for review.',
|
|
owner: 'BradGroux',
|
|
trackingIssue: '#1169',
|
|
reviewBy: '2026-09-01',
|
|
},
|
|
];
|
|
assert.throws(
|
|
() => evaluateCoverage(invalid, { server: {} }, '2026-08-23'),
|
|
/one exact repository-relative file/
|
|
);
|
|
});
|
|
|
|
test('validates selected coverage packages', () => {
|
|
assert.deepEqual(parseSelectedPackages([], ['server', 'web']), ['server', 'web']);
|
|
assert.deepEqual(parseSelectedPackages(['--packages=web'], ['server', 'web']), ['web']);
|
|
assert.throws(() => parseSelectedPackages(['--packages', 'unknown'], ['server']), /Unknown/);
|
|
});
|