Files
fusion/packages/dashboard/app/utils/__tests__/releaseGate.contract.test.ts
gsxdsm a6ce7f89ef FN-8987: expose release-gate verdicts for Promote visibility
Expose server-evaluated release-gate state so Promote controls accurately reflect approval readiness.

- Attach transient release-gate verdicts to task API responses.
- Preserve only fresh REST verdicts across task snapshots and render them in task cards.
- Align hold-release gating and document the Promote-state contract.

Files changed:
 .changeset/fn-8987-release-gate-verdict.md         |   7 +
 docs/dashboard-guide.md                            |   4 +
 docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md |   1 +
 docs/workflow-steps.md                             |   4 +
 packages/core/src/index.gate.ts                    |   2 +-
 packages/core/src/index.ts                         |   2 +-
 packages/core/src/types.ts                         |   2 +
 packages/core/src/types/task/task-core.ts          |  20 +++
 packages/dashboard/app/components/TaskCard.tsx     |   3 +-
 packages/dashboard/app/components/__tests__/TaskCard.test.tsx |  31 ++++
 packages/dashboard/app/hooks/__tests__/useTasks.test.ts |  26 ++++
 packages/dashboard/app/hooks/useTasks.ts           | 135 +++++++++++++++--
 packages/dashboard/app/utils/__tests__/releaseGate.contract.test.ts |  29 ++++
 packages/dashboard/app/utils/__tests__/releaseGate.test.ts |  49 ++++++
 packages/dashboard/app/utils/releaseGate.ts        |  26 ++++
 packages/dashboard/app/utils/reviewBudgetApproval.ts |   9 ++
 packages/dashboard/src/routes/register-task-workflow-routes.ts |  20 ++-
 packages/engine/src/__tests__/release-gate-verdict.test.ts |  40 +++++
 packages/engine/src/execution/hold-release.ts      | 166 +++++++++------------
 packages/engine/src/index.ts                       |   3 +
 20 files changed, 469 insertions(+), 110 deletions(-)

Fusion-Task-Id: FN-8987

Fusion-Task-Lineage: 6f1742bc-2b2b-4a32-9be5-92160335d90d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-11 14:22:14 -07:00

30 lines
1.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { evaluateTaskReleaseGate } from "@fusion/engine";
import type { WorkflowIr } from "@fusion/core";
import { resolvePromoteSuppressed } from "../reviewBudgetApproval";
import { isReleaseGateVerdictFresh, releaseGateEvidenceFingerprint } from "../releaseGate";
const ir: WorkflowIr = {
version: "v2", name: "release-gate-contract", columns: [
{ id: "todo", name: "Todo", traits: [{ trait: "hold", config: { release: "capacity" } }] },
{ id: "in-progress", name: "In progress", traits: [{ trait: "wip" }] },
],
nodes: [{ id: "start", kind: "start", column: "todo" }], edges: [],
} as WorkflowIr;
describe("release-gate client/server contract", () => {
it("uses the engine verdict verbatim and refuses stale evidence", async () => {
const task = {
id: "FN-8987-contract", title: "Specified task", description: "Specified task", column: "todo",
dependencies: [], status: null, enabledWorkflowSteps: undefined,
updatedAt: "2026-08-11T20:00:00.000Z",
} as any;
const verdict = await evaluateTaskReleaseGate({ getTasksDir: () => "/missing" } as any, task, { ir });
expect(verdict).toBeDefined();
expect(resolvePromoteSuppressed({ releaseGate: verdict }, true)).toBe(verdict!.promoteBlocked);
const provenance = { fingerprint: releaseGateEvidenceFingerprint(task), capturedAt: Date.now() };
expect(isReleaseGateVerdictFresh(verdict!, task, provenance, Date.parse(verdict!.evaluatedAt))).toBe(true);
expect(isReleaseGateVerdictFresh(verdict!, { ...task, updatedAt: "2026-08-11T20:00:00.001Z" }, provenance, Date.parse(verdict!.evaluatedAt))).toBe(false);
});
});