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
This commit is contained in:
parent
9701c81158
commit
5219d50d4b
4 changed files with 233 additions and 9 deletions
143
tests/oppsett-okf-write.test.mjs
Normal file
143
tests/oppsett-okf-write.test.mjs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// 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)');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue