// tests/kb-update/test-detect-courses-invariant.test.mjs // Architecture-invariant guard for the C3.4 detector (spec §2). detect-courses.mjs // is STEG 1 of the two-step pattern: it writes ONLY its own diff-state // (course-registry.json) and its report (course-detection-report.json) — and // must NEVER touch the operator gate's ledger (decisions.json). Unlike // discover-new-urls (which READS the ledger to dedup), the course detector // dedups purely against course-registry, so it imports NEITHER load nor save of // decisions-io. The checks are import/source-specific on purpose: a comment that // merely *describes* the invariant must not be able to satisfy it. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const DETECT = join(__dirname, '..', '..', 'scripts', 'kb-update', 'detect-courses.mjs'); const src = readFileSync(DETECT, 'utf8'); const importLines = src.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n'); test('detector does NOT import the ledger write path (saveDecisions)', () => { assert.doesNotMatch(importLines, /\bsaveDecisions\b/); }); test('detector does NOT import decisions-io at all (never touches the ledger)', () => { assert.doesNotMatch(importLines, /decisions-io/); }); test('detector does NOT import atomic-write', () => { assert.doesNotMatch(importLines, /atomic-write/); }); test('detector does NOT import backup', () => { assert.doesNotMatch(importLines, /from\s+'[^']*\/backup\.mjs'/); }); test('detector DOES write its own diff-state + report (saveCourseRegistry + saveReport)', () => { assert.match(importLines, /\bsaveCourseRegistry\b/); assert.match(importLines, /\bsaveReport\b/); }); test('detector reads creds from the Keychain (readSecret)', () => { assert.match(importLines, /\breadSecret\b/); }); test('detector binds the C3.1–C3.3 seams (diffCourses + makeCourseClassifier)', () => { assert.match(importLines, /\bdiffCourses\b/); assert.match(importLines, /\bmakeCourseClassifier\b/); }); test('detector code invokes neither claude nor anthropic (LLM-free STEG 1)', () => { // Strip comments first so the contract can be documented in prose without // tripping the guard — the guarantee is "no LLM INVOCATION", not "the word // never appears". const code = src.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, ''); assert.doesNotMatch(code, /(execFileSync|spawn|exec|execSync)\(\s*['"`]claude/i); assert.doesNotMatch(code, /anthropic/i); });