Pure diff/fold + run-mode selector for course detection (C3.2 av GODKJENT C3-spec). - lib/course-diff.mjs (REN, null imports): normalizeCourse, selectMode, isFullEnumDue, diffCourses, updateRegistry, FULL_ENUM_MAX_AGE_DAYS=30. Full-enum-pin (§4.2): removed beregnes KUN i full-modus; inkrementell og baseline gir removed:[]. Baseline emitterer ingen leads (§8 b). updateRegistry ren (muterer aldri): incremental merger, baseline/full bygger fra API-sett + dropper retirerte UIDs, stamper last_full_enum. - registry-io.mjs +loadCourseRegistry/saveCourseRegistry (additivt, speiler loadRegistry/saveRegistry; fil course-registry.json). - test-course-diff.test.mjs (23) + test-courses-invariant (+3: diff-modul ren). Gate §7 C3.2 møtt. kb-update 237->263, validate PASSED, null regresjon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.9 KiB
JavaScript
62 lines
2.9 KiB
JavaScript
// 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/);
|
|
});
|