From 3b61583f1fb4b69944d39a2cd52b8d0a99e94832 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sun, 5 Jul 2026 09:19:18 +0200 Subject: [PATCH] feat(engine): add isPlanEmpty Expose a pure plan-DAG emptiness check for miner and dashboard flows that gate on whether a plan has steps. Co-authored-by: Cursor --- packages/gittensory-engine/src/index.ts | 1 + packages/gittensory-engine/src/plan-empty.ts | 8 ++++ test/unit/plan-empty.test.ts | 41 ++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 packages/gittensory-engine/src/plan-empty.ts create mode 100644 test/unit/plan-empty.test.ts diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index feef006714..d1ef6cba82 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -138,6 +138,7 @@ export { export * from "./plan-export.js"; export { countPlanStepsByStatus } from "./plan-step-stats.js"; export { countPlanSteps } from "./plan-step-count.js"; +export { isPlanEmpty } from "./plan-empty.js"; export { isPlanFullyCompleted } from "./plan-completion.js"; export { hasPlanFailedSteps } from "./plan-failure.js"; export { hasPlanPendingSteps } from "./plan-pending.js"; diff --git a/packages/gittensory-engine/src/plan-empty.ts b/packages/gittensory-engine/src/plan-empty.ts new file mode 100644 index 0000000000..8e6cf53848 --- /dev/null +++ b/packages/gittensory-engine/src/plan-empty.ts @@ -0,0 +1,8 @@ +import type { PlanDag } from "./plan-export.js"; + +/** + * Return whether the plan has no steps. Pure — reads the plan DAG only. + */ +export function isPlanEmpty(plan: PlanDag): boolean { + return plan.steps.length === 0; +} diff --git a/test/unit/plan-empty.test.ts b/test/unit/plan-empty.test.ts new file mode 100644 index 0000000000..cd6ba888e8 --- /dev/null +++ b/test/unit/plan-empty.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { isPlanEmpty } from "../../packages/gittensory-engine/src/plan-empty"; +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("isPlanEmpty", () => { + it("returns true when the plan has no steps", () => { + expect(isPlanEmpty({ steps: [] })).toBe(true); + }); + + it("returns false when the plan has at least one step", () => { + expect(isPlanEmpty({ steps: [step({ id: "a", title: "Build" })] })).toBe(false); + expect( + isPlanEmpty({ + steps: [ + step({ id: "a", title: "Build", status: "completed" }), + step({ id: "b", title: "Test", status: "pending" }), + ], + }), + ).toBe(false); + }); + + it("is exported from the package barrel", async () => { + const barrel = await import("../../packages/gittensory-engine/src/index"); + expect(typeof barrel.isPlanEmpty).toBe("function"); + expect(barrel.isPlanEmpty({ steps: [] })).toBe(true); + expect(barrel.isPlanEmpty({ steps: [step({ id: "a", title: "A" })] })).toBe(false); + }); +});