okr/tests/frontmatter.test.mjs
Kjell Tore Guttormsen 9a68441161 fix(okr): frontmatter-verdi krysser ikke linjeskift (patch-lane #5)
`get()`-regexens verdi-gren brukte `\s*`, som inkluderer linjeskift. Enhver
key med tom rest-av-linje slukte dermed den neste ikke-tomme linja. Utslaget
var bredere enn list-keys: en tom `title:` returnerte neste keys hele linje
("type: OKR"), og et tomt `description:` dempet okf-checks anbefalt-felt-
advarsel med data som tilhorte en annen key.

Verdi-grenen strammet til `[ \t]*`. Innrykk-ankeret (`^\s*`) er urort -- det
er load-bearing for inject-okr-context.mjs:69s nestede organisasjon:-lesing.
Kontrakt-kommentaren i :13-16 lovet allerede null for list-keys; den er naa
sann i stedet for aa bli rettet ned.

Sju RED-verifiserte tester: (L1) list-key gir null - (L2) tom skalar sluker
ikke neste rot-key - (L3) tom key foran list-blokk - (L4) innrykk-toleransen
bevart, forelder gir null - (L5) tab-separert verdi - (L6) tomt anbefalt felt
demper ikke okf-check-advarselen (rootLevelGet arvet defekten) - (L7)
broedtekst-fallbacken plukker ikke list-verdi fra frontmatter.

(L7) gjor S57s latente begrunnelse maalbar: uttrekket i okf-check.mjs:125 ble
beholdt mot nettopp denne defekten, og naar fm-laget endelig taper for
list-keys er `body = raw` foerste gang en roednende mutasjon.

Mutasjons-verifisert (M1 revert, M3 anker-fjerning, M4 uttrekk-fjerning). M2
viste at (L5) ikke vokter tegnklassen -- .trim() gjor `[ \t]*` og `[ ]*`
ekvivalente -- saa testen er omskrevet til aa paastaa det den faktisk viser.

Suite 321 -> 328.
2026-08-09 14:43:15 +02:00

187 lines
8.3 KiB
JavaScript

// frontmatter.test.mjs
// Tester den delte frontmatter-modulen (SC2): parseFrontmatter/get reproduserer
// hookenes flate first-match-parser MEN (1) strip trailing " #..." KUN fra
// USITERTE verdier, (2) anker key-match til linjestart (ingen midt-i-linje
// falske treff), og (3) tolererer fler-linje OKF-list-verdier (`tags:`) uten
// krasj. writeFrontmatter round-tripper og siterer `#`. Importerer modulen
// direkte (zero npm deps). Moenster: tests/coaching-hook.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseFrontmatter, writeFrontmatter } from '../lib/frontmatter.mjs';
test('basic: sitert skalar leses', () => {
const { get } = parseFrontmatter('---\nnavn: "Org"\n---\n');
assert.equal(get('navn'), 'Org');
});
test('basic: usitert skalar leses', () => {
const { get } = parseFrontmatter('---\nnavn: Org\n---\n');
assert.equal(get('navn'), 'Org');
});
test('usitert verdi: trailing "# kommentar" strippes (comment-leak-fix)', () => {
const { get } = parseFrontmatter(
'---\nokr_frikoblet_fra_loenn: true # frikoblet fra lonn\n---\n',
);
assert.equal(get('okr_frikoblet_fra_loenn'), 'true', 'usitert: # strippes');
});
test('sitert verdi: intern "#" bevares (space-hash i quotes)', () => {
const { get } = parseFrontmatter('---\nkortform: "A #B"\n---\n');
assert.equal(get('kortform'), 'A #B', 'sitert: # er en del av verdien, ikke kommentar');
});
test('sitert verdi med kommentar UTENFOR quotes: kommentar ignoreres', () => {
const { get } = parseFrontmatter('---\nkortform: "A #B" # en note\n---\n');
assert.equal(get('kortform'), 'A #B');
});
test('first-match: forste forekomst vinner', () => {
const { get } = parseFrontmatter('---\nnavn: "Forste"\nnavn: "Andre"\n---\n');
assert.equal(get('navn'), 'Forste');
});
test('nestet: innrykket key resolver (load-bearing inject:69)', () => {
const { get } = parseFrontmatter(
'---\norganisasjon:\n navn: "NestetOrg"\n type: "offentlig"\n---\n',
);
assert.equal(get('navn'), 'NestetOrg', 'innrykket navn under organisasjon: skal resolve');
});
test('linjeanker: key matcher IKKE som substring av annen key', () => {
const { get } = parseFrontmatter('---\netternavn: "Hansen"\n---\n');
assert.equal(get('navn'), null, 'navn skal ikke matche etternavn: (anker linjestart)');
});
test('OKF type: Title-Case enkeltlinje', () => {
const { get } = parseFrontmatter('---\ntype: Organisasjonsprofil\n---\n');
assert.equal(get('type'), 'Organisasjonsprofil');
});
test('OKF resource: URL med ":" bevares', () => {
const { get } = parseFrontmatter('---\nresource: https://example.com/path\n---\n');
assert.equal(get('resource'), 'https://example.com/path');
});
test('OKF timestamp: sitert ISO-8601 med ":" og "+" bevares', () => {
const { get } = parseFrontmatter("---\ntimestamp: '2026-05-28T22:49:59+00:00'\n---\n");
assert.equal(get('timestamp'), '2026-05-28T22:49:59+00:00');
});
test('OKF fler-linje tags-liste: krasjer ikke + folgende skalar resolver', () => {
const content =
'---\n' +
'type: BigQuery Dataset\n' +
'tags:\n' +
'- ecommerce\n' +
'- web analytics\n' +
"timestamp: '2026-05-28T22:49:59+00:00'\n" +
'---\n# body\n';
const { get } = parseFrontmatter(content);
assert.doesNotThrow(() => get('tags'), 'get paa list-key skal ikke krasje');
assert.equal(get('type'), 'BigQuery Dataset');
assert.equal(
get('timestamp'),
'2026-05-28T22:49:59+00:00',
'skalar etter list-blokk skal fortsatt resolve',
);
});
test('BOM + CRLF: frontmatter parses (falsk "mangler type"-fiks, B2)', () => {
// Windows-produsert fil: UTF-8 BOM foran forste fence + CRLF-linjeskift.
const content = '\uFEFF---\r\ntype: Notat\r\ntitle: "X"\r\n---\r\n# body\r\n';
const { raw, get } = parseFrontmatter(content);
assert.notEqual(raw, null, 'BOM/CRLF skal ikke gi raw null');
assert.equal(get('type'), 'Notat');
assert.equal(get('title'), 'X');
});
test('ingen frontmatter: raw null, get returnerer null', () => {
const { raw, get } = parseFrontmatter('ingen frontmatter her\n');
assert.equal(raw, null);
assert.equal(get('navn'), null);
});
test('writeFrontmatter: round-trip via parseFrontmatter', () => {
const block = writeFrontmatter({ navn: 'Org', type: 'Organisasjonsprofil' });
const { get } = parseFrontmatter(block);
assert.equal(get('navn'), 'Org');
assert.equal(get('type'), 'Organisasjonsprofil');
});
test('writeFrontmatter: verdi med "#" siteres og round-tripper', () => {
const block = writeFrontmatter({ kortform: 'A #B' });
assert.match(block, /kortform: "A #B"/, 'verdi med # skal siteres');
const { get } = parseFrontmatter(block);
assert.equal(get('kortform'), 'A #B', 'sitert # round-tripper');
});
test('writeFrontmatter: array-verdi (tags) -> OKF multi-linje liste (Step 3)', () => {
const block = writeFrontmatter({ type: 'OKR', tags: ['a', 'b'], kilde: 'innboks' });
assert.match(block, /tags:\n - a\n - b/, 'array -> innrykket OKF-liste');
const { get } = parseFrontmatter(block);
assert.equal(get('type'), 'OKR', 'skalar FOR list-blokk resolver');
assert.equal(get('kilde'), 'innboks', 'skalar ETTER innrykket list-blokk resolver fortsatt');
// get() paa en list-key returnerer null -- tolerance-kontrakten i
// frontmatter.mjs:14-16 er sann etter at verdi-grenen ble strammet fra \s*
// til [ \t]* (se (L1)). Fram til da returnerte den foerste list-ELEMENT.
assert.equal(get('tags'), null, 'get paa list-key gir null');
});
// --- Linjeskift-aksen (L1-L5) ------------------------------------------------
// `:\s*(.*)$` lot \s* krysse linjeskift, saa ENHVER key med tom rest-av-linje
// slukte den neste ikke-tomme linja. Utslaget er ikke begrenset til list-keys:
// en tom `title:` returnerte neste keys HELE linje ("type: OKR"). Fikset ved aa
// stramme KUN den andre \s* til [ \t]*; den foerste (`^\s*`, innrykk-toleranse)
// er load-bearing for inject-okr-context.mjs:69 og staar urort -- (L4) vokter det.
test('(L1) list-key: get() returnerer null (kontrakten :14-16 blir sann)', () => {
const { get } = parseFrontmatter(
'---\ntype: OKR\ntags:\n - alpha\n - beta\ntitle: Ekte tittel\n---\n',
);
assert.equal(get('tags'), null, 'list-key skal gi null, ikke foerste list-element');
assert.equal(get('title'), 'Ekte tittel', 'skalar etter list-blokk resolver fortsatt');
});
test('(L2) tom skalar-key sluker ikke neste rot-key', () => {
const { get } = parseFrontmatter('---\ntitle:\ntype: OKR\n---\n');
assert.equal(get('title'), null, 'tom title skal gi null, ikke "type: OKR"');
assert.equal(get('type'), 'OKR', 'den slukte keyen resolver selv');
});
test('(L3) tom key rett foran innrykket list-blokk gir null', () => {
const { get } = parseFrontmatter('---\ntype: OKR\ndescription:\ntags:\n - x\n---\n');
assert.equal(get('description'), null, 'tom description skal gi null, ikke "tags:"');
});
test('(L4) innrykk-toleransen BEVART: nestet oppslag resolver, forelder gir null', () => {
const { get } = parseFrontmatter(
'---\norganisasjon:\n navn: "NestetOrg"\n type: "offentlig"\n---\n',
);
assert.equal(get('navn'), 'NestetOrg', 'load-bearing inject:69 -- foerste \\s* er urort');
assert.equal(get('organisasjon'), null, 'tom forelder-key skal ikke gi "navn: ..."');
});
test('(L5) tab etter kolon: verdien resolver rent', () => {
// MERK: denne er IKKE en vakt for `\t` i tegnklassen. Mutasjonstest viste at
// `[ ]*` passerer like godt -- .trim() paa :39 normaliserer tabene uansett,
// saa `[ \t]*` og `[ ]*` er atferdsmessig ekvivalente for all input. `\t` staar
// igjen som intensjons-dokumentasjon (horisontal whitespace), ikke som atferd.
// Testen vokter kontrakten "tab-separert verdi resolver", som holder uansett.
const { get } = parseFrontmatter('---\nnavn:\t\tOrg\n---\n');
assert.equal(get('navn'), 'Org');
});
test('writeFrontmatter: skalar uendret av array-gren (additiv)', () => {
const block = writeFrontmatter({ navn: 'Org', type: 'Notat' });
assert.match(block, /navn: Org/);
assert.match(block, /type: Notat/);
assert.doesNotMatch(block, /^\s*-\s/m, 'ingen list-syntaks for skalarer');
});
test('writeFrontmatter: tom array -> kun key-linje, ingen items', () => {
const block = writeFrontmatter({ tags: [] });
assert.match(block, /tags:\n/, 'tom array gir key-linje');
assert.doesNotMatch(block, /^\s*-\s/m, 'ingen item-linjer for tom array');
});