Keep release-gate verdicts restricted to fresh task-list responses. - Strip release-gate data from SSE and non-board task paths. - Reject stale initial verdicts before rendering promote controls. - Cover server persistence, SSE payload, and client freshness boundaries. Files changed: .../dashboard/app/hooks/__tests__/useTasks.test.ts | 115 +++++++++++++++++++++ packages/dashboard/app/hooks/useTasks.ts | 80 +++++++++----- .../app/utils/__tests__/releaseGate.test.ts | 9 ++ .../routes-tasks-release-gate-transient.test.ts | 103 ++++++++++++++++++ .../src/__tests__/sse-task-deleted-payload.test.ts | 10 +- packages/dashboard/src/sse.ts | 26 +++-- 6 files changed, 305 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-9029 Fusion-Task-Lineage: cf764f3e-c6f5-4fa1-b608-ebb8d547cfdd Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
59 lines
2.8 KiB
TypeScript
59 lines
2.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
RELEASE_GATE_VERDICT_MAX_AGE_MS,
|
|
isReleaseGateVerdictFresh,
|
|
releaseGateEvidenceFingerprint,
|
|
} from "../releaseGate";
|
|
|
|
const task = {
|
|
id: "FN-8987",
|
|
description: "test",
|
|
column: "todo",
|
|
status: null,
|
|
paused: false,
|
|
workflowIrPin: "builtin:coding",
|
|
enabledWorkflowSteps: undefined,
|
|
workflowStepResults: [],
|
|
steps: [{ name: "Plan", status: "pending" }],
|
|
updatedAt: "2026-08-11T20:00:00.000Z",
|
|
} as any;
|
|
const verdict = {
|
|
promoteBlocked: false,
|
|
unplannedForExecution: false,
|
|
blockedOnApproval: false,
|
|
reason: null,
|
|
readyAtCapacityBoundary: false,
|
|
evaluatedAt: "2026-08-11T20:00:00.000Z",
|
|
evaluatedForUpdatedAt: task.updatedAt,
|
|
} as const;
|
|
|
|
describe("release-gate freshness", () => {
|
|
it("fingerprints every browser-visible release input deterministically", () => {
|
|
const fingerprint = releaseGateEvidenceFingerprint(task);
|
|
expect(releaseGateEvidenceFingerprint({ ...task, enabledWorkflowSteps: ["code-review", "plan-review"] })).toBe(
|
|
releaseGateEvidenceFingerprint({ ...task, enabledWorkflowSteps: ["plan-review", "code-review"] }),
|
|
);
|
|
expect(releaseGateEvidenceFingerprint({ ...task, enabledWorkflowSteps: [] })).not.toBe(fingerprint);
|
|
expect(releaseGateEvidenceFingerprint({ ...task, pausedReason: "awaiting-approval" })).not.toBe(fingerprint);
|
|
expect(releaseGateEvidenceFingerprint({ ...task, workflowStepResults: [{ workflowStepId: "plan-review", status: "passed" }] })).not.toBe(fingerprint);
|
|
});
|
|
|
|
it("drops a verdict when its row clock, evidence, or bounded age becomes unsafe", () => {
|
|
const provenance = { fingerprint: releaseGateEvidenceFingerprint(task), capturedAt: 0 };
|
|
const now = Date.parse(verdict.evaluatedAt);
|
|
expect(isReleaseGateVerdictFresh(verdict, task, provenance, now)).toBe(true);
|
|
expect(isReleaseGateVerdictFresh(verdict, { ...task, status: "planning" }, provenance, now)).toBe(false);
|
|
expect(isReleaseGateVerdictFresh(verdict, { ...task, updatedAt: "2026-08-11T20:00:00.001Z" }, provenance, now)).toBe(false);
|
|
expect(isReleaseGateVerdictFresh(verdict, task, provenance, now + RELEASE_GATE_VERDICT_MAX_AGE_MS)).toBe(true);
|
|
expect(isReleaseGateVerdictFresh(verdict, task, provenance, now + RELEASE_GATE_VERDICT_MAX_AGE_MS + 1)).toBe(false);
|
|
});
|
|
|
|
it("requires local provenance, a parseable timestamp, and the evaluated row clock", () => {
|
|
const provenance = { fingerprint: releaseGateEvidenceFingerprint(task), capturedAt: 0 };
|
|
const now = Date.parse(verdict.evaluatedAt);
|
|
expect(isReleaseGateVerdictFresh(verdict, task, undefined, now)).toBe(false);
|
|
expect(isReleaseGateVerdictFresh({ ...verdict, evaluatedAt: "not-a-date" }, task, provenance, now)).toBe(false);
|
|
expect(isReleaseGateVerdictFresh({ ...verdict, evaluatedForUpdatedAt: undefined }, task, provenance, now)).toBe(false);
|
|
});
|
|
});
|