From 2bb2fb7ea26bb4d7e3e59e10e6a86dd89a3defa1 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sun, 5 Jul 2026 08:00:51 +0200 Subject: [PATCH] feat(engine): add isPlanFullyCompleted Expose a pure plan-DAG completion check for miner and dashboard flows that need a single done/not-done signal. Co-authored-by: Cursor --- packages/gittensory-engine/src/index.ts | 1 + .../gittensory-engine/src/plan-completion.ts | 9 +++ test/unit/plan-completion.test.ts | 59 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 packages/gittensory-engine/src/plan-completion.ts create mode 100644 test/unit/plan-completion.test.ts diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 26af2c2973..77a8c321b6 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -120,6 +120,7 @@ export { } from "./governor-ledger.js"; export * from "./plan-export.js"; export { countPlanStepsByStatus } from "./plan-step-stats.js"; +export { isPlanFullyCompleted } from "./plan-completion.js"; export * from "./plan-templates.js"; export * from "./portfolio/queue.js"; export { diff --git a/packages/gittensory-engine/src/plan-completion.ts b/packages/gittensory-engine/src/plan-completion.ts new file mode 100644 index 0000000000..c0a766e278 --- /dev/null +++ b/packages/gittensory-engine/src/plan-completion.ts @@ -0,0 +1,9 @@ +import type { PlanDag } from "./plan-export.js"; + +/** + * Return whether every step in the plan is completed. Empty plans are not considered complete. + * Pure — reads the plan DAG only. + */ +export function isPlanFullyCompleted(plan: PlanDag): boolean { + return plan.steps.length > 0 && plan.steps.every((step) => step.status === "completed"); +} diff --git a/test/unit/plan-completion.test.ts b/test/unit/plan-completion.test.ts new file mode 100644 index 0000000000..5684c90874 --- /dev/null +++ b/test/unit/plan-completion.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; + +import { isPlanFullyCompleted } from "../../packages/gittensory-engine/src/plan-completion"; +import type { PlanStep } from "../../packages/gittensory-engine/src/plan-export"; + +function step(over: Partial & { id: string; title: string }): PlanStep { + return { + actionClass: undefined, + dependsOn: [], + status: "pending", + attempts: 0, + maxAttempts: 3, + lastError: null, + ...over, + }; +} + +describe("isPlanFullyCompleted", () => { + it("returns false for an empty plan", () => { + expect(isPlanFullyCompleted({ steps: [] })).toBe(false); + }); + + it("returns true when every step is completed", () => { + expect( + isPlanFullyCompleted({ + steps: [ + step({ id: "a", title: "Build", status: "completed" }), + step({ id: "b", title: "Test", status: "completed" }), + ], + }), + ).toBe(true); + }); + + it("returns false when any step is not completed", () => { + expect( + isPlanFullyCompleted({ + steps: [ + step({ id: "a", title: "Build", status: "completed" }), + step({ id: "b", title: "Test", status: "pending" }), + ], + }), + ).toBe(false); + expect( + isPlanFullyCompleted({ + steps: [step({ id: "a", title: "Deploy", status: "failed" })], + }), + ).toBe(false); + }); + + it("is exported from the package barrel", async () => { + const barrel = await import("../../packages/gittensory-engine/src/index"); + expect(typeof barrel.isPlanFullyCompleted).toBe("function"); + expect( + barrel.isPlanFullyCompleted({ + steps: [step({ id: "a", title: "A", status: "completed" })], + }), + ).toBe(true); + }); +});