From 676ce7ca71a64296d0385dbeebf0fb1be7d860e1 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sun, 5 Jul 2026 07:53:01 +0200 Subject: [PATCH] feat(engine): add countPlanStepsByStatus Expose a pure plan-DAG helper for counting steps by status in miner and dashboard summaries. Co-authored-by: Cursor --- packages/gittensory-engine/src/index.ts | 1 + .../gittensory-engine/src/plan-step-stats.ts | 8 ++++ test/unit/plan-step-stats.test.ts | 48 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 packages/gittensory-engine/src/plan-step-stats.ts create mode 100644 test/unit/plan-step-stats.test.ts diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 0dede67fc4..26af2c2973 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -119,6 +119,7 @@ export { type NormalizedGovernorLedgerEvent, } from "./governor-ledger.js"; export * from "./plan-export.js"; +export { countPlanStepsByStatus } from "./plan-step-stats.js"; export * from "./plan-templates.js"; export * from "./portfolio/queue.js"; export { diff --git a/packages/gittensory-engine/src/plan-step-stats.ts b/packages/gittensory-engine/src/plan-step-stats.ts new file mode 100644 index 0000000000..b70f28571d --- /dev/null +++ b/packages/gittensory-engine/src/plan-step-stats.ts @@ -0,0 +1,8 @@ +import type { PlanDag, PlanStepStatus } from "./plan-export.js"; + +/** + * Count plan steps matching a given status. Pure — reads the plan DAG only. + */ +export function countPlanStepsByStatus(plan: PlanDag, status: PlanStepStatus): number { + return plan.steps.filter((step) => step.status === status).length; +} diff --git a/test/unit/plan-step-stats.test.ts b/test/unit/plan-step-stats.test.ts new file mode 100644 index 0000000000..adc1f9d750 --- /dev/null +++ b/test/unit/plan-step-stats.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; + +import { countPlanStepsByStatus } from "../../packages/gittensory-engine/src/plan-step-stats"; +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("countPlanStepsByStatus", () => { + it("returns zero for an empty plan", () => { + expect(countPlanStepsByStatus({ steps: [] }, "pending")).toBe(0); + }); + + it("counts only steps that match the requested status", () => { + const plan = { + steps: [ + step({ id: "a", title: "Build", status: "completed" }), + step({ id: "b", title: "Test", status: "pending" }), + step({ id: "c", title: "Deploy", status: "pending" }), + step({ id: "d", title: "Verify", status: "failed" }), + ], + }; + expect(countPlanStepsByStatus(plan, "pending")).toBe(2); + expect(countPlanStepsByStatus(plan, "completed")).toBe(1); + expect(countPlanStepsByStatus(plan, "failed")).toBe(1); + expect(countPlanStepsByStatus(plan, "running")).toBe(0); + }); + + it("is exported from the package barrel", async () => { + const barrel = await import("../../packages/gittensory-engine/src/index"); + expect(typeof barrel.countPlanStepsByStatus).toBe("function"); + expect( + barrel.countPlanStepsByStatus( + { steps: [step({ id: "a", title: "A", status: "skipped" })] }, + "skipped", + ), + ).toBe(1); + }); +});