ms-ai-architect/tests/kb-update/test-courses-invariant.test.mjs
Kjell Tore Guttormsen 0390cc10ca feat(ms-ai-architect): C3.1 — keychain + Learn Platform API-klient (TDD) [skip-docs]
C3 kurs-deteksjon, fase C3.1 (spec docs/c3-course-detection-plan.md §6/§7).
Claude-frie, fil-frie infrastruktur-lag for kurs-detektoren (C3.4):

- lib/keychain.mjs: readSecret() leser én Keychain-item via `security`-CLI,
  fail-soft (manglende item / ikke-macOS → null). execImpl-DI for test.
- lib/learn-api.mjs: getToken (Entra client-credentials), paginate (async-gen,
  følger nextLink), buildApiUrl (pinner api-version), buildUpdatedAtGt (kaster
  på dato-only, canon Z — gotcha #1). Robusthetskontrakt §6: response.ok-sjekk
  (fetch kaster ikke på 4xx/5xx), AbortSignal.timeout, 429/5xx-retry som
  respekterer Retry-After, 4xx≠429 ikke-retry, fail-closed. fetchImpl/sleep-DI.

Tester (24, alle grønne): test-learn-api (token-body, buildUpdatedAtGt,
nextLink-paginering, produkt-param, alle robusthets-asserts), test-keychain,
test-courses-invariant (Claude-fri + skriver-ingen-filer). Live-probe (efemer)
grønn mot ekte Platform API m/ ekte creds: token + full-enum + inkrementell.

Intern infrastruktur (ingen brukervendt kommando/hook/atferd endret ennå —
surfacing + docs lander i C3.6). kb-update 213→237 · validate 239 · null regresjon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:59:16 +02:00

45 lines
2 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 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.
// 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 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/);
});