Lukker de fire release-relevante MAJOR-funnene fra /trekreview (S33). Alle TDD
roed -> groenn, ingen fiks landet uten en test som feilet foerst. Suite 163 -> 167.
R1 (af16d5e4) hooks/scripts/coaching-hook.mjs:83 -- LEVENDE REGRESJON innfoert av
1.8.0. Denne releasen skrev status-malen (commands/sporing.md:86-88) om til den
kanoniske skalaen On Track/At Risk/Off Track, mens hooken fortsatt talte kun
/i fare|blokkert/i over tabellrader. En status generert under 1.8.0 ga derfor
atRiskCount = 0 og SessionStart-nudgen sluttet stille aa utloese seg. Hooken
teller naa de kanoniske etikettene; de to norske er BEHOLDT som bakover-
kompatibilitet for status-filer skrevet foer 1.8.0. Nudge-teksten bruker samme
kanoniske vokabular. Ny testcase mates av malen slik den faktisk genereres i dag;
den eksisterende casen beholder gammelt vokabular og daekker legacy-stien.
R2 (7ec575be) F-i-omskrivingen ga hver kommando en Kontekstbevissthet-blokk som
INSTRUERER Glob, men allowed-tools ble kun utvidet i kaskade.md. Glob lagt til i
export, gap, governance, innfoering, kvalitet, moeter, skriv, sporing. Ny vakt-
case (k): nevner BODY verktoeyet, maa frontmatter deklarere det (13 kommandoer
instruerer Glob; alle 13 dekket). Case (i) grepper kun etter fjernede fraser og
kunne ikke fange dette.
R3 (5e61ae0d) tests/package-shape.test.mjs asserterte KUN package.json -- som er
private:true og dermed den ene flaten som aldri shipper. En delvis bump ville
shippet groenn. Ny versjonssync-case dekker .claude-plugin/plugin.json, README-
badgen og begge SKILL.md, med forventet verdi UTLEDET fra package.json (ett sted
aa endre ved neste bump). Mutasjonsbevist: hver av de fire flatene tilbakestilt
til 1.7.1 en om gangen -> casen roed i alle fire tilfeller.
R4 (ccff16e1 + 231c53fc) Tre parallelle confidence-etikettsett overlevde F-c:
fremdriftssporer-agent.md:68 ("Paa sporet / I fare / Blokkert"), :98
("Confidence: [Hoey/Medium/Lav]" -- en annen akse: stoerrelse, ikke sannsynlighet)
og SKILL.md:48 ("blocked"; kanonisk er "off track"). Alle tre erstattet med
referanse til kanon (okr-framework.md:389-392). sporing.md og agenten den
delegerer til svarer naa i samme vokabular. Vakt-case (a)/(b) skanner naa samme
sett som (d) allerede brukte (+ agents/ + SKILL.md), samlet i canonScan(). Ny
case (a2) fanger etikettsett skrevet som bullet eller mal-linje -- tabell-
signaturen alene fanget dem ikke, og det var nettopp formen driften overlevde i.
Divergens gjenkjennes STRUKTURELT (skraastrek-enumerasjon av >= 2 etiketter, med
>= 1 ikke-kanonisk), saa loepende prosa som "For KR i fare" og "Blokkert av
eksterne faktorer" ikke gir falske positive. Verifisert: roed-listen var noeyaktig
de 3 kjente linjene, ingen andre.
CHANGELOG/README: vakt-antall 13 -> 15 cases, suite 149 -> 167, og de fire
fiksene lagt inn under [1.8.0] (Added + Fixed). Ingen versjonsbump.
GJENSTAAR fra reviewet (3 MAJOR, IKKE i denne bolgen): 607313e3 F-d-kadens-
moensteret kan ikke feile paa den kanoniske kadens-tabellen; 029ef814
freshen-references.md:24 baerer fortsatt en foreldet kontekstblokk og vaktens
exclude-kommentar kaller fila feilaktig "allerede-fikset" (den er utenfor
1.8.0-scope by design, jf. ba91fc2 -- kommentaren er usann, ikke ekskluderingen);
7579d59c "Outcome" er utestbar i 10-dims-casen pga. substring-containment.
Verify: node --test --test-reporter=tap tests/*.test.mjs -> 167 pass / 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pUfkQ1YyH75z9y6RBaBHG
157 lines
5.7 KiB
JavaScript
157 lines
5.7 KiB
JavaScript
// 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('at-risk telles per status-RAD (tabell), ikke raaforekomster i prosa (B2/M1)', () => {
|
|
withWork((work) => {
|
|
writeConfig(work, '---\nid: "T2-2026"\n---\n');
|
|
const statusDir = join(work, '.claude', 'okr', 'syklus', 'T2-2026');
|
|
mkdirSync(statusDir, { recursive: true });
|
|
// 2 markerte rader + markoer-ord i forklaring OG prosa: kun radene skal telle.
|
|
writeFileSync(join(statusDir, 'status.md'), [
|
|
'# Status T2-2026',
|
|
'',
|
|
'Merk: "I fare" betyr flat trend; "Blokkert" betyr ingen fremgang.',
|
|
'',
|
|
'| KR | Baseline | Maal | Naa | Score | Status |',
|
|
'|----|----------|------|-----|-------|--------|',
|
|
'| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | I fare |',
|
|
'| KR2: Oppetid | 10 | 25 | 22 | 0.80 | Paa sporet |',
|
|
'| KR3: Tilfredshet | 60% | 90% | 65% | 0.17 | Blokkert |',
|
|
'',
|
|
'KR1 er i fare fordi trenden er flat. KR3 er blokkert av leverandoer.',
|
|
'',
|
|
].join('\n'));
|
|
const out = runHook(work, '2026-06-20'); // midtveis -> at-risk rapporteres
|
|
assert.match(out, /OBS: 2 KR er merket/, '2 markerte rader -> teller noeyaktig 2 (ikke 6)');
|
|
});
|
|
});
|
|
|
|
// R1 (review.md af16d5e4): 1.8.0 skrev om status-malen (commands/sporing.md:86-88) til den
|
|
// KANONISKE confidence-skalaen (okr-framework.md:389-392) — On Track / At Risk / Off Track.
|
|
// Hooken talte fortsatt kun det gamle norske vokabularet, saa nudgen doede stille under 1.8.0.
|
|
// Denne casen mater malen slik den faktisk genereres i dag; casen over beholder det gamle
|
|
// vokabularet og daekker dermed bakover-kompatibilitet for arkiverte status-filer.
|
|
test('at-risk telles paa kanonisk 1.8.0-vokabular (At Risk + Off Track, ikke On Track)', () => {
|
|
withWork((work) => {
|
|
writeConfig(work, '---\nid: "T2-2026"\n---\n');
|
|
const statusDir = join(work, '.claude', 'okr', 'syklus', 'T2-2026');
|
|
mkdirSync(statusDir, { recursive: true });
|
|
// Emoji som \u-escapes: test-kilden holdes ASCII-ren (bash 3.2 set -u multibyte).
|
|
const GUL = '\u{1F7E1}';
|
|
const GROENN = '\u{1F7E2}';
|
|
const ROED = '\u{1F534}';
|
|
writeFileSync(join(statusDir, 'status.md'), [
|
|
'# Status T2-2026',
|
|
'',
|
|
'| KR | Baseline | Target | Naa | Score | Status |',
|
|
'|----|----------|--------|-----|-------|--------|',
|
|
`| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | At Risk ${GUL} |`,
|
|
`| KR2: Fartshumper installert | 0% | 100% | 60% | 0.60 | On Track ${GROENN} |`,
|
|
`| KR3: Foreldre-tilfredshet | 60% | 90% | 65% | 0.17 | Off Track ${ROED} |`,
|
|
'',
|
|
].join('\n'));
|
|
const out = runHook(work, '2026-06-20');
|
|
assert.match(
|
|
out,
|
|
/OBS: 2 KR er merket/,
|
|
'At Risk + Off Track teller (2); On Track skal IKKE telle',
|
|
);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|