okr/tests/oppsett-okf-write.test.mjs
Kjell Tore Guttormsen 5219d50d4b feat(okr): onboarding skriver OKF-frontmatter paa org-profil (SC6)
compose-org-profile.mjs prepender top-level OKF-noekler (type:
Organisasjonsprofil/resource/timestamp) over den nestede org-profilen i
ETT frontmatter-blokk, pipes til write-org-profile.mjs (uendret). Kun de
tre trygge noeklene top-level; FORBID nye top-level navn/id/fase/domene/
sektor som ville skygge nestede verdier hooken leser. oppsett.md (begge
profil-skriv-sites) + template oppdatert. Test: compose->pip->readback +
inject-roundtrip (4), full suite 51->55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 17:19:14 +02:00

143 lines
6.2 KiB
JavaScript

// 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)');
});
});