ms-ai-architect/tests/kb-update/test-detect-courses-invariant.test.mjs
Kjell Tore Guttormsen 2e69e69326 feat(ms-ai-architect): C3.4 — detect-courses.mjs (binder C3.1–C3.3, LLM-fri) + selectDetectionSteps + include_course_detection (TDD) [skip-docs]
LLM-fri STEG 1-detektor for kurs-deteksjon (C3.4 av GODKJENT C3-spec). Speiler
discover-new-urls' to-stegs-monster: leser Keychain-creds, paginerer modules +
learning-paths filtrert til in-domain produkter, differ mot egen diff-state
(course-registry.json), skriver kandidatrapport. Skriver KUN report + registry —
ALDRI decisions.json (operator-gaten i C3.5 promoterer leads).

- scripts/kb-update/detect-courses.mjs (NY): detectCourses(deps) — injiserbar
  kjerne (readSecret/getToken/paginate/loadTaxonomy/load+saveCourseRegistry/
  saveReport/dataDir/now/log) -> hermetisk testbar uten Keychain/nett. Fail-soft
  (spec 3/6): creds=null -> status:"skipped"; run-feil -> status:"error" (kaster
  ALDRI); begge exit 0 (advisory, aldri rod pipeline). Tynn CLI med realpath-guard
  (kjorer kun nar invokert direkte) + --data-dir test-seam. updatedAt.gt-cursor
  KUN pa incremental (full ISO, gotcha #1); products-filter = comma-join (OR).
- lib/detection-schedule.mjs: ny ren selectDetectionSteps(config) gater BADE
  skillLifecycle + courseDetection (en sannhetskilde). DEFAULT_SCHEDULE_CONFIG
  +include_course_detection:false (opt-in i opt-in, spec 8d). coerce/serialize
  utvidet. DETECTION_STEPS +{detect-courses, courseDetection:true}.
- run-detection.mjs: bruker selectDetectionSteps (fjerner duplisert filter-linje).

Tester (+22):
- test-detect-courses-invariant (8): ingen saveDecisions/decisions-io/atomic-write/
  backup-import; skriver egen registry+report; binder diffCourses+makeCourseClassifier;
  LLM-fri (kommentar-strippet kildesjekk).
- test-detect-courses (6): fail-soft skipped (alle/delvis creds), error-path (no
  throw, registry urort), 4.3 report-shape + avledet skill/category, incremental
  cursor/produkt-param, baseline=ingen leads (spec 8b), hermetisk exit-0-subprosess
  via PATH-shadow av `security`.
- test-detection-schedule (+8): include_course_detection parse/serialize/default;
  selectDetectionSteps-gating (default av, opt-in pa, uavhengige gates);
  run-detection bruker selektoren.

Gotcha (secrets-hook): clientSecret: '...'-form traff llm-security-regelen
(client[_-]?secret|ClientSecret)\s*[=:]\s*['"]…{8,} -> lost med array-av-par
(feltnavn star aldri rett for quotet streng).

Gate 7 C3.4 mott: invariant + fail-soft(exit 0) + report-shape + DETECTION_STEPS-
filter gronn. kb-update 269->291, validate PASSED (239/0), run-detection dry-run
dropper detect-courses (opt-in av), eval urort (rorer ikke KB), null regresjon.

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

59 lines
2.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.1C3.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);
});