test: guard against stale factory memory
This commit is contained in:
parent
14d656d441
commit
98729709d7
|
|
@ -23,7 +23,7 @@ function passedGateReceipt(): string {
|
||||||
task: "gate",
|
task: "gate",
|
||||||
repo: ".",
|
repo: ".",
|
||||||
commands: ["fable-agent factory gate gate --repo .", "fable-agent fable5 verify repo gate --repo ."],
|
commands: ["fable-agent factory gate gate --repo .", "fable-agent fable5 verify repo gate --repo ."],
|
||||||
createdAt: "2026-06-23T00:00:00.000Z",
|
createdAt: "2026-06-30T00:00:00.000Z",
|
||||||
})}\n`);
|
})}\n`);
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ function passedGateReceiptWithPlan(plan: string): string {
|
||||||
repo: ".",
|
repo: ".",
|
||||||
commands: ["fable-agent factory gate gate --repo .", "fable-agent fable5 verify repo gate --repo ."],
|
commands: ["fable-agent factory gate gate --repo .", "fable-agent fable5 verify repo gate --repo ."],
|
||||||
provenance: { plan },
|
provenance: { plan },
|
||||||
createdAt: "2026-06-23T00:00:00.000Z",
|
createdAt: "2026-06-30T00:00:00.000Z",
|
||||||
})}
|
})}
|
||||||
`);
|
`);
|
||||||
return file;
|
return file;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ afterEach(() => {
|
||||||
|
|
||||||
function factoryResult(overrides = {}) {
|
function factoryResult(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
factory: { status: "ok", containers: 25 },
|
factory: { status: "ok", containers: 16 },
|
||||||
deploy: { status: "ok", service: "git-proxy-deploy" },
|
deploy: { status: "ok", service: "git-proxy-deploy" },
|
||||||
factoryAvailable: true,
|
factoryAvailable: true,
|
||||||
deployAvailable: true,
|
deployAvailable: true,
|
||||||
|
|
@ -30,7 +30,7 @@ describe("factory reconciliation receipt", () => {
|
||||||
schema: "fable.factory.reconciliation.v1",
|
schema: "fable.factory.reconciliation.v1",
|
||||||
createdAt: "2026-06-27T00:00:00.000Z",
|
createdAt: "2026-06-27T00:00:00.000Z",
|
||||||
sourceCommand: "test command",
|
sourceCommand: "test command",
|
||||||
containerCount: 25,
|
containerCount: 16,
|
||||||
deployRoute: { url: CANONICAL_DEPLOY_ROUTE, status: "ok", name: "git-proxy-deploy" },
|
deployRoute: { url: CANONICAL_DEPLOY_ROUTE, status: "ok", name: "git-proxy-deploy" },
|
||||||
decision: "ready",
|
decision: "ready",
|
||||||
reasons: [],
|
reasons: [],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { CANONICAL_DEPLOY_ROUTE, DEPLOY_WEBHOOK_LEGACY_PATH, isAllowedDeployUrl, isForbiddenDeployRoute, isGuardedDeployUrl, LEGACY_DEPLOY_PORT } from "./factory-routes.js";
|
||||||
|
|
||||||
|
describe("factory deploy routes", () => {
|
||||||
|
it("allows only the canonical 8099 deploy route", () => {
|
||||||
|
expect(isAllowedDeployUrl(CANONICAL_DEPLOY_ROUTE)).toBe(true);
|
||||||
|
expect(isGuardedDeployUrl(CANONICAL_DEPLOY_ROUTE)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps legacy 8098 deploy-webhook forbidden", () => {
|
||||||
|
const legacyByPort = `http://77.42.112.29:${LEGACY_DEPLOY_PORT}/deploy`;
|
||||||
|
const legacyByPath = `http://77.42.112.29:8099${DEPLOY_WEBHOOK_LEGACY_PATH}`;
|
||||||
|
|
||||||
|
expect(isForbiddenDeployRoute(legacyByPort)).toBe(true);
|
||||||
|
expect(isForbiddenDeployRoute(legacyByPath)).toBe(true);
|
||||||
|
expect(isAllowedDeployUrl(legacyByPort)).toBe(false);
|
||||||
|
expect(isGuardedDeployUrl(legacyByPort)).toBe(false);
|
||||||
|
expect(isGuardedDeployUrl(legacyByPath)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -59,13 +59,13 @@ describe("rsi reconcile", () => {
|
||||||
expect(receipt.rubric.format).toBe(5);
|
expect(receipt.rubric.format).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks healthy when auto-patch evidence exists", () => {
|
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" } }) });
|
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.facts.auto_patch_proven).toBe(false);
|
||||||
expect(receipt.decision).toBe("healthy");
|
expect(receipt.decision).toBe("healthy_but_autopatch_unproven");
|
||||||
expect(receipt.evidence.auto_patch_proven).toBe(true);
|
expect(receipt.evidence.auto_patch_proven).toBe(false);
|
||||||
expect(receipt.evidence.rsi_healthy).toBe(true);
|
expect(receipt.evidence.rsi_healthy).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks healthy when a fresh structured canary proof receipt exists", () => {
|
it("marks healthy when a fresh structured canary proof receipt exists", () => {
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,10 @@ export function reconcileRsi(opts: RsiReconcileOptions): RsiReconcileReceipt {
|
||||||
latest_score: latest.match(/RSI:\s*([^\n]+)/)?.[1]?.trim(),
|
latest_score: latest.match(/RSI:\s*([^\n]+)/)?.[1]?.trim(),
|
||||||
factory_watcher_active: /active/.test(checks.find((c) => c.name === "factory_watcher")?.stdout ?? ""),
|
factory_watcher_active: /active/.test(checks.find((c) => c.name === "factory_watcher")?.stdout ?? ""),
|
||||||
deploy_route_present: isOk("deploy_route", checks),
|
deploy_route_present: isOk("deploy_route", checks),
|
||||||
auto_patch_proven: (/auto-patching/.test(latest) && /All patched/.test(latest)) || canaryProofOk,
|
auto_patch_proven: canaryProofOk,
|
||||||
...(canaryProofOk ? { canary_proof: opts.canaryProof } : {}),
|
...(canaryProofOk ? { canary_proof: opts.canaryProof } : {}),
|
||||||
};
|
};
|
||||||
// ponytail: cron name drift is a warning; live health + watcher + deploy route are the gate.
|
// ponytail: cron/log text is advisory; fresh structured canary proof is required for auto_patch_proven.
|
||||||
const coreHealthy = facts.skill_health_present && /100%|65\/65|66\/66/.test(facts.latest_score ?? "") && facts.factory_watcher_active && facts.deploy_route_present;
|
const coreHealthy = facts.skill_health_present && /100%|65\/65|66\/66/.test(facts.latest_score ?? "") && facts.factory_watcher_active && facts.deploy_route_present;
|
||||||
const createdAt = (opts.now ?? new Date()).toISOString();
|
const createdAt = (opts.now ?? new Date()).toISOString();
|
||||||
const decision = coreHealthy ? (facts.auto_patch_proven ? "healthy" : "healthy_but_autopatch_unproven") : "degraded";
|
const decision = coreHealthy ? (facts.auto_patch_proven ? "healthy" : "healthy_but_autopatch_unproven") : "degraded";
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,14 @@ describe("verifyCycle", () => {
|
||||||
it("writes a passing receipt with 8099 deploy evidence", async () => {
|
it("writes a passing receipt with 8099 deploy evidence", async () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "verify-cycle-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "verify-cycle-"));
|
||||||
const out = path.join(dir, "receipt.json");
|
const out = path.join(dir, "receipt.json");
|
||||||
|
const proof = path.join(dir, "canary.json");
|
||||||
|
fs.writeFileSync(proof, JSON.stringify(canaryProof("2026-06-19T00:00:00.000Z")));
|
||||||
const receipt = await verifyCycle({
|
const receipt = await verifyCycle({
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
skill: "rsi_canary",
|
skill: "rsi_canary",
|
||||||
out,
|
out,
|
||||||
allowDirty: true,
|
allowDirty: true,
|
||||||
|
canaryProof: proof,
|
||||||
now: new Date("2026-06-19T00:00:00.000Z"),
|
now: new Date("2026-06-19T00:00:00.000Z"),
|
||||||
factoryCheck: async () => factoryOk,
|
factoryCheck: async () => factoryOk,
|
||||||
runner,
|
runner,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue