ms-ai-architect/tests/kb-update/test-verify-out.test.mjs
Kjell Tore Guttormsen 6af3624b17 feat(ms-ai-architect): Sesjon 4 - verifisering-ut (lag 5) + adversarial regresjonstest
Lag 5 kjører ETTER transformasjon (lag 4) og FØR en kandidat-endring skrives til
en KB-fil. Den fanger regresjons-klassen: en status-påstand «korrigert» mot en
tilfeldig sitert side og stille auto-applyet (den kjente agentic-retrieval-
regresjonen — hovedkontekst måtte rette manuelt).

Regel (spec §21): status-påstander (GA/preview/versjon/pris) flagges ALLTID for
operatør, aldri auto-applyet — uansett hvor sikker evidensen ser ut.

- lib/verify-out.mjs: ren klassifiserer, null deps (speiler decisions-io).
  detectStatusClaim(text) → {isStatus, kinds}; classifyChange(change) →
  {verdict: flagged|auto-applied, status_claim, reasons}. Tre flag-regler:
  status-gate (§21) · adversarial refutering · autoritets-mismatch (regresjonens
  rotårsak). Konservativ med vilje: ved tvil flagges. SKRIVER ALDRI.
- Fixtur tests/fixtures/kb-update/agentic-retrieval-regression.json: den kjente
  regresjonen (flat «GA» mot nyansert «delvis GA … resten preview»).
- TDD: 13 tester før kode, inkl. import-invariant (ingen write-utils).
- Wiret i kb-update.md §4 c2 (lag 5 mellom identifiser-endring og skriv).

Kriterium møtt: fixtur → flagged:true, ikke auto-applied.
Tester: validate 239 · kb-update 95 (+13) · kb-eval 13 · kb-integrity 115/115.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REiKFhP4w6xGXXqWKpPCJJ
2026-06-19 22:00:36 +02:00

153 lines
6.4 KiB
JavaScript

