voyage/tests/lib/living-paths.test.mjs
Kjell Tore Guttormsen 8295601965
chore(cleanup): remove the June trials, orchestrator docs, dormant agent, caller-less code and the fix-round chain
R4-R7 of the cleanup list (decision 188): the June trial docs and
fixtures, the three orchestrator reference docs, the dormant
synthesis-agent, code with no caller (project-discovery, bash-normalize,
fidelity-diff, test-census, the next-session prompt validator, arm B and
the --workflow flag) and the fix-round chain. Living docs repointed; the
reasons for in-session orchestration and the all-opus pin are now written
in docs/architecture.md and docs/profiles.md.

New tests/lib/living-paths.test.mjs: living tracked text names only
plugin paths that exist (seen red with 22 dead pointers before this
commit). docs/T1-cc26-delegated-orchestration.md and
lib/review/gold-scorer.mjs stay until R-b: the frozen end-state registry
names them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:51:54 +01:00

174 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/lib/living-paths.test.mjs
// Living tracked text names only plugin paths that exist. When a file is
// removed, every command, agent, skill, template and doc that still names it
// turns this test red, so a removal cannot leave a dead pointer behind.
//
// Scope: tracked *.md under agents/ commands/ skills/ templates/ docs/ and the
// root README.md, CLAUDE.md, CONTRIBUTING.md and SECURITY.md. Out of scope:
// CHANGELOG.md (history), *.local.* files (never tracked), and tests/,
// examples/ and evals/, whose text describes target repositories.
//
// A reference is (a) a whole backtick span that starts with a top-level plugin
// directory, with an optional ${CLAUDE_PLUGIN_ROOT}/ prefix and an optional
// :line suffix; (b) a ${CLAUDE_PLUGIN_ROOT}/<path> token anywhere; (c) a
// relative markdown link target. Spans with placeholder characters and spans
// on a credit line (an @<sha> or MIT on the line or the two lines above) are
// skipped: they name files in other repositories.
//
// When this test fails, fix the pointer or remove it; add an EXEMPT entry only
// for a path that is meant to be absent here, with the reason.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { join, dirname, normalize } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const TOP = '(?:agents|commands|docs|examples|hooks|lib|scripts|skills|templates|tests|evals)';
const PATH = `${TOP}/[A-Za-z0-9._/-]*[A-Za-z0-9_/-]`;
const LINES = '(?::\\d+(?:[-–]\\d+)?(?:, ?\\d+(?:[-–]\\d+)?)*)?';
const SPAN_RE = new RegExp(`^(?:\\$\\{CLAUDE_PLUGIN_ROOT\\}/)?(${PATH})${LINES}$`);
const ROOT_TOKEN_RE = new RegExp(`\\$\\{CLAUDE_PLUGIN_ROOT\\}/(${PATH})`, 'g');
const CREDIT_RE = /@[0-9a-f]{7,40}\b|\bMIT\b/;
const PLACEHOLDER_RE = /[<>{}*$…]|\.\.\./;
const SCOPE_DIRS = ['agents/', 'commands/', 'skills/', 'templates/', 'docs/'];
const SCOPE_ROOT = new Set(['README.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'SECURITY.md']);
// Living tracked text in scope (see the header).
export function inScope(rel) {
if (!rel.endsWith('.md') || /\.local\./.test(rel)) return false;
if (rel === 'CHANGELOG.md') return false;
return SCOPE_ROOT.has(rel) || SCOPE_DIRS.some(d => rel.startsWith(d));
}
// Every plugin path a text names, as { line, path } with path relative to the
// repository root. `rel` is the text's own path, for relative links.
export function extractPaths(text, rel) {
const out = [];
const lines = text.split('\n');
lines.forEach((line, i) => {
const context = lines.slice(Math.max(0, i - 2), i + 1).join('\n');
if (CREDIT_RE.test(context)) return;
const seen = new Set();
const add = p => { if (!seen.has(p)) { seen.add(p); out.push({ line: i + 1, path: p }); } };
for (const m of line.matchAll(/`([^`\n]+)`/g)) {
const span = m[1].trim();
const hit = SPAN_RE.exec(span);
if (hit && !PLACEHOLDER_RE.test(hit[1])) add(hit[1]);
}
for (const m of line.matchAll(ROOT_TOKEN_RE)) {
if (!PLACEHOLDER_RE.test(m[1])) add(m[1]);
}
for (const m of line.matchAll(/\]\(([^)\s]+)\)/g)) {
const target = m[1].split('#')[0];
if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('/')) continue;
if (PLACEHOLDER_RE.test(target)) continue;
const resolved = normalize(join(dirname(rel), target)).replace(/\\/g, '/');
if (resolved.startsWith('..')) continue;
add(resolved.replace(/\/$/, ''));
}
});
return out;
}
// "file:line path" for every named path that does not exist under root.
export function deadPaths(root, files) {
const dead = [];
for (const rel of files) {
const text = readFileSync(join(root, rel), 'utf-8');
for (const { line, path } of extractPaths(text, rel)) {
if (!existsSync(join(root, path))) dead.push(`${rel}:${line} ${path}`);
}
}
return dead;
}
function trackedFiles(root) {
const out = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8' });
return out.split('\0').filter(Boolean);
}
// Named exceptions: "file path" -> reason. Each must still be dead (a stale
// exemption fails below).
const TARGET = 'target-repo example';
const RB = 'removed in R-b';
const EXEMPT = new Map([
['commands/trekplan.md tests/e2e.sh', TARGET],
['docs/HANDOVER-CONTRACTS.md docs/specs/api.md', TARGET],
['templates/trekreview-template.md lib/foo.mjs', TARGET],
['docs/release-runbook.md scripts/release-plugin.mjs', "the catalog repo's script"],
['docs/T1-cc26-delegated-orchestration.md scripts/q3-cache-prefix-experiment.mjs', RB],
['docs/T1-cc26-delegated-orchestration.md docs/cc-upgrade-2.1.181-decision-matrix.md', RB],
['docs/T1-cc26-delegated-orchestration.md docs/subagent-delegation-audit.md', RB],
['docs/T1-cc26-delegated-orchestration.md agents/planning-orchestrator.md', RB],
['docs/T1-cc26-delegated-orchestration.md docs/T1-synthesis-poc-results.md', RB],
['docs/T1-cc26-delegated-orchestration.md agents/synthesis-agent.md', RB],
['docs/deep-research-engine-research.md docs/cc-upgrade-2.1.181-decision-matrix.md', RB],
]);
const exemptKey = hit => hit.replace(/:\d+ /, ' ');
test('extractPaths reads spans, plugin-root tokens and relative links (known-positive)', () => {
const text = [
'See `lib/util/present.mjs:12-14` and `lib/util/missing.mjs`.',
'Run node ${CLAUDE_PLUGIN_ROOT}/scripts/gone.mjs --json.',
'Read [the doc](../docs/away.md#part) or [site](https://example.com).',
'Placeholder `tests/<name>.sh` and glob `tests/**/*.test.mjs` are skipped.',
'Credit: someone/repo @c55ee46, `skills/other/SKILL.md`, MIT.',
].join('\n');
const got = extractPaths(text, 'commands/x.md').map(h => `${h.line} ${h.path}`);
assert.deepEqual(got, [
'1 lib/util/present.mjs',
'1 lib/util/missing.mjs',
'2 scripts/gone.mjs',
'3 docs/away.md',
]);
});
test('deadPaths reports only the missing paths (known-positive)', () => {
const dir = mkdtempSync(join(tmpdir(), 'living-paths-'));
try {
mkdirSync(join(dir, 'lib'), { recursive: true });
mkdirSync(join(dir, 'docs'), { recursive: true });
writeFileSync(join(dir, 'lib', 'present.mjs'), '\n');
writeFileSync(join(dir, 'docs', 'a.md'), 'Uses `lib/present.mjs` and `lib/absent.mjs`.\n');
assert.deepEqual(deadPaths(dir, ['docs/a.md']), ['docs/a.md:1 lib/absent.mjs']);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('scope keeps living docs and drops history and target-repo text', () => {
assert.ok(inScope('commands/trekplan.md'));
assert.ok(inScope('docs/architecture.md'));
assert.ok(inScope('README.md'));
assert.ok(!inScope('CHANGELOG.md'));
assert.ok(!inScope('docs/notes.local.md'));
assert.ok(!inScope('tests/fixtures/x.md'));
assert.ok(!inScope('examples/02-real-cli/brief.md'));
assert.ok(!inScope('lib/util/frontmatter.mjs'));
});
test('living tracked text names only paths that exist', () => {
const files = trackedFiles(ROOT);
assert.ok(files.length > 100, `git ls-files returned ${files.length} files; refusing to pass on an empty scan`);
const hits = deadPaths(ROOT, files.filter(inScope)).filter(h => !EXEMPT.has(exemptKey(h)));
assert.deepEqual(hits, [], `${hits.length} dead path reference(s):\n${hits.join('\n')}`);
});
test('every EXEMPT entry is still a dead reference in a tracked file', () => {
const tracked = new Set(trackedFiles(ROOT));
for (const key of EXEMPT.keys()) {
const [rel, path] = key.split(' ');
assert.ok(tracked.has(rel), `EXEMPT names untracked ${rel}`);
const dead = deadPaths(ROOT, [rel]).map(exemptKey);
assert.ok(dead.includes(key), `EXEMPT entry "${key}" is no longer dead; remove it`);
assert.ok(!existsSync(join(ROOT, path)), `EXEMPT path ${path} exists`);
}
});