// oppsett-okf-write.test.mjs // Step 7 (SC6): onboarding skriver OKF-frontmatter paa org-profilen. // Profilen er NESTET (organisasjon: { navn, type }), saa flat writeFrontmatter // er feil verktoey. compose-org-profile.mjs bygger den fulle nestede YAML-en og // PREPENDER tre top-level OKF-noekler (type/resource/timestamp) FOER organisasjon:. // Pipes til write-org-profile.mjs (uendret) som skriver atomisk til hjem. // // Kritisk invariant: kun type/resource/timestamp top-level (leses ikke av hooks). // En NY top-level navn/id/fase/domene/sektor ville matches FOERST av den flate // parseren og skygge den nestede verdien -> brutt org-/syklus-resolusjon. // Zero npm deps. Moenster: tests/org-profile-write.test.mjs. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseFrontmatter } from '../lib/frontmatter.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const COMPOSE = join(ROOT, 'scripts', 'compose-org-profile.mjs'); const WRITER = join(ROOT, 'scripts', 'write-org-profile.mjs'); const HOOK = join(ROOT, 'hooks', 'scripts', 'inject-okr-context.mjs'); // Nestet profil-body slik /okr:oppsett produserer den (organisasjon: + program:). // Bevisst UTEN '#'-kommentarer her -- comment-strip dekkes av frontmatter-testen. const NESTED_BODY = [ 'onboarding_status: fullfort', 'organisasjon:', ' navn: "Testdirektoratet"', ' kortform: "TDIR"', ' type: "offentlig"', ' domene: "digitalisering"', 'program:', ' modenhetsnivaa: "pilot"', ' okr_frikoblet_fra_loenn: true', '', ].join('\n'); const NOW = '2026-06-26T10:00:00+00:00'; function compose(input, env = {}) { return execFileSync('node', [COMPOSE], { env: { ...process.env, ...env }, input, encoding: 'utf8', }); } function write(cwd, home, input) { return execFileSync('node', [WRITER], { cwd, env: { ...process.env, HOME: home }, input, encoding: 'utf8', }); } function runHook(cwd, home) { return execFileSync('node', [HOOK], { cwd, env: { ...process.env, HOME: home }, encoding: 'utf8', }); } function withDirs(fn) { const home = mkdtempSync(join(tmpdir(), 'okrhome-')); const work = mkdtempSync(join(tmpdir(), 'okrwork-')); try { fn(home, work); } finally { rmSync(home, { recursive: true, force: true }); rmSync(work, { recursive: true, force: true }); } } test('compose: prepender top-level type/resource/timestamp, bevarer nestet organisasjon/navn', () => { const composed = compose(NESTED_BODY, { OKR_NOW: NOW }); // Top-level OKF-noekler (linjestart): assert.match(composed, /^type: Organisasjonsprofil$/m, 'top-level type'); assert.match(composed, /^resource: /m, 'top-level resource'); assert.match(composed, /^timestamp: '2026-06-26T10:00:00\+00:00'$/m, 'top-level timestamp (ISO, sitert)'); // Nestet struktur bevart: assert.match(composed, /^organisasjon:$/m, 'nestet organisasjon: bevart'); assert.match(composed, /^ {2}navn: "Testdirektoratet"$/m, 'nestet navn bevart'); // ETT samlet frontmatter-blokk: parseFrontmatter ser BAADE OKF-noekler OG nestet navn. const { get } = parseFrontmatter(composed); assert.equal(get('navn'), 'Testdirektoratet', 'navn resolver (ingen intern --- som trunkerer blokken)'); assert.equal(get('type'), 'Organisasjonsprofil', 'type resolver til OKF-verdi'); // FORBID-regel (Critical risk): ingen NY top-level noekkel som skygger nestet verdi. assert.doesNotMatch(composed, /^navn:/m, 'ingen top-level navn (ville skygge nestet org-navn)'); assert.doesNotMatch(composed, /^id:/m, 'ingen top-level id (ville skygge nestet cycle-id)'); assert.doesNotMatch(composed, /^fase:/m, 'ingen top-level fase'); assert.doesNotMatch(composed, /^domene:/m, 'ingen top-level domene'); assert.doesNotMatch(composed, /^sektor:/m, 'ingen top-level sektor'); }); test('compose: tolererer body som allerede baerer --- fences (samler til ett blokk)', () => { const fenced = `---\n${NESTED_BODY}---\n`; const composed = compose(fenced, { OKR_NOW: NOW }); const { get } = parseFrontmatter(composed); assert.equal(get('navn'), 'Testdirektoratet', 'navn resolver (ingen dobbel-fence trunkerer blokken)'); assert.equal(get('type'), 'Organisasjonsprofil'); const fences = composed.match(/^---$/gm) || []; assert.equal(fences.length, 2, 'noeyaktig to frontmatter-fences'); }); test('compose -> write-org-profile: hjem-profil faar OKF-frontmatter + bevart nestet navn', () => { withDirs((home, work) => { const composed = compose(NESTED_BODY, { OKR_NOW: NOW }); const out = write(work, home, composed); const target = join(home, '.claude', 'okr', 'org', 'profil.md'); assert.ok(existsSync(target), 'hjem-profil skal finnes'); const written = readFileSync(target, 'utf8'); assert.match(written, /^type: Organisasjonsprofil$/m, 'skrevet profil har top-level type'); assert.match(written, /^resource: /m, 'skrevet profil har resource'); assert.match(written, /^timestamp: /m, 'skrevet profil har timestamp'); assert.match(written, /navn: "Testdirektoratet"/, 'skrevet profil bevarer nestet navn'); assert.equal(written, composed, 'write-org-profile er byte-passthrough (skriver stdin uendret)'); assert.ok(out.trim().length > 0, 'helper rapporterer brukt sti paa stdout'); }); }); test('inject-roundtrip: get(navn) resolver org-navn (ikke "Organisasjonsprofil"), ingen #, cap < 512 B', () => { withDirs((home, work) => { const composed = compose(NESTED_BODY, { OKR_NOW: NOW }); write(work, home, composed); // Tomt prosjekt-cwd -> hooken faller til hjem-profil (mest-spesifikk-vinner). const injected = runHook(work, home); const { systemMessage } = JSON.parse(injected); assert.match(systemMessage, /Testdirektoratet/, 'org-navn injiseres'); assert.doesNotMatch(systemMessage, /Organisasjonsprofil/, 'top-level type skal IKKE skygge nestet navn'); assert.ok(!systemMessage.includes('#'), 'ingen # i payload'); assert.ok(Buffer.byteLength(systemMessage, 'utf8') < 512, 'kjerne-payload < 512 B (SC5)'); }); });