Adds scripts/lint-resolution-provenance.cjs — a registry + ratchet CI guard that locks in the agent-skills configured_empty/not_configured contract tests so they cannot be silently removed, and establishes a registration point for future config-interpreting read verbs (ADR-1411 P4). Design rationale: - REGISTRY (one entry: agent-skills → src/init.cts → tests/agent-skills.test.cjs) is the canonical registration site; new verbs are added here. - For each registered verb, the guard asserts its test file contains BOTH a `configured_empty` assertion AND a `not_configured` assertion — proving the configured-empty-vs-not-configured contract is explicitly tested. - NOT a universal static detector (intractable / false positives) — mirrors the no-adhoc-markdown-parsing grandfather pattern. - Uses scripts/lib/allowlist-ratchet.cjs (assertWithinAllowlist) so stale allowlist entries fail (ratchet-down) and novel offenders always fail. - checkRegistry() is factored as a pure exported function tested in tests/lint-resolution-provenance.test.cjs without shelling out. - Wired into lint:ci (package.json) and lint step name updated in test.yml. - CONTEXT.md ### Resolution Convention extended with P4 guard sentence. - Allowlist starts empty ([]) — agent-skills already has its tests. Closes #1417 Part of #1411 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
176 lines
6.0 KiB
JavaScript
176 lines
6.0 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Tests for scripts/lint-resolution-provenance.cjs — the registry-ratchet CI
|
|
* guard that locks in configured_empty / not_configured contract tests for
|
|
* every registered config-interpreting read verb (ADR-1411 P4 / #1417).
|
|
*
|
|
* Tests the PURE check logic (checkRegistry) directly, injecting fixture
|
|
* content rather than shelling out, so the suite is fast and hermetic.
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
|
|
// Import the exported check function.
|
|
const { checkRegistry } = require('../scripts/lint-resolution-provenance.cjs');
|
|
|
|
/**
|
|
* Run checkRegistry with synthetic content injected as testFileContent so
|
|
* the test never reads from the real repo. Returns { ok, failures }.
|
|
*
|
|
* @param {object} opts
|
|
* @param {Array<{verb: string, sourceFile: string, testFile: string}>} opts.registry
|
|
* @param {string[]} opts.allowlist
|
|
* @param {string} opts.testFileContent - text that represents the test file's content
|
|
*/
|
|
function runCheck({ registry, allowlist, testFileContent }) {
|
|
const failures = [];
|
|
const { ok } = checkRegistry({
|
|
registry,
|
|
allowlist,
|
|
// Inject a content-reader so the test is I/O-free.
|
|
readFile: (_filePath) => testFileContent,
|
|
fail: (msg) => failures.push(msg),
|
|
});
|
|
return { ok, failures };
|
|
}
|
|
|
|
// ── suite ────────────────────────────────────────────────────────────────────
|
|
|
|
describe('lint-resolution-provenance: checkRegistry pure logic', () => {
|
|
const BOTH_MARKERS = `
|
|
test('configured_empty: agent_skills[X]=[] ...', () => {
|
|
assert.strictEqual(ir.reason, 'configured_empty');
|
|
});
|
|
test('not_configured: agent not in map ...', () => {
|
|
assert.strictEqual(ir.reason, 'not_configured');
|
|
});
|
|
`;
|
|
|
|
const MISSING_CONFIGURED_EMPTY = `
|
|
test('not_configured: agent not in map ...', () => {
|
|
assert.strictEqual(ir.reason, 'not_configured');
|
|
});
|
|
`;
|
|
|
|
const MISSING_NOT_CONFIGURED = `
|
|
test('configured_empty: agent_skills[X]=[] ...', () => {
|
|
assert.strictEqual(ir.reason, 'configured_empty');
|
|
});
|
|
`;
|
|
|
|
const sampleVerb = { verb: 'agent-skills', sourceFile: 'src/init.cts', testFile: 'tests/agent-skills.test.cjs' };
|
|
|
|
test('ok: registered verb whose test has both markers passes', () => {
|
|
const { ok, failures } = runCheck({
|
|
registry: [sampleVerb],
|
|
allowlist: [],
|
|
testFileContent: BOTH_MARKERS,
|
|
});
|
|
assert.strictEqual(failures.length, 0, `Unexpected failures: ${failures.join('\n')}`);
|
|
assert.ok(ok);
|
|
});
|
|
|
|
test('fail: missing configured_empty marker → fails with actionable message', () => {
|
|
const { ok, failures } = runCheck({
|
|
registry: [sampleVerb],
|
|
allowlist: [],
|
|
testFileContent: MISSING_CONFIGURED_EMPTY,
|
|
});
|
|
assert.ok(!ok);
|
|
assert.ok(failures.length > 0, 'Expected at least one failure');
|
|
assert.match(failures.join('\n'), /configured_empty/);
|
|
assert.match(failures.join('\n'), /agent-skills/);
|
|
});
|
|
|
|
test('fail: missing not_configured marker → fails with actionable message', () => {
|
|
const { ok, failures } = runCheck({
|
|
registry: [sampleVerb],
|
|
allowlist: [],
|
|
testFileContent: MISSING_NOT_CONFIGURED,
|
|
});
|
|
assert.ok(!ok);
|
|
assert.ok(failures.length > 0, 'Expected at least one failure');
|
|
assert.match(failures.join('\n'), /not_configured/);
|
|
assert.match(failures.join('\n'), /agent-skills/);
|
|
});
|
|
|
|
test('fail: missing both markers → fails mentioning both', () => {
|
|
const { ok, failures } = runCheck({
|
|
registry: [sampleVerb],
|
|
allowlist: [],
|
|
testFileContent: '// no relevant markers here',
|
|
});
|
|
assert.ok(!ok);
|
|
const msg = failures.join('\n');
|
|
assert.match(msg, /configured_empty/);
|
|
assert.match(msg, /not_configured/);
|
|
});
|
|
|
|
test('tolerated: allowlisted verb with missing markers is skipped', () => {
|
|
const { ok, failures } = runCheck({
|
|
registry: [sampleVerb],
|
|
allowlist: ['agent-skills'],
|
|
testFileContent: MISSING_CONFIGURED_EMPTY,
|
|
});
|
|
assert.strictEqual(failures.length, 0, `Unexpected failures: ${failures.join('\n')}`);
|
|
assert.ok(ok);
|
|
});
|
|
|
|
test('fail: stale allowlist entry (verb now compliant) must be pruned', () => {
|
|
const { ok, failures } = runCheck({
|
|
registry: [sampleVerb],
|
|
allowlist: ['agent-skills'],
|
|
testFileContent: BOTH_MARKERS,
|
|
});
|
|
assert.ok(!ok);
|
|
assert.match(failures.join('\n'), /agent-skills/);
|
|
assert.match(failures.join('\n'), /stale|prune|no longer/i);
|
|
});
|
|
|
|
test('ok: empty registry always passes', () => {
|
|
const { ok, failures } = runCheck({
|
|
registry: [],
|
|
allowlist: [],
|
|
testFileContent: '',
|
|
});
|
|
assert.strictEqual(failures.length, 0);
|
|
assert.ok(ok);
|
|
});
|
|
|
|
test('ok: multiple verbs all compliant passes', () => {
|
|
const verb2 = { verb: 'config-read', sourceFile: 'src/config.cts', testFile: 'tests/config.test.cjs' };
|
|
const { ok, failures } = runCheck({
|
|
registry: [sampleVerb, verb2],
|
|
allowlist: [],
|
|
testFileContent: BOTH_MARKERS,
|
|
});
|
|
assert.strictEqual(failures.length, 0, `Unexpected failures: ${failures.join('\n')}`);
|
|
assert.ok(ok);
|
|
});
|
|
});
|
|
|
|
describe('lint-resolution-provenance: real repo baseline', () => {
|
|
test('repo baseline passes (real registry vs real agent-skills.test.cjs)', () => {
|
|
// Run the actual check against the real registry and real test file.
|
|
// This is the regression lock: if agent-skills.test.cjs loses its markers
|
|
// the guard catches it here too.
|
|
const { checkRegistry: check, REGISTRY } = require('../scripts/lint-resolution-provenance.cjs');
|
|
const failures = [];
|
|
const { ok } = check({
|
|
registry: REGISTRY,
|
|
allowlist: [],
|
|
readFile: (filePath) => fs.readFileSync(filePath, 'utf8'),
|
|
fail: (msg) => failures.push(msg),
|
|
});
|
|
assert.strictEqual(
|
|
failures.length,
|
|
0,
|
|
`Real repo baseline failed:\n${failures.join('\n')}`
|
|
);
|
|
assert.ok(ok);
|
|
});
|
|
});
|