// tests/kb-update/test-ai-act-deadlines-sync.test.mjs // TDD for RX-REG (review 2026-07-09, B2/B3/B4/B5 + R3-5/M10): the EU AI Act // deadlines get ONE machine-readable source — scripts/kb-update/data/ai-act-deadlines.json // — and every consumer is sync-tested against it: // - CLAUDE.md «Viktige frister»-tabellen (date + label + status verbatim) // - agents/ai-act-assessor.md frist-malen (identical DATE SET; labels are // template-compact by design — R3-5: the 2026-12-02 row was missing) // - hooks/scripts/{session-start-context,stop-assessment-reminder}.mjs must READ // the source instead of carrying hardcoded date literals (B5: the stop hook // labelled 2026-08-02 «GPAI-frist» — wrong article; GPAI was 2025-08-02) // Refuted-claim guard: the final Omnibus text (EP 2026-06-16, Council 2026-06-29) // dropped the acceleration trigger, so «kan fremskynde»/«ytre grense» must not // appear; Nkom is the designated coordinating authority, so «under etablering» // must be gone (source: nkom.no/ki/regulering/hvem-handhever). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { loadAiActDeadlines, DATA_URL } from '../../scripts/kb-update/lib/ai-act-deadlines.mjs'; const pluginRoot = new URL('../../', import.meta.url); const read = (rel) => readFileSync(new URL(rel, pluginRoot), 'utf8'); const source = loadAiActDeadlines(); // --- 1. The source itself --- test('deadline source loads with omnibus adoption-status + deadline entries', () => { assert.ok(source, 'ai-act-deadlines.json must exist and parse'); assert.ok(source.omnibus, 'source must carry an omnibus adoption block'); assert.equal(typeof source.omnibus.status, 'string', 'omnibus.status (vedtaks-status) required'); assert.ok(Array.isArray(source.deadlines) && source.deadlines.length >= 6, 'at least the 6 known deadlines'); for (const dl of source.deadlines) { assert.match(dl.date, /^\d{4}-\d{2}-\d{2}$/, `ISO date, got: ${dl.date}`); assert.ok(dl.label && dl.status, `label + status required for ${dl.date}`); } const dates = source.deadlines.map((d) => d.date); assert.deepEqual(dates, [...dates].sort(), 'deadlines sorted ascending'); assert.equal(new Set(dates).size, dates.length, 'no duplicate dates'); }); test('source carries no refuted claims and no stale authority phrasing', () => { const raw = readFileSync(DATA_URL, 'utf8'); assert.ok(!/fremskynde|ytre grense|under etablering/i.test(raw), 'refuted Omnibus-acceleration claim / stale Nkom phrasing must not re-enter the source'); }); test('status wording never claims «Gjeldende» for a future date (rot-guard)', () => { const today = new Date().toISOString().slice(0, 10); for (const dl of source.deadlines) { if (/^Gjeldende/i.test(dl.status)) { assert.ok(dl.date <= today, `${dl.date} marked «${dl.status}» before it applies`); } } }); // --- 2. Markdown consumers --- // Rows whose FIRST cell is an ISO date; other tables (#-keyed) never match. function parseDeadlineTable(md) { const rows = []; for (const line of md.split('\n')) { const m = line.match(/^\|\s*(\d{4}-\d{2}-\d{2})\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*$/); if (m) rows.push({ date: m[1], label: m[2], status: m[3] }); } return rows; } test('CLAUDE.md frist-tabell matches the source verbatim (date + label + status)', () => { const rows = parseDeadlineTable(read('CLAUDE.md')); assert.deepEqual( rows.map((r) => [r.date, r.label, r.status]), source.deadlines.map((d) => [d.date, d.label, d.status]), ); }); test('CLAUDE.md drops refuted claims and names Nkom', () => { const md = read('CLAUDE.md'); assert.ok(!/fremskynde|ytre grense|under etablering/.test(md)); assert.ok(md.includes('Nkom'), 'M10: Nkom (koordinerende tilsynsmyndighet) must be named'); }); test('ai-act-assessor frist-mal covers exactly the source dates (R3-5: 2026-12-02)', () => { const rows = parseDeadlineTable(read('agents/ai-act-assessor.md')); assert.deepEqual( rows.map((r) => r.date).sort(), source.deadlines.map((d) => d.date).sort(), ); }); test('ai-act-assessor names Nkom and drops «under etablering»', () => { const md = read('agents/ai-act-assessor.md'); assert.ok(md.includes('Nkom')); assert.ok(!/under etablering/.test(md)); }); // --- 3. Hook consumers read the source, no drift-prone literals --- for (const hook of [ 'hooks/scripts/session-start-context.mjs', 'hooks/scripts/stop-assessment-reminder.mjs', ]) { test(`${hook} reads deadlines from the shared source`, () => { const src = read(hook); assert.ok(src.includes('ai-act-deadlines'), 'must import the shared loader'); assert.ok(!/new Date\('20\d{2}-\d{2}-\d{2}'\)/.test(src), 'no hardcoded deadline literals'); assert.ok(!src.includes('GPAI'), 'B5: no GPAI-labelled deadline left in hook code'); }); }