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>
30 lines
1.6 KiB
TypeScript
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);
|
|
});
|
|
});
|