// innboks-frontmatter.test.mjs // Step 5 (SC idempotens): deterministisk frontmatter-projeksjon. projectFrontmatter // utleder type/tags regel-basert (INGEN LLM) og snapper mot lukket vokab; resource = // original relativ sti; description = foerste ikke-tomme avsnitt; timestamp = ISO av // original-mtime (IKKE veggklokke); kilde:innboks; og fester concept.destRel = // routeLevel(type)/slug.md FOER relasjons-steget. Asserterer paa parseFrontmatter(). // get() (aldri substring for verdier). Moenster: tests/oppsett-okf-write.test.mjs:93. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseFrontmatter } from '../lib/frontmatter.mjs'; import { TYPE_VOCAB, routeLevel } from '../lib/okf-vocab.mjs'; import { splitConcepts } from '../lib/innboks-split.mjs'; import { projectFrontmatter } from '../lib/innboks-frontmatter.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const FIX = join(HERE, 'fixtures', 'inbox-converted'); const dokA = readFileSync(join(FIX, 'dok-a.md'), 'utf8'); const MTIME = new Date('2026-03-15T08:30:00.000Z'); const OPTS = { sourcePath: 'innboks/dok-a.txt', sourceMtime: MTIME }; // Frontmatter-noekler i rekkefolge (kun linjestart-noekler; list-elementer hoppes over). function fmKeyOrder(fm) { const { raw } = parseFrontmatter(fm); return (raw || '') .split('\n') .map((l) => { const m = l.match(/^([a-z_]+):/); return m ? m[1] : null; }) .filter(Boolean); } test('projectFrontmatter: type snappes til lukket vokab; keyword i title -> kanonisk type', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e = projectFrontmatter(c1, OPTS); const { get } = parseFrontmatter(e.frontmatter); assert.ok(TYPE_VOCAB.includes(get('type')), 'type i lukket vokab'); assert.equal(get('type'), 'Tildelingsbrev', 'keyword "Tildelingsbrev" i title -> kanonisk type'); }); test('projectFrontmatter: ukjent emne -> safe default Dokument (negativ vokab-sjekk)', () => { const concept = { sourceSlug: 'x', title: 'Helt annerledes emne', slug: 'helt-annerledes-emne', level: 1, body: 'Innhold uten vokab-noekkelord.', }; const e = projectFrontmatter(concept, { sourcePath: 'innboks/x.txt', sourceMtime: MTIME }); assert.equal(parseFrontmatter(e.frontmatter).get('type'), 'Dokument'); assert.doesNotMatch( e.frontmatter, /^type: (Tildelingsbrev|Virksomhetsplan|OKR)$/m, 'ikke feil-snappet til en foeringstype', ); }); test('projectFrontmatter: resource = original relativ sti (kanonisk navn, ikke "source")', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e = projectFrontmatter(c1, OPTS); const { get } = parseFrontmatter(e.frontmatter); assert.equal(get('resource'), 'innboks/dok-a.txt'); assert.equal(get('source'), null, 'noekkelen heter resource, ikke source'); }); test('projectFrontmatter: description deterministisk (2 kjoeringer byte-identiske, foerste avsnitt)', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e1 = projectFrontmatter(c1, OPTS); const e2 = projectFrontmatter(c1, OPTS); assert.equal(e1.frontmatter, e2.frontmatter, 'byte-identisk projeksjon'); assert.match(parseFrontmatter(e1.frontmatter).get('description'), /Tildelingsbrevet gir overordnede/); }); test('projectFrontmatter: lang body -> description trunkeres deterministisk', () => { const longBody = 'A'.repeat(500); const concept = { sourceSlug: 'lang', title: 'Lang', slug: 'lang', level: 1, body: longBody }; const e = projectFrontmatter(concept, { sourcePath: 'innboks/lang.txt', sourceMtime: MTIME }); const desc = parseFrontmatter(e.frontmatter).get('description'); assert.ok(desc.length <= 244, 'trunkert til <= 240 + ellipsis'); assert.match(desc, /\.\.\.$/, 'ellipsis-markoer ved trunkering'); }); test('projectFrontmatter: timestamp = ISO av sourceMtime (idempotent paa tvers av veggklokke)', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e = projectFrontmatter(c1, OPTS); assert.equal(parseFrontmatter(e.frontmatter).get('timestamp'), MTIME.toISOString()); }); test('projectFrontmatter: kilde:innboks provenans-markoer alltid satt', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e = projectFrontmatter(c1, OPTS); assert.equal(parseFrontmatter(e.frontmatter).get('kilde'), 'innboks'); assert.equal(e.kilde, 'innboks', 'kilde ogsaa paa konsept-objektet'); }); test('projectFrontmatter: concept.destRel satt + konsistent med routeLevel(type)', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e = projectFrontmatter(c1, OPTS); assert.ok(e.destRel, 'destRel satt FOER relasjons-steget'); assert.equal(e.destRel, `${routeLevel(e.type)}/${c1.slug}.md`); assert.equal(e.destRel, 'strategisk-kontekst/tildelingsbrev-2026.md', 'Tildelingsbrev -> strategisk-kontekst'); }); test('projectFrontmatter: kanonisk noekkel-rekkefolge (type/resource/title/desc/tags/timestamp/kilde)', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e = projectFrontmatter(c1, OPTS); const order = fmKeyOrder(e.frontmatter); const canon = ['type', 'resource', 'title', 'description', 'tags', 'timestamp', 'kilde']; assert.deepEqual(order, canon.filter((k) => order.includes(k)), 'present keys i kanonisk rekkefolge'); assert.equal(order[0], 'type', 'type foerst'); assert.equal(order[order.length - 1], 'kilde', 'kilde sist'); }); test('projectFrontmatter: tags er multi-linje liste (writeFrontmatter array-gren) + i vokab', () => { const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' }); const e = projectFrontmatter(c1, OPTS); assert.match(e.frontmatter, /^tags:$/m, 'multi-linje tags-noekkel'); assert.match(e.frontmatter, /^ {2}- Tildelingsbrev$/m, 'tag-element i vokab'); });