119 lines
4.5 KiB
JavaScript
119 lines
4.5 KiB
JavaScript
// reference-integrity.test.mjs
|
|
// B1 (1.7.1): referanse-integritet for alle ${CLAUDE_PLUGIN_ROOT}-stier i
|
|
// commands/ og agents/. Fanger doed-referanse-klassen permanent (review 4:
|
|
// freshen-references pekte paa gitignored .claude/-sti som aldri shippes).
|
|
// To invarianter per referert sti:
|
|
// 1. Stien maa finnes paa disk (relativt til plugin-rot).
|
|
// 2. Stien maa vaere shippbar: aldri under .claude/ (gitignored, finnes
|
|
// lokalt men ikke i installert plugin — ren existsSync er falsk groenn).
|
|
// Glob-stier (* i siste segment) sjekkes som: katalog finnes + minst ett treff.
|
|
// Zero npm deps. Moenster: tests/package-shape.test.mjs (les fil, assert).
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const SCAN_DIRS = ['commands', 'agents'];
|
|
|
|
// Stopper paa whitespace, backtick, anfoerselstegn, ), ] — tegnene som
|
|
// avslutter en sti i markdown-prosa/lenker. Ingen legitime stier her
|
|
// inneholder dem.
|
|
const PATH_RE = /\$\{CLAUDE_PLUGIN_ROOT\}\/([^\s`"')\]]+)/g;
|
|
|
|
function collectReferences() {
|
|
const refs = [];
|
|
for (const dir of SCAN_DIRS) {
|
|
for (const name of readdirSync(join(ROOT, dir))) {
|
|
if (!name.endsWith('.md')) continue;
|
|
const file = join(dir, name);
|
|
const body = readFileSync(join(ROOT, file), 'utf8');
|
|
for (const m of body.matchAll(PATH_RE)) {
|
|
refs.push({ file, rel: m[1] });
|
|
}
|
|
}
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
function assertResolvable(rel) {
|
|
const starName = rel.lastIndexOf('*');
|
|
if (starName === -1) {
|
|
assert.ok(existsSync(join(ROOT, rel)), `finnes ikke: ${rel}`);
|
|
return;
|
|
}
|
|
// Glob: kun *-i-siste-segment brukes i repoet (f.eks. references/*.md).
|
|
const dir = dirname(rel);
|
|
assert.ok(!dir.includes('*'), `glob i katalogsegment stoettes ikke: ${rel}`);
|
|
assert.ok(existsSync(join(ROOT, dir)), `glob-katalog finnes ikke: ${dir}`);
|
|
const suffix = rel.slice(starName + 1);
|
|
const hits = readdirSync(join(ROOT, dir)).filter((n) => n.endsWith(suffix));
|
|
assert.ok(hits.length > 0, `glob uten treff: ${rel}`);
|
|
}
|
|
|
|
test('commands/agents refererer minst en plugin-rot-sti (regex-sanity)', () => {
|
|
const refs = collectReferences();
|
|
assert.ok(refs.length >= 10, `fant bare ${refs.length} referanser — regex broken?`);
|
|
});
|
|
|
|
test('alle ${CLAUDE_PLUGIN_ROOT}-stier er shippbare (aldri under .claude/)', () => {
|
|
const offenders = collectReferences().filter(({ rel }) =>
|
|
rel === '.claude' || rel.startsWith('.claude/'));
|
|
assert.deepEqual(
|
|
offenders.map((o) => `${o.file} -> ${o.rel}`),
|
|
[],
|
|
'gitignored .claude/-stier shippes aldri med pluginen'
|
|
);
|
|
});
|
|
|
|
test('alle ${CLAUDE_PLUGIN_ROOT}-stier finnes paa disk', () => {
|
|
const missing = [];
|
|
for (const { file, rel } of collectReferences()) {
|
|
try {
|
|
assertResolvable(rel);
|
|
} catch (e) {
|
|
missing.push(`${file} -> ${rel} (${e.message})`);
|
|
}
|
|
}
|
|
assert.deepEqual(missing, [], 'doede referanser funnet');
|
|
});
|
|
|
|
// 1.8.0 (En kanon): kanon-konsolideringen legger til NYE krysslenker mellom
|
|
// referansefiler (Steps 3/4/6/10/11). Denne casen fanger doede *relative*
|
|
// .md-lenker i skills/-treet - komplementaert til ${CLAUDE_PLUGIN_ROOT}-
|
|
// invariantene over. Kilder: SKILL.md (backtick `references/x.md` / `x.md`)
|
|
// + sibling-krysslenker (`x.md`) inni references/. Alle forventes aa peke
|
|
// paa en fil i references/ paa disk.
|
|
const REF_REL = join('skills', 'okr-offentlig-sektor', 'references');
|
|
const REL_MD_RE = /`(?:references\/)?([a-z0-9-]+\.md)`/g;
|
|
|
|
function collectSkillRelativeRefs() {
|
|
const refs = [];
|
|
const sources = [join('skills', 'okr-offentlig-sektor', 'SKILL.md')];
|
|
for (const name of readdirSync(join(ROOT, REF_REL))) {
|
|
if (name.endsWith('.md')) sources.push(join(REF_REL, name));
|
|
}
|
|
for (const file of sources) {
|
|
const body = readFileSync(join(ROOT, file), 'utf8');
|
|
for (const m of body.matchAll(REL_MD_RE)) {
|
|
refs.push({ file, rel: join(REF_REL, m[1]) });
|
|
}
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
test('SKILL.md + referansefil-krysslenker resolverer paa disk (relative .md)', () => {
|
|
const refs = collectSkillRelativeRefs();
|
|
assert.ok(refs.length >= 15, `fant bare ${refs.length} relative refs (regex broken?)`);
|
|
const missing = [];
|
|
for (const { file, rel } of refs) {
|
|
try {
|
|
assertResolvable(rel);
|
|
} catch (e) {
|
|
missing.push(`${file} -> ${rel} (${e.message})`);
|
|
}
|
|
}
|
|
assert.deepEqual(missing, [], 'doede relative referanser i skills/-treet');
|
|
});
|