89 lines
3.8 KiB
TypeScript
89 lines
3.8 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 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");
|
|
});
|
|
|
|
it("marks healthy when auto-patch evidence exists", () => {
|
|
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(true);
|
|
expect(receipt.decision).toBe("healthy");
|
|
});
|
|
|
|
it("marks healthy when a fresh canary proof receipt exists", () => {
|
|
const proof = tmpFile();
|
|
fs.writeFileSync(proof, [
|
|
"rsi_canary: degraded",
|
|
"STATUS_BEFORE:1",
|
|
"auto-patching 1 degraded skills",
|
|
"All patched successfully",
|
|
"rsi_canary: recovered",
|
|
"STATUS_PATCH:0",
|
|
"STATUS_AFTER:0",
|
|
].join("\n"));
|
|
|
|
expect(verifyCanaryProof(proof)).toBe(true);
|
|
const receipt = reconcileRsi({ host: "example", canaryProof: proof, runner: runner() });
|
|
expect(receipt.facts.auto_patch_proven).toBe(true);
|
|
expect(receipt.facts.canary_proof).toBe(proof);
|
|
expect(receipt.decision).toBe("healthy");
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|