From 9ab9ebe4992ea1571df1ef93d138223bfd35fa92 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Wed, 24 Jun 2026 11:37:23 +0200 Subject: [PATCH] feat(okr): hook hybrid org-lese-fallback + node-test (SC6) [skip-docs] --- hooks/scripts/inject-okr-context.mjs | 15 +++++- tests/inject-okr-context.test.mjs | 78 ++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/inject-okr-context.test.mjs diff --git a/hooks/scripts/inject-okr-context.mjs b/hooks/scripts/inject-okr-context.mjs index 69e8e47..20e2a5e 100644 --- a/hooks/scripts/inject-okr-context.mjs +++ b/hooks/scripts/inject-okr-context.mjs @@ -7,11 +7,22 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; +import { homedir } from 'node:os'; const cwd = process.cwd(); -const configPath = join(cwd, '.claude', 'okr.local.md'); +const projectConfigPath = join(cwd, '.claude', 'okr.local.md'); +const homeConfigPath = join(homedir(), '.claude', 'okr', 'org', 'profil.md'); -if (!existsSync(configPath)) { +// Hybrid org-profile resolution (most-specific-wins): project-local overrides +// the machine-global home profile. The home path is the Fase 3 migration target; +// this read is forward-compatible and stays inert until that file exists. +// NOTE: only the org PROFILE resolves to home. The cycle/work tree scan +// (okrDir, below) stays cwd-bound -- syklus data is always project-local. +const configPath = existsSync(projectConfigPath) + ? projectConfigPath + : (existsSync(homeConfigPath) ? homeConfigPath : null); + +if (!configPath) { process.exit(0); } diff --git a/tests/inject-okr-context.test.mjs b/tests/inject-okr-context.test.mjs new file mode 100644 index 0000000..e05cf39 --- /dev/null +++ b/tests/inject-okr-context.test.mjs @@ -0,0 +1,78 @@ +// inject-okr-context.test.mjs +// Tester hybrid org-profil-resolusjon (SC6): prosjekt-lokal -> hjem-profil. +// Spawner hooken som subprosess med kontrollert cwd + HOME. Zero npm deps. +// Plassert i tests/ (ikke hooks/scripts/) pga. pathguard-vern av hook-mappa. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HOOK = join( + dirname(fileURLToPath(import.meta.url)), + '..', + 'hooks', + 'scripts', + 'inject-okr-context.mjs', +); + +function makeHomeProfil(homeDir, navn) { + const p = join(homeDir, '.claude', 'okr', 'org'); + mkdirSync(p, { recursive: true }); + writeFileSync(join(p, 'profil.md'), `---\nnavn: "${navn}"\n---\n`); +} + +function makeProjectConfig(workDir, navn) { + const p = join(workDir, '.claude'); + mkdirSync(p, { recursive: true }); + writeFileSync(join(p, 'okr.local.md'), `---\nnavn: "${navn}"\n---\n`); +} + +function runHook(cwd, home) { + // execFileSync returns stdout; the hook always exits 0. + 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('kun-hjem: leser org fra hjem-profil, ingen syklus-kontekst', () => { + withDirs((home, work) => { + makeHomeProfil(home, 'HjemOrg'); + const out = runHook(work, home); + assert.match(out, /HjemOrg/, 'org skal komme fra hjem-profil naar prosjekt-config mangler'); + assert.doesNotMatch(out, /Tilgjengelige kontekstfiler/, 'kun-hjem har ingen prosjekt-lokalt syklus-tre (okrDir er cwd-bundet)'); + }); +}); + +test('begge: prosjekt-lokal vinner over hjem (mest-spesifikk-vinner)', () => { + withDirs((home, work) => { + makeHomeProfil(home, 'HjemOrg'); + makeProjectConfig(work, 'ProsjektOrg'); + const out = runHook(work, home); + assert.match(out, /ProsjektOrg/, 'prosjekt-lokal skal vinne'); + assert.doesNotMatch(out, /HjemOrg/, 'hjem skal ikke leses naar prosjekt-config finnes'); + }); +}); + +test('ingen: exit 0, tom output (graceful, regresjonsvakt)', () => { + withDirs((home, work) => { + const out = runHook(work, home); + assert.equal(out.trim(), '', 'ingen config -> tom stdout, ingen blokkering'); + }); +});