/** * executionPlanner.test.ts — S415 test suite * * Copre: * - buildExecutionPlan(goal, spec, arch) → ExecutionPlan * - formatExecutionPlan(plan) → string * - formatExecutionPlanCompact(plan) → string * - Ordinamento layer: auth → database → backend → frontend → integration * - Assegnazione reqIds per layer * - isMinimal per goal semplici * - criticalPath esclude integration steps */ import { describe, it, expect } from "vitest"; import { buildExecutionPlan, formatExecutionPlan, formatExecutionPlanCompact } from "../executionPlanner"; import { decomposeGoal } from "../requirementDecomposer"; import { planArchitecture } from "../architecturePlanner"; import type { GoalSpec } from "../requirementDecomposer"; import type { ArchitecturePlan } from "../architecturePlanner"; // ─── helper per costruire spec/arch minimali senza LLM ──────────────────────── function makeSpec(reqs: Array<{ feature: string; priority?: string }>): GoalSpec { return { goal: "test goal", requirements: reqs.map((r, i) => ({ id: `REQ-${String(i + 1).padStart(3, "0")}`, feature: r.feature, priority: (r.priority ?? "high") as "critical" | "high" | "normal", acceptanceCriteria: [`${r.feature} funzionante`], })), complexity: reqs.length >= 4 ? "high" : reqs.length >= 2 ? "medium" : "low", }; } function makeArch(layers: Array): ArchitecturePlan { return { goal: "test goal", nodes: layers.map((layer, i) => ({ id: `node-${i}`, layer, name: `${layer.charAt(0).toUpperCase()}${layer.slice(1)} Service`, tech: "generic", description: "", })), edges: [], entryPoint: layers[0] ?? "frontend", }; } // ─── 1. buildExecutionPlan — ordinamento layer ───────────────────────────────── describe("buildExecutionPlan — ordinamento layer", () => { it("auth → database → backend → frontend (ordine garantito)", () => { const spec = makeSpec([ { feature: "Auth/Login", priority: "critical" }, { feature: "Database schema", priority: "high" }, { feature: "API REST", priority: "high" }, { feature: "Dashboard UI", priority: "normal" }, ]); const arch = makeArch(["auth", "database", "backend", "frontend"]); const plan = buildExecutionPlan("test", spec, arch); const layers = plan.steps.map(s => s.layer); const authIdx = layers.indexOf("auth"); const dbIdx = layers.indexOf("database"); const backendIdx = layers.indexOf("backend"); const frontendIdx = layers.indexOf("frontend"); expect(authIdx).toBeLessThan(dbIdx); expect(dbIdx).toBeLessThan(backendIdx); expect(backendIdx).toBeLessThan(frontendIdx); }); it("integration steps vengono DOPO backend", () => { const spec = makeSpec([ { feature: "Backend API" }, { feature: "Redis cache" }, ]); const arch = makeArch(["backend", "cache"]); const plan = buildExecutionPlan("test", spec, arch); const backendIdx = plan.steps.findIndex(s => s.layer === "backend"); const integrationIdx = plan.steps.findIndex(s => s.layer === "integration"); if (backendIdx >= 0 && integrationIdx >= 0) { expect(backendIdx).toBeLessThan(integrationIdx); } }); it("senza auth nell'arch, ordine inizia da database o backend", () => { const spec = makeSpec([{ feature: "Database schema" }, { feature: "API" }]); const arch = makeArch(["database", "backend"]); const plan = buildExecutionPlan("test", spec, arch); expect(plan.steps[0].layer).toBe("database"); }); }); // ─── 2. buildExecutionPlan — struttura ExecutionPlan ────────────────────────── describe("buildExecutionPlan — struttura output", () => { it("plan ha goal, steps, criticalPath, totalSteps, isMinimal, estimatedComplexity", () => { const spec = makeSpec([{ feature: "Login" }, { feature: "CRUD" }]); const arch = makeArch(["auth", "database"]); const plan = buildExecutionPlan("app con login e CRUD", spec, arch); expect(plan).toHaveProperty("goal"); expect(plan).toHaveProperty("steps"); expect(plan).toHaveProperty("criticalPath"); expect(plan).toHaveProperty("totalSteps"); expect(plan).toHaveProperty("isMinimal"); expect(plan).toHaveProperty("estimatedComplexity"); expect(plan.totalSteps).toBe(plan.steps.length); }); it("ogni step ha order, name, layer, purpose, filesToCreate, verifyHint, deps, isCritical", () => { const spec = makeSpec([{ feature: "Auth" }, { feature: "Database" }]); const arch = makeArch(["auth", "database"]); const plan = buildExecutionPlan("test", spec, arch); for (const step of plan.steps) { expect(step).toHaveProperty("order"); expect(step).toHaveProperty("name"); expect(step).toHaveProperty("layer"); expect(step).toHaveProperty("purpose"); expect(step).toHaveProperty("filesToCreate"); expect(step).toHaveProperty("verifyHint"); expect(step).toHaveProperty("deps"); expect(step).toHaveProperty("isCritical"); expect(Array.isArray(step.filesToCreate)).toBe(true); expect(Array.isArray(step.deps)).toBe(true); } }); it("steps ordinati per order (0, 1, 2...)", () => { const spec = makeSpec([ { feature: "Auth" }, { feature: "DB" }, { feature: "API" }, { feature: "UI" }, ]); const arch = makeArch(["auth", "database", "backend", "frontend"]); const plan = buildExecutionPlan("test", spec, arch); const orders = plan.steps.map(s => s.order); for (let i = 0; i < orders.length; i++) { expect(orders[i]).toBe(i); } }); it("integration step ha isCritical = false", () => { const spec = makeSpec([{ feature: "Redis caching" }]); const arch = makeArch(["cache"]); const plan = buildExecutionPlan("test", spec, arch); const integStep = plan.steps.find(s => s.layer === "integration"); if (integStep) { expect(integStep.isCritical).toBe(false); } }); it("auth step ha isCritical = true", () => { const spec = makeSpec([{ feature: "Login" }]); const arch = makeArch(["auth"]); const plan = buildExecutionPlan("test", spec, arch); const authStep = plan.steps.find(s => s.layer === "auth"); if (authStep) { expect(authStep.isCritical).toBe(true); } }); }); // ─── 3. isMinimal ───────────────────────────────────────────────────────────── describe("buildExecutionPlan — isMinimal", () => { it("piano con 1 step → isMinimal = true", () => { const spec = makeSpec([{ feature: "UI semplice" }]); const arch = makeArch(["frontend"]); const plan = buildExecutionPlan("test", spec, arch); expect(plan.isMinimal).toBe(plan.totalSteps < 2); }); it("piano con ≥2 step → isMinimal = false", () => { const spec = makeSpec([{ feature: "Auth" }, { feature: "Database" }]); const arch = makeArch(["auth", "database"]); const plan = buildExecutionPlan("test", spec, arch); if (plan.totalSteps >= 2) expect(plan.isMinimal).toBe(false); }); }); // ─── 4. estimatedComplexity ──────────────────────────────────────────────────── describe("buildExecutionPlan — estimatedComplexity", () => { it("≥4 step → high", () => { const spec = makeSpec([ { feature: "Auth" }, { feature: "DB" }, { feature: "API" }, { feature: "UI" }, ]); const arch = makeArch(["auth", "database", "backend", "frontend"]); const plan = buildExecutionPlan("test", spec, arch); if (plan.totalSteps >= 4) expect(plan.estimatedComplexity).toBe("high"); }); it("2-3 step → medium", () => { const spec = makeSpec([{ feature: "DB" }, { feature: "API" }]); const arch = makeArch(["database", "backend"]); const plan = buildExecutionPlan("test", spec, arch); if (plan.totalSteps >= 2 && plan.totalSteps < 4) { expect(["medium", "high"]).toContain(plan.estimatedComplexity); } }); }); // ─── 5. criticalPath ────────────────────────────────────────────────────────── describe("buildExecutionPlan — criticalPath", () => { it("criticalPath contiene solo step non-integration", () => { const spec = makeSpec([ { feature: "Auth" }, { feature: "Backend" }, { feature: "Redis" }, ]); const arch = makeArch(["auth", "backend", "cache"]); const plan = buildExecutionPlan("test", spec, arch); for (const idx of plan.criticalPath) { expect(plan.steps[idx]?.isCritical).toBe(true); } }); }); // ─── 6. formatExecutionPlan ─────────────────────────────────────────────────── describe("formatExecutionPlan", () => { it("piano isMinimal → stringa vuota", () => { const plan = buildExecutionPlan("test", makeSpec([{ feature: "UI" }]), makeArch(["frontend"])); if (plan.isMinimal) { expect(formatExecutionPlan(plan)).toBe(""); } }); it("piano con ≥2 step → contiene [STEP N]", () => { const spec = makeSpec([{ feature: "Auth" }, { feature: "Database" }]); const arch = makeArch(["auth", "database"]); const plan = buildExecutionPlan("test", spec, arch); if (!plan.isMinimal) { const text = formatExecutionPlan(plan); expect(text).toMatch(/\[STEP \d\]/); expect(text).toContain("Checkpoint"); expect(text).toContain("REGOLA"); } }); it("output contiene il critical path", () => { const spec = makeSpec([{ feature: "Auth" }, { feature: "Database" }]); const arch = makeArch(["auth", "database"]); const plan = buildExecutionPlan("test", spec, arch); if (!plan.isMinimal) { expect(formatExecutionPlan(plan)).toContain("Critical path"); } }); it("ogni step contiene verifyHint nel testo", () => { const spec = makeSpec([{ feature: "Auth" }, { feature: "API" }]); const arch = makeArch(["auth", "backend"]); const plan = buildExecutionPlan("test", spec, arch); if (!plan.isMinimal) { const text = formatExecutionPlan(plan); expect(text).toMatch(/Verifica:/); } }); }); // ─── 7. formatExecutionPlanCompact ──────────────────────────────────────────── describe("formatExecutionPlanCompact", () => { it("piano isMinimal → stringa vuota", () => { const plan = buildExecutionPlan("test", makeSpec([{ feature: "UI" }]), makeArch(["frontend"])); if (plan.isMinimal) { expect(formatExecutionPlanCompact(plan)).toBe(""); } }); it("piano con ≥2 step → contiene 🗺️ e step names", () => { const spec = makeSpec([{ feature: "Auth" }, { feature: "Database" }]); const arch = makeArch(["auth", "database"]); const plan = buildExecutionPlan("test", spec, arch); if (!plan.isMinimal) { const compact = formatExecutionPlanCompact(plan); expect(compact).toContain("🗺️"); expect(compact).toContain("→"); } }); it("compact è più corto di full", () => { const spec = makeSpec([{ feature: "Auth" }, { feature: "DB" }, { feature: "API" }]); const arch = makeArch(["auth", "database", "backend"]); const plan = buildExecutionPlan("test", spec, arch); if (!plan.isMinimal) { expect(formatExecutionPlanCompact(plan).length).toBeLessThan(formatExecutionPlan(plan).length); } }); }); // ─── 8. Integrazione con decomposeGoal + planArchitecture ───────────────────── describe("buildExecutionPlan — integrazione decomposeGoal + planArchitecture", () => { it("goal 'app con login, database e dashboard' → piano ordinato auth→db→frontend", () => { const goal = "Crea app con login JWT, schema PostgreSQL e dashboard React"; const spec = decomposeGoal(goal); const arch = planArchitecture(goal, spec.requirements); const plan = buildExecutionPlan(goal, spec, arch); expect(plan.steps.length).toBeGreaterThan(0); expect(plan.goal).toBe(goal); // Auth deve venire prima di database se entrambi presenti const layers = plan.steps.map(s => s.layer); const authIdx = layers.indexOf("auth"); const dbIdx = layers.indexOf("database"); if (authIdx >= 0 && dbIdx >= 0) { expect(authIdx).toBeLessThan(dbIdx); } }); it("goal semplice → piano con almeno 1 step", () => { const goal = "Crea una semplice app React"; const spec = decomposeGoal(goal); const arch = planArchitecture(goal, spec.requirements); const plan = buildExecutionPlan(goal, spec, arch); expect(plan.steps.length).toBeGreaterThanOrEqual(1); }); });