* chore(#3059): close the ESLint glob-coverage escape and guard it 62 tracked source files matched no `files:` glob in eslint.config.mjs, so ESLint skipped them entirely while `eslint .` still exited 0 — including all 26 files under hooks/, the enforcement machinery itself. Covers 56 of them (eslint-rules/, hooks/, bin/lib/, pi/, examples/, vscode/, the plugin shims, root *.mjs) and allowlists the 6 deliberate must-not-compile brand-typing fixtures with a recorded reason each. hooks/** is covered with n/no-process-exit deliberately off: a hook's whole contract is its exit code, several exits are load-bearing stdin-timeout guards where nothing else terminates the process, and ADR-0012/0174 scope the no-process-exit convention to the Command Routing Hub. bin/lib/ui-safety-gate.cjs is dual-mode, so it keeps the rule live and takes two targeted disables in its require.main===module tail instead. Adds scripts/lint-eslint-glob-coverage.cjs + a node:test drift guard so the class cannot regrow: allowlist entries require a non-empty reason, the list ratchets down only (a stale entry fails), and a tracked-count floor means a broken `git ls-files` fails rather than reporting clean. Closes #3059 * chore(#3059): apply review findings — correct the changeset count, add parser properties Isolated adversarial review, confirmed by rebuilding a byte-for-byte replica of the pre-change eslint.config.mjs: the changeset claimed 44 previously- unlinted files. The real figure is 56 (56 covered + 6 allowlisted = 62). That was user-facing CHANGELOG text and was wrong; corrected, along with three consequential figures in the design record. CLAUDE.md requires a fast-check property test for parsers and budget limits, and listTrackedSourceFiles is a parser. The standards review called this "satisfied in spirit"; it is not. Adds three properties driving the real exported parser through an injected execFile: extension totality/soundness including a trailing terminator, backslash-normalization totality, and CRLF/LF equivalence — the invariant the repo's recurring CRLF defect class breaks. Also de-duplicates the anchor rows onto one shared resolver, kept deliberately independent of the guard's own resolveFileCoverage so an anchor still fails if that resolution regresses, and records in the guard's header why the bin/install.js family is NOT allowlisted: it resolves to 2 rules under ADR-1703, so an entry would trip the allowlist_stale ratchet. * fix(#3059): make the coverage guard's git call container-safe The remote runner reported the guard degrading to `git_failed` on both Node lanes: fatal: detected dubious ownership in repository at '/work' The runner executes in a container where the repo is owned by a different UID, so git refuses to operate on it. The guard's degraded-verdict path worked exactly as designed — it reported the failure instead of throwing or falsely reporting clean — but a guard that cannot run in CI is not a gate. `git ls-files` is now invoked as `git -c safe.directory=* ls-files`. `-c` scopes the override to the single invocation and mutates no config file, and the wildcard is appropriate because this command only enumerates tracked paths in the repository it is already executing inside. Adds a regression test that captures the argv through the injected execFile seam and asserts `-c safe.directory=*` precedes `ls-files`, so the container case is pinned behaviorally rather than by reading the script's source. * chore(#3059): backfill changeset PR number pr:0 placeholder replaced with the real number now that #3277 exists. --------- Co-authored-by: sim <sim@local>
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', 'gsd-core/d.mjs', 'README.md', 'docs/e.txt'].join('\n'),
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(
|
|
result.files.sort(),
|
|
['gsd-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 gsd-core/bin/lib/*.cjs is not reported as uncovered', async () => {
|
|
const result = await checkGlobCoverage({
|
|
trackedFiles: ['gsd-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'));
|
|
});
|
|
});
|