// org-profile-write.test.mjs // Tester atomisk org-profil-skrivehelper (SC1): hjem-skriv, circuit-breaker // fallback til prosjektlokal, og round-trip mot inject-okr-context. // Spawner helperen som subprosess med kontrollert HOME + cwd + stdin. // Zero npm deps. Plassert i tests/ (samme moenster som inject-okr-context.test.mjs). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, writeFileSync, readFileSync, existsSync, realpathSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const HELPER = join(ROOT, 'scripts', 'write-org-profile.mjs'); const HOOK = join(ROOT, 'hooks', 'scripts', 'inject-okr-context.mjs'); function runHelper(cwd, home, input) { // execFileSync returnerer stdout; helperen avslutter alltid med exit 0. return execFileSync('node', [HELPER], { 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('hjem-skriv: helper skriver org-profil til ~/.claude/okr/org/profil.md', () => { withDirs((home, work) => { const profil = '---\nnavn: "HjemskrivOrg"\n---\n'; const out = runHelper(work, home, profil); const target = join(home, '.claude', 'okr', 'org', 'profil.md'); assert.ok(existsSync(target), 'hjem-profil skal finnes etter skriv'); assert.equal(readFileSync(target, 'utf8'), profil, 'innhold skal matche stdin'); // realpathSync normaliserer macOS /var -> /private/var-symlink paa begge sider. assert.equal(realpathSync(out.trim()), realpathSync(target), 'stdout skal rapportere faktisk brukt sti (hjem)'); }); }); test('circuit-breaker: uskrivbart hjem -> fallback til prosjektlokal uten error', () => { withDirs((home, work) => { // Gjoer hjem-skriv umulig: ~/.claude er en FIL, ikke katalog -> mkdirSync // (recursive) under den feiler med ENOTDIR (rot-uavhengig, deterministisk). writeFileSync(join(home, '.claude'), 'not a directory\n'); const profil = '---\nnavn: "FallbackOrg"\n---\n'; // execFileSync kaster hvis exit != 0; at dette IKKE kaster beviser exit 0. const out = runHelper(work, home, profil); const fallback = join(work, '.claude', 'okr.local.md'); assert.ok(existsSync(fallback), 'fallback-profil skal finnes i prosjektlokal sti'); assert.equal(readFileSync(fallback, 'utf8'), profil, 'fallback-innhold skal matche stdin'); assert.equal(realpathSync(out.trim()), realpathSync(fallback), 'stdout skal rapportere fallback-sti'); }); }); test('round-trip: hjem-skrevet org reflekteres av inject-okr-context', () => { withDirs((home, work) => { const profil = '---\nnavn: "RoundTripOrg"\n---\n'; runHelper(work, home, profil); // Tom prosjektkatalog (work) -> hooken faller til hjem-profil (mest-spesifikk-vinner). const injected = runHook(work, home); assert.match(injected, /RoundTripOrg/, 'inject skal lese hjem-profilen helperen skrev'); }); });