113 lines
4.9 KiB
TypeScript
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");
|
|
});
|
|
});
|