From cfde65fdd5d460874be5d00ab8c48f95a0c26976 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Wed, 6 May 2026 01:12:49 +0200 Subject: [PATCH] test: strengthen uok lifecycle parity contracts --- .../sf/tests/uok-parity-report.test.mjs | 193 +++++++++++++++++- .../extensions/sf/uok/parity-report.js | 29 ++- 2 files changed, 213 insertions(+), 9 deletions(-) diff --git a/src/resources/extensions/sf/tests/uok-parity-report.test.mjs b/src/resources/extensions/sf/tests/uok-parity-report.test.mjs index b1c3f8d66..8ad2e7629 100644 --- a/src/resources/extensions/sf/tests/uok-parity-report.test.mjs +++ b/src/resources/extensions/sf/tests/uok-parity-report.test.mjs @@ -1,13 +1,70 @@ import assert from "node:assert/strict"; -import { test } from "vitest"; - +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, test } from "vitest"; +import { runAutoLoopWithUok } from "../uok/kernel.js"; import { buildParityReport, hasCurrentParityWarning, + parseParityEvents, UNMATCHED_RUN_STALE_MS, } from "../uok/parity-report.js"; const NOW = Date.parse("2026-05-06T00:00:00.000Z"); +const tmpRoots = []; + +afterEach(() => { + for (const dir of tmpRoots.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +function makeProject() { + const root = mkdtempSync(join(tmpdir(), "sf-uok-parity-")); + tmpRoots.push(root); + mkdirSync(join(root, ".sf", "runtime"), { recursive: true }); + return root; +} + +function testDeps(preferences = {}) { + return { + loadEffectiveSFPreferences() { + return { + preferences: { + uok: { + enabled: true, + audit_envelope: { enabled: false }, + audit_unified: { enabled: false }, + ...preferences.uok, + }, + }, + }; + }, + }; +} + +function testCtx(sessionId = "session-test") { + return { + sessionManager: { + getSessionId() { + return sessionId; + }, + }, + }; +} + +function readProjectParityEvents(projectRoot) { + const path = join(projectRoot, ".sf", "runtime", "uok-parity.jsonl"); + assert.equal(existsSync(path), true); + return parseParityEvents(readFileSync(path, "utf-8")); +} test("buildParityReport_legacy_anonymous_missing_exit_is_historical_not_current", () => { const report = buildParityReport( @@ -96,3 +153,135 @@ test("buildParityReport_run_exit_balances_enter", () => { assert.deepEqual(report.unmatchedRuns, []); assert.equal(hasCurrentParityWarning(report), false); }); + +test("buildParityReport_fresh_error_is_current_but_stale_error_is_historical", () => { + const report = buildParityReport( + [ + { + ts: new Date(NOW - 5_000).toISOString(), + runId: "uok-fresh-error", + path: "uok-kernel", + phase: "exit", + status: "error", + error: "fresh failure", + }, + { + ts: new Date(NOW - UNMATCHED_RUN_STALE_MS - 1_000).toISOString(), + runId: "uok-old-error", + path: "uok-kernel", + phase: "exit", + status: "error", + error: "old failure", + }, + ], + "/tmp/uok-parity.jsonl", + NOW, + ); + + assert.equal(report.currentErrorEvents, 1); + assert.equal(report.historicalErrorEvents, 1); + assert.deepEqual(report.criticalMismatches, ["fresh failure"]); + assert.deepEqual(report.historicalCriticalMismatches, ["old failure"]); + assert.equal(hasCurrentParityWarning(report), true); +}); + +test("buildParityReport_stale_parity_diff_is_historical_not_current", () => { + const report = buildParityReport( + [ + { + kind: "parity-diff", + ts: new Date(NOW - UNMATCHED_RUN_STALE_MS - 1_000).toISOString(), + plane: "gitops", + turnId: "T01", + match: false, + divergence: "legacy=commit uok=status-only", + }, + ], + "/tmp/uok-parity.jsonl", + NOW, + ); + + assert.equal(report.divergencesByPlane.gitops, 1); + assert.deepEqual(report.criticalMismatches, []); + assert.deepEqual(report.historicalCriticalMismatches, [ + "[gitops] legacy=commit uok=status-only", + ]); + assert.equal(hasCurrentParityWarning(report), false); +}); + +test("runAutoLoopWithUok_success_writes_balanced_run_id_heartbeats", async () => { + const projectRoot = makeProject(); + const state = { basePath: projectRoot, autoStartTime: NOW }; + let observerProvided = false; + + await runAutoLoopWithUok({ + ctx: testCtx("session-success"), + pi: {}, + s: state, + deps: testDeps(), + async runKernelLoop(_ctx, _pi, _s, deps) { + observerProvided = Boolean(deps.uokObserver); + }, + async runStandardLoop() { + throw new Error("standard loop should not run when uok is enabled"); + }, + }); + + assert.equal(observerProvided, true); + assert.equal(state.currentUokRunId, undefined); + const events = readProjectParityEvents(projectRoot); + assert.equal(events.length, 2); + assert.equal(events[0].phase, "enter"); + assert.equal(events[1].phase, "exit"); + assert.equal(events[1].status, "ok"); + assert.equal(events[0].runId, events[1].runId); + const report = JSON.parse( + readFileSync( + join(projectRoot, ".sf", "runtime", "uok-parity-report.json"), + "utf-8", + ), + ); + assert.equal(report.missingExitEvents, 0); + assert.equal(hasCurrentParityWarning(report), false); +}); + +test("runAutoLoopWithUok_throw_still_writes_exit_and_current_error_report", async () => { + const projectRoot = makeProject(); + const state = { basePath: projectRoot, autoStartTime: NOW }; + + await assert.rejects( + () => + runAutoLoopWithUok({ + ctx: testCtx("session-error"), + pi: {}, + s: state, + deps: testDeps(), + async runKernelLoop() { + throw new Error("boom"); + }, + async runStandardLoop() { + throw new Error("standard loop should not run when uok is enabled"); + }, + }), + /boom/, + ); + + assert.equal(state.currentUokRunId, undefined); + const events = readProjectParityEvents(projectRoot); + assert.equal(events.length, 2); + assert.equal(events[0].phase, "enter"); + assert.equal(events[1].phase, "exit"); + assert.equal(events[1].status, "error"); + assert.equal(events[1].error, "boom"); + assert.equal(events[0].runId, events[1].runId); + const report = JSON.parse( + readFileSync( + join(projectRoot, ".sf", "runtime", "uok-parity-report.json"), + "utf-8", + ), + ); + assert.equal(report.missingExitEvents, 0); + assert.equal(report.currentErrorEvents, 1); + assert.deepEqual(report.criticalMismatches, ["boom"]); + assert.equal(hasCurrentParityWarning(report), true); +}); diff --git a/src/resources/extensions/sf/uok/parity-report.js b/src/resources/extensions/sf/uok/parity-report.js index b4a74b513..8de6d2a7e 100644 --- a/src/resources/extensions/sf/uok/parity-report.js +++ b/src/resources/extensions/sf/uok/parity-report.js @@ -63,7 +63,10 @@ export function buildParityReport( const paths = {}; const statuses = {}; const criticalMismatches = []; + const historicalCriticalMismatches = []; const runs = new Map(); + let currentErrorEvents = 0; + let historicalErrorEvents = 0; let enterEvents = 0; let exitEvents = 0; let legacyEnterEvents = 0; @@ -82,12 +85,13 @@ export function buildParityReport( if (!event.match) { divergencesByPlane[event.plane] = (divergencesByPlane[event.plane] ?? 0) + 1; + const target = isFreshTimestamp(event.ts, nowMs, staleMs) + ? criticalMismatches + : historicalCriticalMismatches; if (event.divergence) { - criticalMismatches.push(`[${event.plane}] ${event.divergence}`); + target.push(`[${event.plane}] ${event.divergence}`); } else { - criticalMismatches.push( - `[${event.plane}] divergence (turn=${event.turnId})`, - ); + target.push(`[${event.plane}] divergence (turn=${event.turnId})`); } } continue; @@ -139,7 +143,14 @@ export function buildParityReport( } } if (heartbeat.status === "error") { - criticalMismatches.push(heartbeat.error ?? "parity event reported error"); + const message = heartbeat.error ?? "parity event reported error"; + if (isFreshTimestamp(heartbeat.ts, nowMs, staleMs)) { + currentErrorEvents += 1; + criticalMismatches.push(message); + } else { + historicalErrorEvents += 1; + historicalCriticalMismatches.push(message); + } } } const unmatchedRuns = Array.from(runs.values()).filter( @@ -172,6 +183,9 @@ export function buildParityReport( paths, statuses, criticalMismatches, + historicalCriticalMismatches, + currentErrorEvents, + historicalErrorEvents, enterEvents, exitEvents, missingExitEvents, @@ -186,8 +200,9 @@ export function buildParityReport( export function hasCurrentParityWarning(report) { const criticalMismatches = report?.criticalMismatches ?? []; - const errors = report?.statuses?.error ?? 0; - return criticalMismatches.length > 0 || errors > 0; + const currentErrors = + report?.currentErrorEvents ?? report?.statuses?.error ?? 0; + return criticalMismatches.length > 0 || currentErrors > 0; } export function writeParityReport(basePath) {