From b594eeb88c862fbf22b17ecc19ffb8695531f575 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 26 Jun 2026 00:11:14 +0200 Subject: [PATCH] feat(okr): coaching-hook klokke-seam + node-test fixtures (SC4) [skip-docs] --- hooks/scripts/coaching-hook.mjs | 4 +- tests/coaching-hook.test.mjs | 99 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 tests/coaching-hook.test.mjs diff --git a/hooks/scripts/coaching-hook.mjs b/hooks/scripts/coaching-hook.mjs index 9f98df2..fd6eae5 100644 --- a/hooks/scripts/coaching-hook.mjs +++ b/hooks/scripts/coaching-hook.mjs @@ -50,7 +50,9 @@ try { totalWeeks = 13; } - const now = new Date(); + // Testable clock seam: OKR_NOW (ISO date) overrides the wall clock so the + // phase branches (early/mid/late) can be asserted deterministically. + const now = process.env.OKR_NOW ? new Date(process.env.OKR_NOW) : new Date(); const cycleStart = new Date(cycleYear, startMonth, 1); const cycleEnd = new Date(cycleYear, endMonth + 1, 0); // last day of end month diff --git a/tests/coaching-hook.test.mjs b/tests/coaching-hook.test.mjs new file mode 100644 index 0000000..aa45bcf --- /dev/null +++ b/tests/coaching-hook.test.mjs @@ -0,0 +1,99 @@ +// coaching-hook.test.mjs +// Tester coaching-hook (SC4): graceful-exit-grener + gyldig-syklus-emisjon + +// deterministisk fase via OKR_NOW klokke-seam. Spawner hooken som subprosess +// med kontrollert cwd + env. Zero npm deps. Coaching leser kun cwd, saa én +// temp-dir per test holder. Moenster: tests/inject-okr-context.test.mjs. + +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', + 'coaching-hook.mjs', +); + +function writeConfig(workDir, body) { + const p = join(workDir, '.claude'); + mkdirSync(p, { recursive: true }); + writeFileSync(join(p, 'okr.local.md'), body); +} + +function runHook(cwd, okrNow) { + // execFileSync returnerer stdout; hooken avslutter alltid med exit 0. + const env = { ...process.env }; + if (okrNow) env.OKR_NOW = okrNow; + else delete env.OKR_NOW; + return execFileSync('node', [HOOK], { cwd, env, encoding: 'utf8' }); +} + +function withWork(fn) { + const work = mkdtempSync(join(tmpdir(), 'okrcoach-')); + try { + fn(work); + } finally { + rmSync(work, { recursive: true, force: true }); + } +} + +test('ingen config: exit 0, tom stdout (graceful)', () => { + withWork((work) => { + const out = runHook(work); + assert.equal(out.trim(), '', 'ingen config -> tom stdout, ingen blokkering'); + }); +}); + +test('config uten id: exit 0, tom stdout', () => { + withWork((work) => { + writeConfig(work, '---\nnavn: "Org"\n---\n'); + const out = runHook(work); + assert.equal(out.trim(), '', 'manglende id -> tom stdout'); + }); +}); + +test('ugyldig id: exit 0, tom stdout', () => { + withWork((work) => { + writeConfig(work, '---\nid: "ugyldig"\n---\n'); + const out = runHook(work); + assert.equal(out.trim(), '', 'id som ikke matcher T/Q-moenster -> tom stdout'); + }); +}); + +test('gyldig syklus: emitterer systemMessage', () => { + withWork((work) => { + writeConfig(work, '---\nid: "T2-2026"\n---\n'); + const out = runHook(work); + assert.match(out, /OKR coaching/, 'gyldig syklus skal emittere coaching-melding'); + }); +}); + +test('OKR_NOW tidlig fase: early-coaching', () => { + withWork((work) => { + writeConfig(work, '---\nid: "T2-2026"\n---\n'); + const out = runHook(work, '2026-05-05'); + assert.match(out, /Tidlig i syklusen/, 'uke 1 av T2 -> tidlig fase'); + }); +}); + +test('OKR_NOW midtveis fase: mid-coaching', () => { + withWork((work) => { + writeConfig(work, '---\nid: "T2-2026"\n---\n'); + const out = runHook(work, '2026-06-20'); + assert.match(out, /Midtveis i syklusen/, 'uke ~8 av T2 -> midtveis fase'); + }); +}); + +test('OKR_NOW sen fase: late-coaching', () => { + withWork((work) => { + writeConfig(work, '---\nid: "T2-2026"\n---\n'); + const out = runHook(work, '2026-08-25'); + assert.match(out, /sluttspurt/, 'uke ~16 av T2 -> sen fase'); + }); +});