mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 09:26:14 -08:00
Adds contract-focused regression coverage for the Electron HTTP server, remote-control events, settings events, and managed download paths, and makes Tier A coverage fail closed when instrumentation fails or a runtime-owning production file disappears from a project or from the merged Istanbul report. The old `coverage:ci` exited 0 despite a `Failed to collect coverage` diagnostic: libs/m3u-state/src/lib/effects.ts was simply absent from the merged map. All 30 Tier A reports are now required, the merged map covers 710 files, and effects.ts is reported as 0/159 instead of silently disappearing. Also fixes remote static-file path containment for encoded, malformed, NUL, POSIX and Win32-style traversal inputs, with behavior-preserving testability seams. Statements 69.27% -> 69.54%; http-server.ts 0% -> 90.21%, remote-control.events.ts 0% -> 96.55%, settings.events.ts 59.25% -> 96.29%.
905 lines
29 KiB
JavaScript
905 lines
29 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, describe, it } from 'node:test';
|
|
|
|
import {
|
|
createCoverageOutputScanner,
|
|
evaluateCoverageRatchets,
|
|
hasRuntimeOwnedStatement,
|
|
validateMergedCoverage,
|
|
validateProjectCoverage,
|
|
validateRequiredProjectReports,
|
|
} from './coverage-integrity.mjs';
|
|
|
|
const temporaryRoots = [];
|
|
|
|
afterEach(() => {
|
|
for (const root of temporaryRoots.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function makeProjectFixture({
|
|
name = 'example',
|
|
projectRoot = `libs/${name}`,
|
|
workspaceRoot = undefined,
|
|
} = {}) {
|
|
const ownsWorkspace = workspaceRoot === undefined;
|
|
const fixtureRoot =
|
|
workspaceRoot ??
|
|
mkdtempSync(path.join(tmpdir(), 'iptvnator-coverage-integrity-'));
|
|
if (ownsWorkspace) {
|
|
temporaryRoots.push(fixtureRoot);
|
|
}
|
|
|
|
const sourceRoot = `${projectRoot}/src`;
|
|
const sourcePath = path.join(fixtureRoot, sourceRoot, 'runtime.ts');
|
|
const reportPath = path.join(
|
|
fixtureRoot,
|
|
'coverage',
|
|
projectRoot,
|
|
'coverage-final.json'
|
|
);
|
|
|
|
mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
writeFileSync(sourcePath, 'export const runtime = true;\n');
|
|
|
|
const project = {
|
|
name,
|
|
root: projectRoot,
|
|
sourceRoot,
|
|
};
|
|
|
|
return {
|
|
project,
|
|
reportPath,
|
|
sourcePath,
|
|
workspaceRoot: fixtureRoot,
|
|
};
|
|
}
|
|
|
|
function coverageEntry(filePath, hits = [0]) {
|
|
const statementMap = Object.fromEntries(
|
|
hits.map((_, index) => [
|
|
index,
|
|
{
|
|
start: { line: index + 1, column: 0 },
|
|
end: { line: index + 1, column: 28 },
|
|
},
|
|
])
|
|
);
|
|
const statementHits = Object.fromEntries(
|
|
hits.map((hit, index) => [index, hit])
|
|
);
|
|
|
|
return {
|
|
path: filePath,
|
|
statementMap,
|
|
fnMap: {},
|
|
branchMap: {},
|
|
s: statementHits,
|
|
f: {},
|
|
b: {},
|
|
};
|
|
}
|
|
|
|
function functionOnlyCoverageEntry(filePath) {
|
|
return {
|
|
path: filePath,
|
|
statementMap: {},
|
|
fnMap: {
|
|
0: {
|
|
name: 'runtime',
|
|
decl: {
|
|
start: { line: 1, column: 7 },
|
|
end: { line: 1, column: 15 },
|
|
},
|
|
loc: {
|
|
start: { line: 1, column: 7 },
|
|
end: { line: 1, column: 28 },
|
|
},
|
|
line: 1,
|
|
},
|
|
},
|
|
branchMap: {},
|
|
s: {},
|
|
f: { 0: 1 },
|
|
b: {},
|
|
};
|
|
}
|
|
|
|
function writeCompleteReport(fixture, reportKey = fixture.sourcePath) {
|
|
writeFileSync(
|
|
fixture.reportPath,
|
|
JSON.stringify({
|
|
[reportKey]: coverageEntry(fixture.sourcePath),
|
|
})
|
|
);
|
|
}
|
|
|
|
function fullyCoveredSummary() {
|
|
return {
|
|
statements: { covered: 1, total: 1, pct: 100 },
|
|
branches: { covered: 0, total: 0, pct: 100 },
|
|
functions: { covered: 0, total: 0, pct: 100 },
|
|
lines: { covered: 1, total: 1, pct: 100 },
|
|
};
|
|
}
|
|
|
|
function ratchetWithCriticalFiles(criticalFiles) {
|
|
return {
|
|
merged: {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
criticalFiles,
|
|
};
|
|
}
|
|
|
|
describe('hasRuntimeOwnedStatement', () => {
|
|
it('excludes type-only, import-only, and pure re-export source', () => {
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement(
|
|
'import type { Settings } from "./settings";'
|
|
),
|
|
false
|
|
);
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement(
|
|
'import settings = require("./settings");'
|
|
),
|
|
false
|
|
);
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement('interface Settings { enabled: boolean }'),
|
|
false
|
|
);
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement('export type Mode = "live" | "vod";'),
|
|
false
|
|
);
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement('export { value } from "./value";'),
|
|
false
|
|
);
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement('declare const injected: string;'),
|
|
false
|
|
);
|
|
});
|
|
|
|
it('includes emitted declarations and executable statements', () => {
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement('export const enabled = true;'),
|
|
true
|
|
);
|
|
assert.equal(
|
|
hasRuntimeOwnedStatement(
|
|
'export class RuntimeBoundary { start() {} }'
|
|
),
|
|
true
|
|
);
|
|
assert.equal(hasRuntimeOwnedStatement('console.log("runtime");'), true);
|
|
});
|
|
});
|
|
|
|
describe('createCoverageOutputScanner', () => {
|
|
it('detects an ANSI-colored marker split across chunks', () => {
|
|
const scanner = createCoverageOutputScanner(128);
|
|
|
|
scanner.push('\u001b[31mFailed to collect');
|
|
scanner.push(' coverage from /workspace/effects.ts\u001b[39m');
|
|
|
|
assert.equal(scanner.collectionFailed, true);
|
|
});
|
|
|
|
it('detects a marker across a split ANSI escape sequence', () => {
|
|
const scanner = createCoverageOutputScanner(128);
|
|
|
|
scanner.push('Failed to \u001b[3');
|
|
scanner.push('1mcollect coverage from /workspace/effects.ts');
|
|
|
|
assert.equal(scanner.collectionFailed, true);
|
|
assert.equal(scanner.tail.includes('\u001b'), false);
|
|
});
|
|
|
|
it('keeps only a bounded rolling tail', () => {
|
|
const scanner = createCoverageOutputScanner(32);
|
|
|
|
scanner.push('x'.repeat(256));
|
|
|
|
assert.equal(scanner.tail.length, 32);
|
|
assert.equal(scanner.collectionFailed, false);
|
|
});
|
|
|
|
it('detects the marker before trimming a long chunk', () => {
|
|
const scanner = createCoverageOutputScanner(32);
|
|
|
|
scanner.push(`Failed to collect coverage${'x'.repeat(256)}`);
|
|
|
|
assert.equal(scanner.tail.length, 32);
|
|
assert.equal(scanner.collectionFailed, true);
|
|
});
|
|
});
|
|
|
|
describe('validateProjectCoverage', () => {
|
|
it('accepts a report containing every runtime-owning file', () => {
|
|
const fixture = makeProjectFixture();
|
|
const relativeReportKey = path.relative(
|
|
fixture.workspaceRoot,
|
|
fixture.sourcePath
|
|
);
|
|
writeCompleteReport(fixture, relativeReportKey);
|
|
|
|
const result = validateProjectCoverage(fixture);
|
|
|
|
assert.deepEqual(result.errors, []);
|
|
assert.equal(result.report?.project.name, 'example');
|
|
assert.equal(result.report?.path, fixture.reportPath);
|
|
});
|
|
|
|
it('reports a runtime-owning source omitted from a valid report', () => {
|
|
const fixture = makeProjectFixture();
|
|
writeFileSync(fixture.reportPath, '{}');
|
|
|
|
const result = validateProjectCoverage(fixture);
|
|
|
|
assert.equal(result.errors.length, 1);
|
|
assert.match(result.errors[0], /example/);
|
|
assert.match(result.errors[0], /libs\/example\/src\/runtime\.ts/);
|
|
});
|
|
|
|
it('rejects a malformed Istanbul coverage entry', () => {
|
|
const fixture = makeProjectFixture();
|
|
writeFileSync(
|
|
fixture.reportPath,
|
|
JSON.stringify({ [fixture.sourcePath]: {} })
|
|
);
|
|
|
|
const result = validateProjectCoverage(fixture);
|
|
|
|
assert.equal(result.report, undefined);
|
|
assert.match(
|
|
result.errors[0],
|
|
/example.*coverage\/libs\/example\/coverage-final\.json.*valid Istanbul/
|
|
);
|
|
});
|
|
|
|
it('rejects duplicate report paths after normalization', () => {
|
|
const fixture = makeProjectFixture();
|
|
const relativePath = path.relative(
|
|
fixture.workspaceRoot,
|
|
fixture.sourcePath
|
|
);
|
|
writeFileSync(
|
|
fixture.reportPath,
|
|
JSON.stringify({
|
|
[relativePath]: coverageEntry(relativePath),
|
|
[fixture.sourcePath]: coverageEntry(fixture.sourcePath),
|
|
})
|
|
);
|
|
|
|
const result = validateProjectCoverage(fixture);
|
|
|
|
assert.equal(result.report, undefined);
|
|
assert.equal(result.errors.length, 1);
|
|
assert.match(
|
|
result.errors[0],
|
|
/example.*coverage\/libs\/example\/coverage-final\.json.*duplicate.*libs\/example\/src\/runtime\.ts.*normalization/
|
|
);
|
|
});
|
|
|
|
it('rejects a runtime source without usable instrumentation', () => {
|
|
const fixture = makeProjectFixture();
|
|
writeFileSync(
|
|
fixture.reportPath,
|
|
JSON.stringify({
|
|
[fixture.sourcePath]: coverageEntry(fixture.sourcePath, []),
|
|
})
|
|
);
|
|
|
|
const result = validateProjectCoverage(fixture);
|
|
|
|
assert.equal(result.errors.length, 1);
|
|
assert.match(result.errors[0], /example/);
|
|
assert.match(result.errors[0], /libs\/example\/src\/runtime\.ts/);
|
|
assert.match(result.errors[0], /usable instrumentation/);
|
|
});
|
|
|
|
it('accepts function-only instrumentation for a runtime source', () => {
|
|
const fixture = makeProjectFixture();
|
|
writeFileSync(
|
|
fixture.sourcePath,
|
|
'export function runtime() {}\n'
|
|
);
|
|
writeFileSync(
|
|
fixture.reportPath,
|
|
JSON.stringify({
|
|
[fixture.sourcePath]: functionOnlyCoverageEntry(
|
|
fixture.sourcePath
|
|
),
|
|
})
|
|
);
|
|
|
|
const result = validateProjectCoverage(fixture);
|
|
|
|
assert.deepEqual(result.errors, []);
|
|
assert.equal(result.report?.project.name, 'example');
|
|
});
|
|
|
|
it('does not require excluded or type-only TypeScript sources', () => {
|
|
const fixture = makeProjectFixture();
|
|
const excludedSources = [
|
|
'feature.spec.ts',
|
|
'feature.test.ts',
|
|
'ambient.d.ts',
|
|
'test-setup.ts',
|
|
'test-stubs/runtime.ts',
|
|
'feature.generated.ts',
|
|
'environments/environment.ts',
|
|
'index.ts',
|
|
];
|
|
for (const relativePath of excludedSources) {
|
|
const filePath = path.join(
|
|
fixture.workspaceRoot,
|
|
fixture.project.sourceRoot,
|
|
relativePath
|
|
);
|
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
writeFileSync(filePath, 'export const omitted = true;\n');
|
|
}
|
|
writeFileSync(
|
|
path.join(
|
|
fixture.workspaceRoot,
|
|
fixture.project.sourceRoot,
|
|
'contract.ts'
|
|
),
|
|
'export interface Contract { enabled: boolean }\n'
|
|
);
|
|
writeCompleteReport(fixture);
|
|
|
|
assert.deepEqual(validateProjectCoverage(fixture).errors, []);
|
|
});
|
|
|
|
it('reports missing, malformed, and non-object project reports', () => {
|
|
const missing = makeProjectFixture();
|
|
const malformed = makeProjectFixture();
|
|
const array = makeProjectFixture();
|
|
const nullValue = makeProjectFixture();
|
|
writeFileSync(malformed.reportPath, '{ invalid json');
|
|
writeFileSync(array.reportPath, '[]');
|
|
writeFileSync(nullValue.reportPath, 'null');
|
|
|
|
assert.match(
|
|
validateProjectCoverage(missing).errors[0],
|
|
/example.*did not produce.*coverage\/libs\/example\/coverage-final\.json/
|
|
);
|
|
assert.match(
|
|
validateProjectCoverage(malformed).errors[0],
|
|
/example.*invalid JSON/
|
|
);
|
|
assert.match(
|
|
validateProjectCoverage(array).errors[0],
|
|
/example.*JSON object/
|
|
);
|
|
assert.match(
|
|
validateProjectCoverage(nullValue).errors[0],
|
|
/example.*JSON object/
|
|
);
|
|
});
|
|
|
|
it('requires every configured report and preserves policy order', () => {
|
|
const first = makeProjectFixture({
|
|
name: 'first',
|
|
projectRoot: 'libs/first',
|
|
});
|
|
const missing = makeProjectFixture({
|
|
name: 'missing',
|
|
projectRoot: 'libs/missing',
|
|
workspaceRoot: first.workspaceRoot,
|
|
});
|
|
const last = makeProjectFixture({
|
|
name: 'last',
|
|
projectRoot: 'libs/last',
|
|
workspaceRoot: first.workspaceRoot,
|
|
});
|
|
writeCompleteReport(first);
|
|
writeCompleteReport(last);
|
|
|
|
const result = validateRequiredProjectReports({
|
|
projects: [last.project, missing.project, first.project],
|
|
workspaceRoot: first.workspaceRoot,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
result.reports.map((report) => report.project.name),
|
|
['last', 'first']
|
|
);
|
|
assert.equal(result.errors.length, 1);
|
|
assert.match(result.errors[0], /missing/);
|
|
});
|
|
});
|
|
|
|
describe('validateMergedCoverage', () => {
|
|
it('reports the exact runtime source omitted from an otherwise valid merged map', () => {
|
|
const included = makeProjectFixture({
|
|
name: 'included',
|
|
projectRoot: 'libs/included',
|
|
});
|
|
const omitted = makeProjectFixture({
|
|
name: 'omitted',
|
|
projectRoot: 'libs/omitted',
|
|
workspaceRoot: included.workspaceRoot,
|
|
});
|
|
writeCompleteReport(included);
|
|
writeCompleteReport(omitted);
|
|
|
|
assert.deepEqual(validateProjectCoverage(included).errors, []);
|
|
assert.deepEqual(validateProjectCoverage(omitted).errors, []);
|
|
|
|
const result = validateMergedCoverage({
|
|
coverageData: {
|
|
[included.sourcePath]: coverageEntry(included.sourcePath),
|
|
},
|
|
projects: [included.project, omitted.project],
|
|
reportPath: path.join(
|
|
included.workspaceRoot,
|
|
'coverage/merged/coverage-final.json'
|
|
),
|
|
workspaceRoot: included.workspaceRoot,
|
|
});
|
|
|
|
assert.deepEqual(result.errors, [
|
|
'Merged coverage report coverage/merged/coverage-final.json is missing runtime-owning source libs/omitted/src/runtime.ts from Tier A project omitted.',
|
|
]);
|
|
});
|
|
|
|
it('fails completeness when permissive ratchets accept a recomputed summary', () => {
|
|
const fixture = makeProjectFixture({
|
|
name: 'm3u-state',
|
|
projectRoot: 'libs/m3u-state',
|
|
});
|
|
const effectsPath = path.join(
|
|
fixture.workspaceRoot,
|
|
fixture.project.sourceRoot,
|
|
'effects.ts'
|
|
);
|
|
writeFileSync(
|
|
effectsPath,
|
|
'export const effects = () => "runtime";\n'
|
|
);
|
|
writeFileSync(
|
|
fixture.reportPath,
|
|
JSON.stringify({
|
|
[fixture.sourcePath]: coverageEntry(fixture.sourcePath, [1]),
|
|
[effectsPath]: coverageEntry(effectsPath, [0]),
|
|
})
|
|
);
|
|
assert.deepEqual(validateProjectCoverage(fixture).errors, []);
|
|
|
|
const coverageData = {
|
|
[fixture.sourcePath]: coverageEntry(fixture.sourcePath, [1]),
|
|
};
|
|
const completeness = validateMergedCoverage({
|
|
coverageData,
|
|
projects: [fixture.project],
|
|
reportPath: path.join(
|
|
fixture.workspaceRoot,
|
|
'coverage/merged/coverage-final.json'
|
|
),
|
|
workspaceRoot: fixture.workspaceRoot,
|
|
});
|
|
const recomputedSummary = completeness.coverageMap
|
|
.getCoverageSummary()
|
|
.toJSON();
|
|
|
|
assert.deepEqual(
|
|
evaluateCoverageRatchets({
|
|
coverageData,
|
|
mergedSummary: recomputedSummary,
|
|
ratchet: ratchetWithCriticalFiles([]),
|
|
workspaceRoot: fixture.workspaceRoot,
|
|
}),
|
|
[]
|
|
);
|
|
assert.deepEqual(completeness.errors, [
|
|
'Merged coverage report coverage/merged/coverage-final.json is missing runtime-owning source libs/m3u-state/src/effects.ts from Tier A project m3u-state.',
|
|
]);
|
|
});
|
|
|
|
it('accepts a complete merged map with normalized relative and absolute paths', () => {
|
|
const relative = makeProjectFixture({
|
|
name: 'relative',
|
|
projectRoot: 'libs/relative',
|
|
});
|
|
const absolute = makeProjectFixture({
|
|
name: 'absolute',
|
|
projectRoot: 'libs/absolute',
|
|
workspaceRoot: relative.workspaceRoot,
|
|
});
|
|
const relativePath = path.relative(
|
|
relative.workspaceRoot,
|
|
relative.sourcePath
|
|
);
|
|
|
|
const result = validateMergedCoverage({
|
|
coverageData: {
|
|
[relativePath]: coverageEntry(relativePath),
|
|
[absolute.sourcePath]: coverageEntry(absolute.sourcePath),
|
|
},
|
|
projects: [relative.project, absolute.project],
|
|
reportPath: path.join(
|
|
relative.workspaceRoot,
|
|
'coverage/merged/coverage-final.json'
|
|
),
|
|
workspaceRoot: relative.workspaceRoot,
|
|
});
|
|
|
|
assert.deepEqual(result.errors, []);
|
|
});
|
|
|
|
it('rejects unusable merged instrumentation for a runtime source', () => {
|
|
const fixture = makeProjectFixture();
|
|
|
|
const result = validateMergedCoverage({
|
|
coverageData: {
|
|
[fixture.sourcePath]: coverageEntry(fixture.sourcePath, []),
|
|
},
|
|
projects: [fixture.project],
|
|
reportPath: path.join(
|
|
fixture.workspaceRoot,
|
|
'coverage/merged/coverage-final.json'
|
|
),
|
|
workspaceRoot: fixture.workspaceRoot,
|
|
});
|
|
|
|
assert.deepEqual(result.errors, [
|
|
'Merged coverage report coverage/merged/coverage-final.json has no usable instrumentation for runtime-owning source libs/example/src/runtime.ts from Tier A project example.',
|
|
]);
|
|
});
|
|
|
|
it('rejects duplicate merged paths after normalization', () => {
|
|
const fixture = makeProjectFixture();
|
|
const relativePath = path.relative(
|
|
fixture.workspaceRoot,
|
|
fixture.sourcePath
|
|
);
|
|
|
|
const result = validateMergedCoverage({
|
|
coverageData: {
|
|
[relativePath]: coverageEntry(relativePath),
|
|
[fixture.sourcePath]: coverageEntry(fixture.sourcePath),
|
|
},
|
|
projects: [fixture.project],
|
|
reportPath: path.join(
|
|
fixture.workspaceRoot,
|
|
'coverage/merged/coverage-final.json'
|
|
),
|
|
workspaceRoot: fixture.workspaceRoot,
|
|
});
|
|
|
|
assert.deepEqual(result.errors, [
|
|
'Merged coverage report coverage/merged/coverage-final.json contains a duplicate entry for libs/example/src/runtime.ts after path normalization.',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('evaluateCoverageRatchets', () => {
|
|
it('reports all aggregate percentage regressions', () => {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: {
|
|
statements: { covered: 68, total: 100, pct: 68 },
|
|
branches: { covered: 57, total: 100, pct: 57 },
|
|
functions: { covered: 67, total: 100, pct: 67 },
|
|
lines: { covered: 69, total: 100, pct: 69 },
|
|
},
|
|
ratchet: {
|
|
merged: {
|
|
statements: 68.86,
|
|
branches: 58.66,
|
|
functions: 67.19,
|
|
lines: 69.2,
|
|
},
|
|
criticalFiles: [],
|
|
},
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.deepEqual(errors, [
|
|
'Merged statements coverage regressed: observed 68%, required at least 68.86%.',
|
|
'Merged branches coverage regressed: observed 57%, required at least 58.66%.',
|
|
'Merged functions coverage regressed: observed 67%, required at least 67.19%.',
|
|
'Merged lines coverage regressed: observed 69%, required at least 69.2%.',
|
|
]);
|
|
});
|
|
|
|
it('rejects missing, non-numeric, and out-of-range aggregate minima', () => {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: {
|
|
merged: {
|
|
branches: '58.66',
|
|
functions: -1,
|
|
lines: 101,
|
|
},
|
|
criticalFiles: [],
|
|
},
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.equal(errors.length, 4);
|
|
assert.match(errors[0], /merged\.statements.*undefined/);
|
|
assert.match(errors[1], /merged\.branches.*"58\.66"/);
|
|
assert.match(errors[2], /merged\.functions.*-1/);
|
|
assert.match(errors[3], /merged\.lines.*101/);
|
|
});
|
|
|
|
it('accepts an explicitly empty criticalFiles array', () => {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: ratchetWithCriticalFiles([]),
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
it('requires an explicit criticalFiles array', () => {
|
|
const merged = ratchetWithCriticalFiles([]).merged;
|
|
const ratchets = [
|
|
{ merged },
|
|
{ merged, criticalFiles: null },
|
|
];
|
|
|
|
for (const ratchet of ratchets) {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet,
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /criticalFiles.*array/);
|
|
}
|
|
});
|
|
|
|
it('rejects a non-array criticalFiles container without throwing', () => {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: ratchetWithCriticalFiles({}),
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /criticalFiles.*array.*object/);
|
|
});
|
|
|
|
it('requires a non-empty string path for every critical file', () => {
|
|
for (const invalidPath of [undefined, null, 42, '', ' ']) {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: ratchetWithCriticalFiles([
|
|
{
|
|
path: invalidPath,
|
|
statements: {
|
|
minimumCovered: 0,
|
|
minimumPercent: 0,
|
|
},
|
|
},
|
|
]),
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /criticalFiles\[0\]\.path/);
|
|
}
|
|
});
|
|
|
|
it('rejects invalid critical-file statement minima', () => {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: {
|
|
merged: {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
criticalFiles: [
|
|
{
|
|
path: 'apps/missing-values.ts',
|
|
statements: {},
|
|
},
|
|
{
|
|
path: 'apps/string-values.ts',
|
|
statements: {
|
|
minimumCovered: '2',
|
|
minimumPercent: '75',
|
|
},
|
|
},
|
|
{
|
|
path: 'apps/out-of-range.ts',
|
|
statements: {
|
|
minimumCovered: -1,
|
|
minimumPercent: 101,
|
|
},
|
|
},
|
|
{
|
|
path: 'apps/fractional.ts',
|
|
statements: {
|
|
minimumCovered: 1.5,
|
|
minimumPercent: 75,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.equal(errors.length, 7);
|
|
assert.match(
|
|
errors[0],
|
|
/apps\/missing-values\.ts.*minimumCovered.*undefined/
|
|
);
|
|
assert.match(
|
|
errors[1],
|
|
/apps\/missing-values\.ts.*minimumPercent.*undefined/
|
|
);
|
|
assert.match(
|
|
errors[2],
|
|
/apps\/string-values\.ts.*minimumCovered.*"2"/
|
|
);
|
|
assert.match(
|
|
errors[3],
|
|
/apps\/string-values\.ts.*minimumPercent.*"75"/
|
|
);
|
|
assert.match(
|
|
errors[4],
|
|
/apps\/out-of-range\.ts.*minimumCovered.*-1/
|
|
);
|
|
assert.match(
|
|
errors[5],
|
|
/apps\/out-of-range\.ts.*minimumPercent.*101/
|
|
);
|
|
assert.match(
|
|
errors[6],
|
|
/apps\/fractional\.ts.*minimumCovered.*1\.5/
|
|
);
|
|
});
|
|
|
|
it('requires both covered statements and percentage for a critical file', () => {
|
|
const workspaceRoot = '/workspace';
|
|
const filePath = path.join(workspaceRoot, 'apps/runtime.ts');
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {
|
|
[filePath]: coverageEntry(filePath, [1, 0]),
|
|
},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: {
|
|
merged: {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
criticalFiles: [
|
|
{
|
|
path: 'apps/runtime.ts',
|
|
statements: {
|
|
minimumCovered: 2,
|
|
minimumPercent: 75,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
workspaceRoot,
|
|
});
|
|
|
|
assert.deepEqual(errors, [
|
|
'Critical coverage apps/runtime.ts statements covered regressed: observed 1, required at least 2.',
|
|
'Critical coverage apps/runtime.ts statements percent regressed: observed 50%, required at least 75%.',
|
|
]);
|
|
});
|
|
|
|
it('looks up a relative Istanbul key by its normalized critical path', () => {
|
|
const workspaceRoot = process.cwd();
|
|
const relativePath = 'apps/runtime.ts';
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {
|
|
[relativePath]: coverageEntry(relativePath, [1, 1]),
|
|
},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: {
|
|
merged: {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
criticalFiles: [
|
|
{
|
|
path: relativePath,
|
|
statements: {
|
|
minimumCovered: 2,
|
|
minimumPercent: 100,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
workspaceRoot,
|
|
});
|
|
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
it('rejects duplicate Istanbul keys after path normalization', () => {
|
|
const workspaceRoot = process.cwd();
|
|
const relativePath = 'apps/runtime.ts';
|
|
const absolutePath = path.join(workspaceRoot, relativePath);
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {
|
|
[relativePath]: coverageEntry(relativePath, [1]),
|
|
[absolutePath]: coverageEntry(absolutePath, [1]),
|
|
},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: {
|
|
merged: {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
criticalFiles: [],
|
|
},
|
|
workspaceRoot,
|
|
});
|
|
|
|
assert.equal(errors.length, 1);
|
|
assert.match(
|
|
errors[0],
|
|
/duplicate.*apps\/runtime\.ts.*normalization/
|
|
);
|
|
});
|
|
|
|
it('reports a critical file missing from merged coverage', () => {
|
|
const errors = evaluateCoverageRatchets({
|
|
coverageData: {},
|
|
mergedSummary: fullyCoveredSummary(),
|
|
ratchet: {
|
|
merged: {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
criticalFiles: [
|
|
{
|
|
path: 'apps/missing.ts',
|
|
statements: {
|
|
minimumCovered: 1,
|
|
minimumPercent: 0,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
workspaceRoot: '/workspace',
|
|
});
|
|
|
|
assert.deepEqual(errors, [
|
|
'Critical coverage apps/missing.ts is missing from merged coverage.',
|
|
]);
|
|
});
|
|
});
|