fable-agent/src/fable5/rsi-reconcile.test.ts

113 lines
4.9 KiB
TypeScript

import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { reconcileRsi, verifyCanaryProof } from "./rsi-reconcile.js";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
function tmpFile(): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "fable-rsi-reconcile-"));
roots.push(root);
return path.join(root, "receipt.json");
}
function canaryProof(createdAt: string) {
return {
schema: "fable.rsi.canary-proof.v1",
created_at: createdAt,
skill: "rsi_canary",
degraded_before: true,
status_before: 1,
auto_patch: {
command: "python3 /tmp/skill_health.py --auto-patch",
status: 0,
evidence: "auto-patching 1 degraded skills\nAll patched successfully",
},
recovered_after: true,
status_after: 0,
};
}
function runner(overrides: Record<string, { status: number; stdout: string; stderr?: string }> = {}) {
return (_command: string, args: string[]) => {
const script = args.at(-1) ?? "";
const hit = Object.entries(overrides).find(([needle]) => script.includes(needle))?.[1];
if (hit) return { status: hit.status, stdout: hit.stdout, stderr: hit.stderr ?? "" };
if (script.includes("skill_health.py|rsi-diagnosis")) return { status: 0, stdout: "0 */6 * * * docker cp /tmp/skill_health.py hermes:/tmp/skill_health.py\n", stderr: "" };
if (script.includes("test -f /tmp/skill_health.py")) return { status: 0, stdout: "present\n", stderr: "" };
if (script.includes("tail -160")) return { status: 0, stdout: "RSI: 65/65 (100%)\nAll healthy — ZTE idle\n", stderr: "" };
if (script.includes("systemctl is-active")) return { status: 0, stdout: "active\n", stderr: "" };
if (script.includes("/deploy")) return { status: 0, stdout: "present\n", stderr: "" };
return { status: 1, stdout: "", stderr: "unexpected" };
};
}
describe("rsi reconcile", () => {
it("marks healthy but autopatch unproven when all health checks pass", () => {
const receipt = reconcileRsi({ host: "example", runner: runner(), now: new Date("2026-06-18T00:00:00Z") });
expect(receipt.facts.cron_present).toBe(true);
expect(receipt.facts.latest_score).toBe("65/65 (100%)");
expect(receipt.decision).toBe("healthy_but_autopatch_unproven");
expect(receipt.evidence.deploy_target).toBe("8099/deploy");
expect(receipt.evidence.rsi_healthy).toBe(false);
expect(receipt.rubric.format).toBe(5);
});
it("keeps loose auto-patch log text advisory", () => {
const receipt = reconcileRsi({ host: "example", runner: runner({ "tail -160": { status: 0, stdout: "RSI: 65/65 (100%)\nauto-patching 1 degraded skills\nAll patched successfully\n" } }) });
expect(receipt.facts.auto_patch_proven).toBe(false);
expect(receipt.decision).toBe("healthy_but_autopatch_unproven");
expect(receipt.evidence.auto_patch_proven).toBe(false);
expect(receipt.evidence.rsi_healthy).toBe(false);
});
it("marks healthy when a fresh structured canary proof receipt exists", () => {
const proof = tmpFile();
fs.writeFileSync(proof, JSON.stringify(canaryProof("2026-06-18T00:00:00.000Z")));
expect(verifyCanaryProof(proof, new Date("2026-06-18T01:00:00.000Z"))).toEqual({ ok: true });
const receipt = reconcileRsi({ host: "example", canaryProof: proof, runner: runner(), now: new Date("2026-06-18T01:00:00.000Z") });
expect(receipt.facts.auto_patch_proven).toBe(true);
expect(receipt.facts.canary_proof).toBe(proof);
expect(receipt.decision).toBe("healthy");
});
it("rejects stale or unstructured canary proof", () => {
const old = tmpFile();
fs.writeFileSync(old, JSON.stringify(canaryProof("2026-06-16T00:00:00.000Z")));
expect(verifyCanaryProof(old, new Date("2026-06-18T01:00:00.000Z"))).toEqual({ ok: false, reason: "stale" });
const text = tmpFile();
fs.writeFileSync(text, "rsi_canary: degraded\nSTATUS_BEFORE:1\n");
expect(verifyCanaryProof(text, new Date("2026-06-18T01:00:00.000Z"))).toEqual({ ok: false, reason: "invalid_json" });
});
it("keeps healthy when only cron naming drifts", () => {
const receipt = reconcileRsi({ host: "example", runner: runner({ "skill_health.py|rsi-diagnosis": { status: 1, stdout: "" } }) });
expect(receipt.facts.cron_present).toBe(false);
expect(receipt.decision).toBe("healthy_but_autopatch_unproven");
});
it("marks degraded when live skill health is missing", () => {
const receipt = reconcileRsi({ host: "example", runner: runner({ "test -f /tmp/skill_health.py": { status: 1, stdout: "" } }) });
expect(receipt.facts.skill_health_present).toBe(false);
expect(receipt.decision).toBe("degraded");
});
it("writes a receipt", () => {
const out = tmpFile();
reconcileRsi({ host: "example", out, runner: runner() });
expect(fs.readFileSync(out, "utf-8")).toContain("healthy_but_autopatch_unproven");
});
});