Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
451 lines
18 KiB
JavaScript
451 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* eslint-glob-coverage.test.cjs
|
|
*
|
|
* Coverage for scripts/lint-eslint-glob-coverage.cjs (#3059): every unit row
|
|
* uses injected deps (no temp repos, no real subprocess, no chmod). The
|
|
* ANCHOR rows and the real-tree row drive the actual ESLint config against
|
|
* eslint.config.mjs to prove the discrimination logic holds outside the
|
|
* mocked seams.
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('path');
|
|
const fc = require('./helpers/fast-check-setup.cjs');
|
|
|
|
const {
|
|
checkGlobCoverage,
|
|
listTrackedSourceFiles,
|
|
loadAllowlist,
|
|
SOURCE_EXT_RE,
|
|
MIN_TRACKED_SOURCE_FILES,
|
|
} = require('../scripts/lint-eslint-glob-coverage.cjs');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
|
|
/** A resolveConfig stub that reports every file as covered by exactly one rule. */
|
|
function coveredResolver() {
|
|
return () => ({ ignored: false, ruleCount: 1 });
|
|
}
|
|
|
|
/** A resolveConfig stub driven by a Map<path, {ignored, ruleCount}>. */
|
|
function mapResolver(entries) {
|
|
return (relPath) => entries.get(relPath) || { ignored: false, ruleCount: 0 };
|
|
}
|
|
|
|
describe('eslint-glob-coverage: real tree (expected end state)', () => {
|
|
test('the current tree resolves clean against the guard (drives the guard to green)', async () => {
|
|
const result = await checkGlobCoverage();
|
|
assert.equal(
|
|
result.ok,
|
|
true,
|
|
`expected 0 violations, got ${result.violations.length}: ${JSON.stringify(result.violations, null, 2)}`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: uncovered detection', () => {
|
|
test('an unmatched file is reported uncovered; a matched file is not', async () => {
|
|
const resolver = mapResolver(
|
|
new Map([
|
|
['scripts/covered.cjs', { ignored: false, ruleCount: 3 }],
|
|
['scripts/escaped.cjs', { ignored: false, ruleCount: 0 }],
|
|
])
|
|
);
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['scripts/covered.cjs', 'scripts/escaped.cjs'],
|
|
resolveConfig: resolver,
|
|
allowlist: [],
|
|
minTrackedFiles: 0,
|
|
});
|
|
assert.equal(result.ok, false);
|
|
assert.deepEqual(result.escapes, [{ path: 'scripts/escaped.cjs' }]);
|
|
const kinds = result.violations.map((v) => v.path);
|
|
assert.ok(!kinds.includes('scripts/covered.cjs'));
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: allowlist reason validation', () => {
|
|
test('non-empty reason passes; empty/whitespace reason and missing reason each fail', async () => {
|
|
const resolver = mapResolver(
|
|
new Map([
|
|
['a.cjs', { ignored: false, ruleCount: 0 }],
|
|
['b.cjs', { ignored: false, ruleCount: 0 }],
|
|
['c.cjs', { ignored: false, ruleCount: 0 }],
|
|
['d.cjs', { ignored: false, ruleCount: 0 }],
|
|
])
|
|
);
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['a.cjs', 'b.cjs', 'c.cjs', 'd.cjs'],
|
|
resolveConfig: resolver,
|
|
minTrackedFiles: 0,
|
|
allowlist: [
|
|
{ path: 'a.cjs', reason: 'a real reason' },
|
|
{ path: 'b.cjs', reason: '' },
|
|
{ path: 'c.cjs', reason: ' ' },
|
|
{ path: 'd.cjs' },
|
|
],
|
|
});
|
|
const byPath = (p) => result.violations.filter((v) => v.path === p).map((v) => v.kind);
|
|
assert.deepEqual(byPath('a.cjs'), []);
|
|
assert.deepEqual(byPath('b.cjs'), ['allowlist_empty_reason']);
|
|
assert.deepEqual(byPath('c.cjs'), ['allowlist_empty_reason']);
|
|
assert.deepEqual(byPath('d.cjs'), ['allowlist_missing_reason']);
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: allowlist ratchet', () => {
|
|
test('a stale entry (path now resolves to >=1 rule) reports allowlist_stale', async () => {
|
|
const resolver = mapResolver(new Map([['now-covered.cjs', { ignored: false, ruleCount: 2 }]]));
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['now-covered.cjs'],
|
|
resolveConfig: resolver,
|
|
minTrackedFiles: 0,
|
|
allowlist: [{ path: 'now-covered.cjs', reason: 'stale — should be pruned' }],
|
|
});
|
|
assert.equal(result.ok, false);
|
|
const kinds = result.violations.filter((v) => v.path === 'now-covered.cjs').map((v) => v.kind);
|
|
assert.deepEqual(kinds, ['allowlist_stale']);
|
|
});
|
|
|
|
test('an allowlist path not in the tracked list reports allowlist_missing_path', async () => {
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['tracked.cjs'],
|
|
resolveConfig: coveredResolver(),
|
|
minTrackedFiles: 0,
|
|
allowlist: [{ path: 'ghost.cjs', reason: 'not actually tracked' }],
|
|
});
|
|
assert.equal(result.ok, false);
|
|
const kinds = result.violations.filter((v) => v.path === 'ghost.cjs').map((v) => v.kind);
|
|
assert.deepEqual(kinds, ['allowlist_missing_path']);
|
|
});
|
|
|
|
test('a duplicate entry reports allowlist_duplicate', async () => {
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['dup.cjs'],
|
|
resolveConfig: mapResolver(new Map([['dup.cjs', { ignored: false, ruleCount: 0 }]])),
|
|
minTrackedFiles: 0,
|
|
allowlist: [
|
|
{ path: 'dup.cjs', reason: 'first' },
|
|
{ path: 'dup.cjs', reason: 'second' },
|
|
],
|
|
});
|
|
assert.equal(result.ok, false);
|
|
const kinds = result.violations.filter((v) => v.path === 'dup.cjs').map((v) => v.kind);
|
|
assert.ok(kinds.includes('allowlist_duplicate'));
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: tracked-count floor boundary', () => {
|
|
function fakeTrackedFiles(n) {
|
|
const files = [];
|
|
for (let i = 0; i < n; i += 1) files.push(`scripts/fake-${i}.cjs`);
|
|
return files;
|
|
}
|
|
|
|
test('499 tracked files (limit-1) fails tracked_count_below_floor', async () => {
|
|
const files = fakeTrackedFiles(MIN_TRACKED_SOURCE_FILES - 1);
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: files,
|
|
resolveConfig: coveredResolver(),
|
|
allowlist: [],
|
|
});
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.violations.some((v) => v.kind === 'tracked_count_below_floor'));
|
|
});
|
|
|
|
test('500 tracked files (limit) passes', async () => {
|
|
const files = fakeTrackedFiles(MIN_TRACKED_SOURCE_FILES);
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: files,
|
|
resolveConfig: coveredResolver(),
|
|
allowlist: [],
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.ok(!result.violations.some((v) => v.kind === 'tracked_count_below_floor'));
|
|
});
|
|
|
|
test('501 tracked files (limit+1) passes', async () => {
|
|
const files = fakeTrackedFiles(MIN_TRACKED_SOURCE_FILES + 1);
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: files,
|
|
resolveConfig: coveredResolver(),
|
|
allowlist: [],
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.ok(!result.violations.some((v) => v.kind === 'tracked_count_below_floor'));
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: never a vacuous clean', () => {
|
|
test('an empty git ls-files result never reports clean (fails the floor)', async () => {
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: [],
|
|
resolveConfig: coveredResolver(),
|
|
allowlist: [],
|
|
});
|
|
assert.equal(result.ok, false);
|
|
assert.ok(result.violations.some((v) => v.kind === 'tracked_count_below_floor'));
|
|
});
|
|
|
|
test('listTrackedSourceFiles itself returns ok:true with zero files on empty stdout (not a throw)', () => {
|
|
const result = listTrackedSourceFiles({ execFile: () => '' });
|
|
assert.deepEqual(result, { ok: true, files: [] });
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: container safe.directory (#3059)', () => {
|
|
test('git invocation passes -c safe.directory=* before ls-files, so a container-owned repo ("dubious ownership") never degrades the guard to git_failed', () => {
|
|
let capturedArgs;
|
|
const result = listTrackedSourceFiles({
|
|
execFile: (cmd, args) => {
|
|
capturedArgs = args;
|
|
return 'scripts/a.cjs\n';
|
|
},
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(
|
|
capturedArgs.slice(0, 3),
|
|
['-c', 'safe.directory=*', 'ls-files'],
|
|
'the safe.directory override must precede the ls-files subcommand'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: git failure handling', () => {
|
|
test('an injected git failure reports git_failed without throwing', async () => {
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: { ok: false, files: [], error: 'git ls-files: exit 128' },
|
|
resolveConfig: coveredResolver(),
|
|
allowlist: [],
|
|
});
|
|
assert.equal(result.ok, false);
|
|
assert.deepEqual(
|
|
result.violations.map((v) => v.kind),
|
|
['git_failed']
|
|
);
|
|
});
|
|
|
|
test('listTrackedSourceFiles degrades (does not throw) when execFile throws', () => {
|
|
const boom = () => {
|
|
throw new Error('git: command not found');
|
|
};
|
|
const result = listTrackedSourceFiles({ execFile: boom });
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.files.length, 0);
|
|
assert.match(result.error, /git: command not found/);
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: path normalization', () => {
|
|
test('a backslash path from git ls-files normalizes before allowlist matching', () => {
|
|
const result = listTrackedSourceFiles({
|
|
execFile: () => 'scripts\\weird-windows-path.cjs\nsrc\\normal.cts\n',
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(result.files.sort(), ['scripts/weird-windows-path.cjs', 'src/normal.cts'].sort());
|
|
});
|
|
|
|
test('CRLF git ls-files output parses with no phantom paths', () => {
|
|
const result = listTrackedSourceFiles({
|
|
execFile: () => 'scripts/a.cjs\r\nscripts/b.cjs\r\n',
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(result.files, ['scripts/a.cjs', 'scripts/b.cjs']);
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: listTrackedSourceFiles parser properties (fast-check)', () => {
|
|
// Generators for `git ls-files`-style path-like lines. Segments avoid '.',
|
|
// '\n', '\r' so extension/line boundaries stay unambiguous; the generators
|
|
// below deliberately introduce the shapes the parser must handle: source
|
|
// extensions, non-source extensions, no extension, backslashes, spaces,
|
|
// and empty lines.
|
|
const pathSegment = fc.stringMatching(/^[A-Za-z0-9_\- ]{1,12}$/);
|
|
const sourceExt = fc.constantFrom('cjs', 'cts', 'js', 'mjs');
|
|
const nonSourceExt = fc.constantFrom('md', 'json', 'txt');
|
|
|
|
const sourcePath = fc
|
|
.tuple(fc.array(pathSegment, { minLength: 1, maxLength: 3 }), sourceExt)
|
|
.map(([segs, ext]) => `${segs.join('/')}.${ext}`);
|
|
|
|
const nonSourcePath = fc
|
|
.tuple(fc.array(pathSegment, { minLength: 1, maxLength: 3 }), nonSourceExt)
|
|
.map(([segs, ext]) => `${segs.join('/')}.${ext}`);
|
|
|
|
const noExtPath = fc.array(pathSegment, { minLength: 1, maxLength: 3 }).map((segs) => segs.join('/'));
|
|
|
|
const backslashPath = fc
|
|
.tuple(fc.array(pathSegment, { minLength: 1, maxLength: 3 }), sourceExt)
|
|
.map(([segs, ext]) => `${segs.join('\\')}.${ext}`);
|
|
|
|
const spacedPath = fc
|
|
.tuple(fc.array(pathSegment, { minLength: 1, maxLength: 2 }), sourceExt)
|
|
.map(([segs, ext]) => `${segs.join(' / ')} file.${ext}`);
|
|
|
|
const emptyPath = fc.constant('');
|
|
|
|
const anyPathLine = fc.oneof(sourcePath, nonSourcePath, noExtPath, backslashPath, spacedPath, emptyPath);
|
|
const terminator = fc.constantFrom('\n', '\r\n');
|
|
|
|
// (1) Extension totality / soundness: every returned path ends in a
|
|
// source extension, is traceable back to a generated input line (modulo
|
|
// backslash->slash normalization), and no returned entry is empty. Each
|
|
// generated line carries its OWN terminator (mixing \n and \r\n within a
|
|
// single stdout blob), including the last line — this also exercises "a
|
|
// trailing terminator produces no phantom empty entry" on every run,
|
|
// since every line (including the last) is terminator-suffixed.
|
|
test('property: extension totality — every returned path is source-extensioned, traceable, non-empty', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(fc.tuple(anyPathLine, terminator), { maxLength: 20 }),
|
|
(lines) => {
|
|
const stdout = lines.map(([p, t]) => p + t).join('');
|
|
const result = listTrackedSourceFiles({ execFile: () => stdout });
|
|
assert.equal(result.ok, true);
|
|
|
|
const normalizedInputs = new Set(
|
|
lines.map(([p]) => p.replace(/\\/g, '/')).filter((p) => p.length > 0)
|
|
);
|
|
|
|
for (const file of result.files) {
|
|
assert.notEqual(file, '', 'no returned entry is the empty string');
|
|
assert.match(file, SOURCE_EXT_RE, `${file} must end in a source extension`);
|
|
assert.ok(normalizedInputs.has(file), `${file} must be traceable to a generated input line`);
|
|
}
|
|
|
|
// Exact-equality corollary: the parser's own filter/normalize
|
|
// pipeline applied to the same inputs must reproduce the result.
|
|
const expected = lines
|
|
.map(([p]) => p)
|
|
.filter((p) => p.length > 0)
|
|
.map((p) => p.replace(/\\/g, '/'))
|
|
.filter((p) => SOURCE_EXT_RE.test(p));
|
|
assert.deepEqual(result.files, expected);
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
// (2) Separator normalization is total: any generated path containing a
|
|
// backslash never survives into the output with a backslash intact.
|
|
test('property: separator normalization is total — no returned path contains a backslash', () => {
|
|
fc.assert(
|
|
fc.property(fc.array(backslashPath, { minLength: 0, maxLength: 15 }), (paths) => {
|
|
const stdout = paths.map((p) => `${p}\n`).join('');
|
|
const result = listTrackedSourceFiles({ execFile: () => stdout });
|
|
assert.equal(result.ok, true);
|
|
for (const file of result.files) {
|
|
assert.ok(!file.includes('\\'), `${file} must not contain a backslash`);
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// (3) CRLF/LF equivalence: the same set of path lines, terminated
|
|
// uniformly with LF vs. uniformly with CRLF, must parse to the same
|
|
// result — the invariant the recurring CRLF defect class breaks.
|
|
test('property: CRLF and LF inputs describing the same path set yield the same result', () => {
|
|
fc.assert(
|
|
fc.property(fc.array(anyPathLine, { maxLength: 20 }), (lines) => {
|
|
const lfStdout = lines.map((p) => `${p}\n`).join('');
|
|
const crlfStdout = lines.map((p) => `${p}\r\n`).join('');
|
|
|
|
const lfResult = listTrackedSourceFiles({ execFile: () => lfStdout });
|
|
const crlfResult = listTrackedSourceFiles({ execFile: () => crlfStdout });
|
|
|
|
assert.equal(lfResult.ok, true);
|
|
assert.equal(crlfResult.ok, true);
|
|
assert.deepEqual(lfResult.files, crlfResult.files);
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: SOURCE_EXT_RE filtering', () => {
|
|
test('listTrackedSourceFiles filters to cjs/cts/js/mjs only', () => {
|
|
const result = listTrackedSourceFiles({
|
|
execFile: () =>
|
|
['scripts/a.cjs', 'src/b.cts', 'hooks/c.js', 'msd-core/d.mjs', 'README.md', 'docs/e.txt'].join('\n'),
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(
|
|
result.files.sort(),
|
|
['msd-core/d.mjs', 'hooks/c.js', 'scripts/a.cjs', 'src/b.cts'].sort()
|
|
);
|
|
});
|
|
|
|
test('SOURCE_EXT_RE matches the four source extensions and rejects others', () => {
|
|
assert.equal(SOURCE_EXT_RE.test('foo.cjs'), true);
|
|
assert.equal(SOURCE_EXT_RE.test('foo.cts'), true);
|
|
assert.equal(SOURCE_EXT_RE.test('foo.js'), true);
|
|
assert.equal(SOURCE_EXT_RE.test('foo.mjs'), true);
|
|
assert.equal(SOURCE_EXT_RE.test('foo.ts'), false);
|
|
assert.equal(SOURCE_EXT_RE.test('foo.md'), false);
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: ANCHOR rows (real ESLint against real config)', () => {
|
|
// One shared resolver for the anchor rows: the anchors deliberately re-resolve
|
|
// the config themselves rather than importing the guard's resolveFileCoverage,
|
|
// so an anchor still fails if the guard's own resolution regresses.
|
|
const { ESLint } = require('eslint');
|
|
const anchorEslint = new ESLint({ cwd: ROOT });
|
|
async function rulesFor(relPath) {
|
|
const config = await anchorEslint.calculateConfigForFile(path.join(ROOT, relPath));
|
|
return config && config.rules;
|
|
}
|
|
|
|
test('tests/*.test.cjs has local/no-unbounded-spawn reachable', async () => {
|
|
const rules = await rulesFor('tests/eslint-glob-coverage.test.cjs');
|
|
assert.ok(rules, 'expected a resolved rule set');
|
|
assert.ok(
|
|
Object.prototype.hasOwnProperty.call(rules, 'local/no-unbounded-spawn'),
|
|
'expected local/no-unbounded-spawn to be reachable on a tests/*.test.cjs file'
|
|
);
|
|
});
|
|
|
|
test('scripts/*.cjs has n/no-path-concat reachable', async () => {
|
|
const rules = await rulesFor('scripts/lint-eslint-glob-coverage.cjs');
|
|
assert.ok(rules, 'expected a resolved rule set');
|
|
assert.ok(
|
|
Object.prototype.hasOwnProperty.call(rules, 'n/no-path-concat'),
|
|
'expected n/no-path-concat to be reachable on a scripts/*.cjs file'
|
|
);
|
|
});
|
|
|
|
test('src/*.cts has at least one @typescript-eslint/* rule reachable', async () => {
|
|
const rules = await rulesFor('src/milestone.cts');
|
|
assert.ok(rules, 'expected a resolved rule set');
|
|
const tsRules = Object.keys(rules).filter((r) => r.startsWith('@typescript-eslint/'));
|
|
assert.ok(tsRules.length > 0, 'expected at least one @typescript-eslint/* rule reachable on src/*.cts');
|
|
});
|
|
});
|
|
|
|
describe('eslint-glob-coverage: ignored vs. unmatched discrimination (real ESLint)', () => {
|
|
test('an ADR-457 tsc artifact under msd-core/bin/lib/*.cjs is not reported as uncovered', async () => {
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['msd-core/bin/lib/milestone.cjs'],
|
|
allowlist: [],
|
|
minTrackedFiles: 0,
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(result.escapes, []);
|
|
});
|
|
|
|
test('bin/install.js is not reported as uncovered and is not in the real allowlist', async () => {
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['bin/install.js'],
|
|
allowlist: [],
|
|
minTrackedFiles: 0,
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(result.escapes, []);
|
|
|
|
const realAllowlist = loadAllowlist();
|
|
assert.ok(!realAllowlist.some((entry) => entry.path === 'bin/install.js'));
|
|
});
|
|
});
|