// tests/kb-update/test-verify-out.test.mjs
// Unit tests for scripts/kb-update/lib/verify-out.mjs — lag 5 (verifisering-UT):
// the adversarial "try-to-refute" + status-gate that runs AFTER transformation
// (lag 4) and BEFORE a candidate change is written to a KB file.
//
// Kjernekriterium (roadmap §48): den KJENTE agentic-retrieval-regresjonen som
// fixtur → flagged:true, IKKE auto-applied. Lag 5 skal aldri auto-applye en
// status-paastand (GA/preview/versjon/pris) — den flagges ALLTID for operatoer
// (spec §21). verify-out er en REN klassifiserer: den beregner en verdict og
// skriver aldri (invariant nederst).
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';
import { detectStatusClaim, classifyChange } from '../../scripts/kb-update/lib/verify-out.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const FIXTURE = JSON.parse(
readFileSync(join(__dirname, '..', 'fixtures', 'kb-update', 'agentic-retrieval-regression.json'), 'utf8'),
);
// --- detectStatusClaim — release stage / version / price tokens ---
test('detectStatusClaim — recognizes GA', () => {
const r = detectStatusClaim('Tjenesten er nå generally available (GA).');
assert.equal(r.isStatus, true);
assert.ok(r.kinds.includes('release-stage'));
});
test('detectStatusClaim — recognizes public preview', () => {
assert.equal(detectStatusClaim('Available in public preview').isStatus, true);
});
test('detectStatusClaim — recognizes a preview API-version date (2026-05-01-preview)', () => {
const r = detectStatusClaim('Krever REST 2026-05-01-preview for multi-turn.');
assert.equal(r.isStatus, true);
assert.ok(r.kinds.includes('version'));
});
test('detectStatusClaim — recognizes deprecation / retirement', () => {
assert.equal(detectStatusClaim('This API is deprecated and will be retired.').isStatus, true);
});
test('detectStatusClaim — recognizes price / SLA', () => {
assert.equal(detectStatusClaim('Priset til $30 per million tokens.').isStatus, true);
assert.equal(detectStatusClaim('99.9% uptime SLA').isStatus, true);
});
test('detectStatusClaim — benign prose is NOT a status claim', () => {
const r = detectStatusClaim(
'RAG kombinerer henting med generering for å gi kontekst-forankrede svar.',
);
assert.equal(r.isStatus, false);
assert.deepEqual(r.kinds, []);
});
test('detectStatusClaim — a bare calendar date alone is NOT a status claim', () => {
// Unngaa aa flagge enhver datert setning; bare -preview-versjoner / release-ord teller.
assert.equal(detectStatusClaim('Sist verifisert 2026-06-19 av teamet.').isStatus, false);
});
// --- classifyChange — ACCEPTANCE CRITERION (roadmap §48) ---
test('agentic-retrieval regression fixture → flagged, NOT auto-applied', () => {
const v = classifyChange(FIXTURE.change);
assert.equal(v.verdict, 'flagged', 'the known regression must be flagged for the operator');
assert.notEqual(v.verdict, 'auto-applied');
assert.equal(v.status_claim, true, 'GA/preview transition is a status claim');
assert.ok(v.reasons.length >= 1, 'a flag must carry at least one human-readable reason');
assert.ok(
v.reasons.some((r) => /status/i.test(r)),
'the status-gate reason must be present',
);
});
// --- classifyChange — benign change auto-applies ---
test('benign non-status change with matching authority → auto-applied', () => {
const v = classifyChange({
field: 'RAG-introduksjon — formulering',
old_value: 'RAG henter dokumenter og genererer svar.',
new_value: 'RAG henter relevante dokumenter og genererer kontekst-forankrede svar.',
source_url: 'https://learn.microsoft.com/azure/search/retrieval-augmented-generation-overview',
authority_source: 'https://learn.microsoft.com/azure/search/retrieval-augmented-generation-overview',
refutations: [],
});
assert.equal(v.verdict, 'auto-applied');
assert.equal(v.status_claim, false);
assert.deepEqual(v.reasons, []);
});
test('benign change with no authority info (null) still auto-applies', () => {
const v = classifyChange({
field: 'beskrivelse',
old_value: 'Et verktoey for henting.',
new_value: 'Et verktoey for henting og rangering.',
source_url: 'https://learn.microsoft.com/azure/search/x',
authority_source: null,
refutations: [],
});
assert.equal(v.verdict, 'auto-applied');
});
// --- classifyChange — adversarial refutation flags ---
test('any refutation with refuted:true → flagged (adversarial panel found counter-evidence)', () => {
const v = classifyChange({
field: 'beskrivelse',
old_value: 'Stoetter X.',
new_value: 'Stoetter X og Y.',
source_url: 'https://learn.microsoft.com/azure/search/x',
authority_source: 'https://learn.microsoft.com/azure/search/x',
refutations: [
{ refuted: false, reason: 'kunne ikke motbevise X' },
{ refuted: true, reason: 'autoritetskilden nevner ikke Y' },
],
});
assert.equal(v.verdict, 'flagged');
assert.ok(v.reasons.some((r) => /refut|motbevis/i.test(r)));
});
// --- classifyChange — authority mismatch flags (regression root cause) ---
test('source_url differs from designated authority_source → flagged', () => {
const v = classifyChange({
field: 'beskrivelse uten status-ord',
old_value: 'Stoetter henting.',
new_value: 'Stoetter henting og caching.',
source_url: 'https://learn.microsoft.com/azure/search/whats-new',
authority_source: 'https://learn.microsoft.com/azure/search/search-agentic-retrieval-how-to-retrieve',
refutations: [],
});
assert.equal(v.verdict, 'flagged');
assert.ok(v.reasons.some((r) => /authority|autoritet/i.test(r)));
});
// --- INVARIANT (spec §83 / STATE.md): verify-out is a pure classifier; it
// NEVER imports a write path. It computes a verdict — applying the change is the
// gate's job, not lag 5's. Import-specific (a comment can't create a false hit).
test('verify-out imports NO write-utils (pure classifier)', () => {
const src = readFileSync(
join(__dirname, '..', '..', 'scripts', 'kb-update', 'lib', 'verify-out.mjs'),
'utf8',
);
const importLines = src.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n');
assert.doesNotMatch(importLines, /\bsaveDecisions\b/);
assert.doesNotMatch(importLines, /\bsaveRegistry\b/);
assert.doesNotMatch(importLines, /from\s+'[^']*atomic-write[^']*'/);
assert.doesNotMatch(importLines, /from\s+'[^']*\/backup\.mjs'/);
});