// tests/kb-update/test-courses-invariant.test.mjs // Architecture-invariant guard for the C3 course-detection libs (spec §2, §6). // The network/secret/diff layers are Claude-free and write NO files: // - learn-api.mjs is a pure network client (writes nothing, no KB/ledger). // - keychain.mjs reads secrets via the `security` CLI only. // - course-diff.mjs is PURE diff/fold logic (zero imports; no fs, no IO). // As with test-discover-invariant, the checks are import/source-specific: 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 LIB = join(__dirname, '..', '..', 'scripts', 'kb-update', 'lib'); const learnApiSrc = readFileSync(join(LIB, 'learn-api.mjs'), 'utf8'); const keychainSrc = readFileSync(join(LIB, 'keychain.mjs'), 'utf8'); const courseDiffSrc = readFileSync(join(LIB, 'course-diff.mjs'), 'utf8'); const importLines = (src) => src.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n'); test('learn-api is Claude-free (no claude/anthropic references)', () => { assert.doesNotMatch(learnApiSrc, /claude|anthropic/i); }); test('keychain is Claude-free (no claude/anthropic references)', () => { assert.doesNotMatch(keychainSrc, /claude|anthropic/i); }); test('learn-api writes NO files (no fs / atomic-write / save* imports)', () => { const imports = importLines(learnApiSrc); assert.doesNotMatch(imports, /node:fs/); assert.doesNotMatch(imports, /atomic-write/); assert.doesNotMatch(imports, /\bsaveRegistry\b|\bsaveDecisions\b|\bsaveReport\b|\bsaveTaxonomy\b/); }); test('learn-api also does not call fs write functions inline', () => { assert.doesNotMatch(learnApiSrc, /\bwriteFileSync\b|\brenameSync\b|\bmkdirSync\b/); }); test('keychain imports only child_process (reads via the security CLI)', () => { const imports = importLines(keychainSrc); assert.match(imports, /node:child_process/); assert.doesNotMatch(imports, /node:fs|atomic-write|decisions-io|registry-io/); }); test('course-diff is Claude-free (no claude/anthropic references)', () => { assert.doesNotMatch(courseDiffSrc, /claude|anthropic/i); }); test('course-diff is PURE — imports NO write-utils (fs / atomic-write / registry-io / decisions-io / save*)', () => { const imports = importLines(courseDiffSrc); assert.doesNotMatch(imports, /node:fs/); assert.doesNotMatch(imports, /atomic-write|registry-io|decisions-io|backup/); assert.doesNotMatch(imports, /\bsaveRegistry\b|\bsaveCourseRegistry\b|\bsaveDecisions\b|\bsaveReport\b/); }); test('course-diff also does not call fs write functions inline', () => { assert.doesNotMatch(courseDiffSrc, /\bwriteFileSync\b|\brenameSync\b|\bmkdirSync\b/); });