test: strengthen uok lifecycle parity contracts
This commit is contained in:
parent
fec9292104
commit
cfde65fdd5
2 changed files with 213 additions and 9 deletions
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue