diff --git a/plugins/voyage/hooks/hooks.json b/plugins/voyage/hooks/hooks.json index 851194a..e403b20 100644 --- a/plugins/voyage/hooks/hooks.json +++ b/plugins/voyage/hooks/hooks.json @@ -60,6 +60,16 @@ } ] } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/otel-export.mjs" + } + ] + } ] } } diff --git a/plugins/voyage/tests/hooks/hooks-json-stop-wired.test.mjs b/plugins/voyage/tests/hooks/hooks-json-stop-wired.test.mjs new file mode 100644 index 0000000..dc9523d --- /dev/null +++ b/plugins/voyage/tests/hooks/hooks-json-stop-wired.test.mjs @@ -0,0 +1,65 @@ +// SC-13: hooks.json wires Stop event to otel-export.mjs +// HIGH-risk-mitigering — verify deterministic config-pinning (mønster fra +// tests/lib/doc-consistency.test.mjs). + +import { test } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const HOOKS_JSON_PATH = resolve(__dirname, '../../hooks/hooks.json'); + +function loadHooksJson() { + const raw = readFileSync(HOOKS_JSON_PATH, 'utf8'); + return JSON.parse(raw); +} + +test('hooks.json — Stop key exists with at least one entry', () => { + const cfg = loadHooksJson(); + assert.ok(cfg.hooks, 'hooks.json mangler top-level "hooks" object'); + assert.ok(Array.isArray(cfg.hooks.Stop), 'hooks.json mangler "Stop" array'); + assert.ok(cfg.hooks.Stop.length >= 1, 'Stop array er tom — forventet ≥1 entry'); +}); + +test('hooks.json — Stop entry refererer otel-export.mjs', () => { + const cfg = loadHooksJson(); + const stopEntries = cfg.hooks.Stop; + const allCommands = stopEntries.flatMap((entry) => + (entry.hooks || []).map((h) => h.command || ''), + ); + const hasOtelExport = allCommands.some((cmd) => cmd.includes('otel-export.mjs')); + assert.ok( + hasOtelExport, + `ingen Stop-hook refererer otel-export.mjs. Funnet: ${JSON.stringify(allCommands)}`, + ); +}); + +test('hooks.json — Stop entry bruker ${CLAUDE_PLUGIN_ROOT}-substitusjon', () => { + const cfg = loadHooksJson(); + const stopEntries = cfg.hooks.Stop; + const otelEntry = stopEntries + .flatMap((entry) => entry.hooks || []) + .find((h) => (h.command || '').includes('otel-export.mjs')); + assert.ok(otelEntry, 'fant ikke otel-export-entry i Stop'); + assert.match( + otelEntry.command, + /\$\{CLAUDE_PLUGIN_ROOT\}/, + 'otel-export-command bruker ikke ${CLAUDE_PLUGIN_ROOT}-prefix — relative paths feiler i headless', + ); + assert.match( + otelEntry.command, + /^node\s+/, + 'otel-export-command starter ikke med "node " — invocation-form ikke korrekt', + ); +}); + +test('hooks.json — Stop entry har "type": "command"', () => { + const cfg = loadHooksJson(); + const stopEntries = cfg.hooks.Stop; + const otelHook = stopEntries + .flatMap((entry) => entry.hooks || []) + .find((h) => (h.command || '').includes('otel-export.mjs')); + assert.equal(otelHook.type, 'command', 'otel-export-hook mangler "type": "command"'); +});