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
143 lines
6.2 KiB
JavaScript
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)');
|
|
});
|
|
});
|