Spaces:
Sleeping
Sleeping
| /** | |
| * 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<import("../architecturePlanner").LayerType>): 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); | |
| }); | |
| }); | |