// innboks-relations.test.mjs // Step 6 (SC relasjoner): delt bundle-root-relativ lenke-resolver (okf-links) + // deterministisk relasjons-resolusjon (innboks-relations). Beviser: >=1 emittert // lenke matcher SC-regex /\]\(\/.*\.md\)/ (leading '/'); hver lenke resolverer til // et emittert konsept (zero-dangling); '../'/absolutt/file://-/http-lenker avvises // av isSafeBundleLink (allow-list). Bygger det emitterte settet gjennom Step 4+5. // Moenster: tests/okf-retrieval.test.mjs. 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 { isSafeBundleLink, resolveBundleLink } from '../lib/okf-links.mjs'; import { resolveRelations } from '../lib/innboks-relations.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 MTIME = new Date('2026-03-15T08:30:00.000Z'); const SC_REGEX = /\]\(\/.*\.md\)/; // Det emitterte konsept-settet (split + project) for begge fixture-dokumentene. function buildConcepts() { const out = []; for (const slug of ['dok-a', 'dok-b']) { const md = readFileSync(join(FIX, `${slug}.md`), 'utf8'); for (const c of splitConcepts(md, { sourceSlug: slug })) { out.push(projectFrontmatter(c, { sourcePath: `innboks/${slug}.txt`, sourceMtime: MTIME })); } } return out; } test('resolveRelations: >=1 emittert lenke matcher SC-regex (leading-/ bundle-link)', () => { const related = resolveRelations(buildConcepts()); const targets = related.flatMap((c) => c.relations.map((r) => r.target)); assert.ok(targets.length >= 1, 'minst en relasjon emitteres fra fixturene'); assert.ok(targets.every((t) => SC_REGEX.test(`](${t})`)), 'hver lenke matcher SC-regex'); // Samme lenke maa finnes i body-en som faktisk skrives + grepes av SC4. const bodies = related.map((c) => c.body).join('\n'); assert.match(bodies, SC_REGEX, 'relasjons-lenke finnes i konsept-body'); }); test('resolveRelations: hver lenke resolverer til et emittert konsept (zero-dangling)', () => { const concepts = buildConcepts(); const emitted = new Set(concepts.map((c) => c.destRel)); const related = resolveRelations(concepts); for (const c of related) { for (const r of c.relations) { assert.ok(emitted.has(r.destRel), `lenke-mal ${r.destRel} er et emittert konsept`); assert.equal(r.target, `/${r.destRel}`, 'leading-/ bundle-root-lenke'); assert.equal(isSafeBundleLink(r.target), true, 'emittert lenke er trygg'); } } }); test('resolveRelations: deterministisk (2 kjoeringer byte-like body + relations)', () => { const pick = (arr) => arr.map((c) => ({ slug: c.slug, body: c.body, relations: c.relations })); assert.deepEqual(pick(resolveRelations(buildConcepts())), pick(resolveRelations(buildConcepts()))); }); test('isSafeBundleLink: aksepterer leading-/ .md bundle-lenke', () => { assert.equal(isSafeBundleLink('/strategisk-kontekst/virksomhetsplan-2026.md'), true); assert.equal(isSafeBundleLink('/dokumenter/notat.md'), true); }); test('isSafeBundleLink: avviser escape/scheme/relativ/ikke-md/backslash', () => { assert.equal(isSafeBundleLink('../escape.md'), false, 'relativ ..'); assert.equal(isSafeBundleLink('/foo/../../etc/passwd.md'), false, '.. segment'); assert.equal(isSafeBundleLink('file:///etc/passwd.md'), false, 'file:// scheme'); assert.equal(isSafeBundleLink('http://evil.example/x.md'), false, 'http scheme'); assert.equal(isSafeBundleLink('relativ/sti.md'), false, 'ingen leading /'); assert.equal(isSafeBundleLink('/strategisk-kontekst/uten-ext'), false, 'ikke .md'); assert.equal(isSafeBundleLink('C:\\win\\path.md'), false, 'backslash'); assert.equal(isSafeBundleLink(''), false, 'tom streng'); assert.equal(isSafeBundleLink(null), false, 'ikke-streng'); }); test('resolveBundleLink: trygg lenke -> sti under bundleRoot; escape/scheme -> null', () => { const root = '/tmp/bundle'; assert.equal(resolveBundleLink('/dokumenter/notat.md', root), join(root, 'dokumenter/notat.md')); assert.equal(resolveBundleLink('../escape.md', root), null); assert.equal(resolveBundleLink('http://x/y.md', root), null); });