diff --git a/src/fable5/factory-deploy.test.ts b/src/fable5/factory-deploy.test.ts index fb4bfe8..eb54fa6 100644 --- a/src/fable5/factory-deploy.test.ts +++ b/src/fable5/factory-deploy.test.ts @@ -23,7 +23,7 @@ function passedGateReceipt(): string { task: "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`); return file; } @@ -36,7 +36,7 @@ function passedGateReceiptWithPlan(plan: string): string { repo: ".", commands: ["fable-agent factory gate gate --repo .", "fable-agent fable5 verify repo gate --repo ."], provenance: { plan }, - createdAt: "2026-06-23T00:00:00.000Z", + createdAt: "2026-06-30T00:00:00.000Z", })} `); return file; diff --git a/src/fable5/factory-reconciliation.test.ts b/src/fable5/factory-reconciliation.test.ts index cfe181f..2086d13 100644 --- a/src/fable5/factory-reconciliation.test.ts +++ b/src/fable5/factory-reconciliation.test.ts @@ -13,7 +13,7 @@ afterEach(() => { function factoryResult(overrides = {}) { return { - factory: { status: "ok", containers: 25 }, + factory: { status: "ok", containers: 16 }, deploy: { status: "ok", service: "git-proxy-deploy" }, factoryAvailable: true, deployAvailable: true, @@ -30,7 +30,7 @@ describe("factory reconciliation receipt", () => { schema: "fable.factory.reconciliation.v1", createdAt: "2026-06-27T00:00:00.000Z", sourceCommand: "test command", - containerCount: 25, + containerCount: 16, deployRoute: { url: CANONICAL_DEPLOY_ROUTE, status: "ok", name: "git-proxy-deploy" }, decision: "ready", reasons: [], diff --git a/src/fable5/factory-routes.test.ts b/src/fable5/factory-routes.test.ts new file mode 100644 index 0000000..aba4340 --- /dev/null +++ b/src/fable5/factory-routes.test.ts @@ -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); + }); +}); diff --git a/src/fable5/rsi-reconcile.test.ts b/src/fable5/rsi-reconcile.test.ts index 655662c..9a29960 100644 --- a/src/fable5/rsi-reconcile.test.ts +++ b/src/fable5/rsi-reconcile.test.ts @@ -59,13 +59,13 @@ describe("rsi reconcile", () => { 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" } }) }); - expect(receipt.facts.auto_patch_proven).toBe(true); - expect(receipt.decision).toBe("healthy"); - expect(receipt.evidence.auto_patch_proven).toBe(true); - expect(receipt.evidence.rsi_healthy).toBe(true); + 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", () => { diff --git a/src/fable5/rsi-reconcile.ts b/src/fable5/rsi-reconcile.ts index a5ff96e..d329ce4 100644 --- a/src/fable5/rsi-reconcile.ts +++ b/src/fable5/rsi-reconcile.ts @@ -75,10 +75,10 @@ export function reconcileRsi(opts: RsiReconcileOptions): RsiReconcileReceipt { latest_score: latest.match(/RSI:\s*([^\n]+)/)?.[1]?.trim(), factory_watcher_active: /active/.test(checks.find((c) => c.name === "factory_watcher")?.stdout ?? ""), 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 } : {}), }; - // 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 createdAt = (opts.now ?? new Date()).toISOString(); const decision = coreHealthy ? (facts.auto_patch_proven ? "healthy" : "healthy_but_autopatch_unproven") : "degraded"; diff --git a/src/fable5/verify-cycle.test.ts b/src/fable5/verify-cycle.test.ts index 75d8c6d..185473e 100644 --- a/src/fable5/verify-cycle.test.ts +++ b/src/fable5/verify-cycle.test.ts @@ -47,11 +47,14 @@ describe("verifyCycle", () => { it("writes a passing receipt with 8099 deploy evidence", async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "verify-cycle-")); 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({ host: "127.0.0.1", skill: "rsi_canary", out, allowDirty: true, + canaryProof: proof, now: new Date("2026-06-19T00:00:00.000Z"), factoryCheck: async () => factoryOk, runner,