// innboks-write.test.mjs // Step 7 (SC original + riktig nivaa): konsept-skriver med type->nivaa-ruting og // ikke-destruktiv original-bevaring. Driver den reelle kjernen // (splitConcepts -> projectFrontmatter -> resolveRelations) per original og lar // writeConcepts persistere settet i en temp-bundle. Verifiserer: // - konsept med strategisk-kontekst/-destRel skrives dit, dokumenter/-destRel dit // - skrevet fil = frontmatter (verbatim) + body (verbatim), trailing newline // - originalens sha256 er uendret etter skriv (ikke-destruktiv, SC3) // - peker-fil i konseptets nivaa lenker til originalen i drop-zonen (grep-bar) // - mal-sti utenfor bundle-rot avvist (../-escape) og home-org-rot avvist // - ingen .tmp lekker (atomisk temp+renameSync) // Zero npm deps. Moenster: tests/org-profile-write.test.mjs (mkdtemp + realpath/sha). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync } from 'node:fs'; import { tmpdir, homedir } from 'node:os'; import { join, dirname } from 'node:path'; import { splitConcepts } from '../lib/innboks-split.mjs'; import { projectFrontmatter } from '../lib/innboks-frontmatter.mjs'; import { resolveRelations } from '../lib/innboks-relations.mjs'; import { writeConcepts } from '../lib/innboks-write.mjs'; const MTIME = new Date('2026-03-15T08:30:00.000Z'); function sha256(path) { return createHash('sha256').update(readFileSync(path)).digest('hex'); } // Kjor den reelle kjernen for ett dokument -> konsepter (m/ destRel + frontmatter + body). function ingestOne(markdown, sourceSlug, sourcePath) { const raw = splitConcepts(markdown, { sourceSlug }); const projected = raw.map((c) => projectFrontmatter(c, { sourcePath, sourceMtime: MTIME })); return resolveRelations(projected); } // Temp-bundle: /.claude/okr (+ innboks/ drop-zone). Ryddes alltid. function withBundle(fn) { const tmp = mkdtempSync(join(tmpdir(), 'okrwrite-')); const bundleRoot = join(tmp, '.claude', 'okr'); const inbox = join(bundleRoot, 'innboks'); mkdirSync(inbox, { recursive: true }); try { fn({ tmp, bundleRoot, inbox }); } finally { rmSync(tmp, { recursive: true, force: true }); } } const TILDELING = '# Tildelingsbrev 2026\n\nStatens vegvesen skal levere paa foelgende maal i 2026.\n'; const MERKNAD = '# Generell merknad\n\nEt fritt notat uten foeringsord i tittel.\n'; test('writeConcepts: type->nivaa-ruting (strategisk-kontekst + dokumenter)', () => { withBundle(({ bundleRoot, inbox }) => { const tPath = join(inbox, 'tildeling.txt'); const mPath = join(inbox, 'merknad.txt'); writeFileSync(tPath, TILDELING); writeFileSync(mPath, MERKNAD); const tConcepts = ingestOne(TILDELING, 'tildeling', 'innboks/tildeling.txt'); const mConcepts = ingestOne(MERKNAD, 'merknad', 'innboks/merknad.txt'); const [tc] = tConcepts; const [mc] = mConcepts; // Forventning fra Step 5-ruting (sanity foer skriv). assert.equal(dirname(tc.destRel), 'strategisk-kontekst', 'Tildelingsbrev -> strategisk-kontekst/'); assert.equal(dirname(mc.destRel), 'dokumenter', 'default Dokument -> dokumenter/'); writeConcepts([...tConcepts, ...mConcepts], { bundleRoot, originals: [ { sourceSlug: 'tildeling', path: tPath }, { sourceSlug: 'merknad', path: mPath }, ], }); const tFile = join(bundleRoot, tc.destRel); const mFile = join(bundleRoot, mc.destRel); assert.ok(existsSync(tFile), 'tildelingsbrev-konsept skrevet til strategisk-kontekst/'); assert.ok(existsSync(mFile), 'merknad-konsept skrevet til dokumenter/'); // Skrevet fil = frontmatter (verbatim) + body (verbatim), trailing newline. IKKE re-serialisert. const tContent = readFileSync(tFile, 'utf8'); assert.ok(tContent.startsWith(tc.frontmatter), 'frontmatter verbatim oeverst'); assert.ok(tContent.includes(tc.body), 'body verbatim bevart'); assert.ok(tContent.endsWith('\n'), 'fil avsluttes med newline'); }); }); test('writeConcepts: original uendret (sha256) + peker-fil lenker til drop-zone-original', () => { withBundle(({ bundleRoot, inbox }) => { const tPath = join(inbox, 'tildeling.txt'); writeFileSync(tPath, TILDELING); const before = sha256(tPath); const concepts = ingestOne(TILDELING, 'tildeling', 'innboks/tildeling.txt'); writeConcepts(concepts, { bundleRoot, originals: [{ sourceSlug: 'tildeling', path: tPath }], }); // SC3: ikke-destruktiv -- originalen er byte-uendret. assert.equal(sha256(tPath), before, 'original-sha256 uendret etter skriv'); assert.ok(existsSync(tPath), 'original finnes fortsatt i drop-zonen'); // Peker-fil i konseptets nivaa, grep-bar lenke til originalen. const level = dirname(concepts[0].destRel); const pointer = join(bundleRoot, level, 'tildeling.kilde.md'); assert.ok(existsSync(pointer), 'peker-fil skrevet i konseptets nivaa'); const pointerBody = readFileSync(pointer, 'utf8'); assert.match(pointerBody, /\(\/innboks\/tildeling\.txt\)/, 'peker lenker til original i drop-zonen'); assert.match(pointerBody, /^type: Notat$/m, 'peker baerer gyldig OKF-type (vokab)'); }); }); test('writeConcepts: ../-escape destRel avvist (path-confinement)', () => { withBundle(({ bundleRoot }) => { const evil = { slug: 'evil', sourceSlug: 'evil', destRel: '../escape.md', frontmatter: '---\ntype: Dokument\n---\n', body: 'x' }; assert.throws( () => writeConcepts([evil], { bundleRoot, originals: [] }), /utenfor bundle-rot/, 'destRel som escaper bundle-rota skal kaste', ); assert.ok(!existsSync(join(bundleRoot, '..', 'escape.md')), 'ingenting skrevet utenfor bundle'); }); }); test('writeConcepts: skriv til home-org-rot avvist (ingestion roerer aldri ~/.claude/okr/org)', () => { const homeOrg = join(homedir(), '.claude', 'okr', 'org'); const concept = { slug: 'p', sourceSlug: 'p', destRel: 'dokumenter/p.md', frontmatter: '---\ntype: Dokument\n---\n', body: 'x' }; // Kaster FOER noen skriv -- home-profilen roeres aldri. assert.throws( () => writeConcepts([concept], { bundleRoot: homeOrg, originals: [] }), /home-org-rot avvist/, 'home-org bundleRoot skal kaste', ); }); test('writeConcepts: atomisk -- ingen .tmp lekker etter skriv', () => { withBundle(({ bundleRoot, inbox }) => { const tPath = join(inbox, 'tildeling.txt'); writeFileSync(tPath, TILDELING); const concepts = ingestOne(TILDELING, 'tildeling', 'innboks/tildeling.txt'); writeConcepts(concepts, { bundleRoot, originals: [{ sourceSlug: 'tildeling', path: tPath }] }); const level = dirname(concepts[0].destRel); const leftover = readdirSync(join(bundleRoot, level)).filter((f) => f.endsWith('.tmp')); assert.deepEqual(leftover, [], 'ingen temp-fil igjen etter atomisk renameSync'); }); });