import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClosableStateStores } from "../../src/infrastructure/adapters/sqlite/create-state-stores.js"; import { createClosableStateStoresUnchecked } from "../../src/infrastructure/adapters/sqlite/create-state-stores.js"; describe("slice-close completeness invariant is wired", () => { let stores: ClosableStateStores; let sliceId: string; const driveToShipping = (id: string) => { const path = [ "discussing", "researching", "planning", "executing", "verifying", "reviewing", "shipping", ] as const; for (const target of path) { stores.sliceStore.transitionSlice(id, target); } }; beforeEach(() => { stores = createClosableStateStoresUnchecked(":memory:"); // Seed: project → milestone → slice → task claimed by "exec-A" → drive to shipping → seed approved code + security reviews. stores.projectStore.saveProject({ name: "Test Project" }); stores.milestoneStore.createMilestone({ number: 1, name: "Milestone One" }); const msResult = stores.milestoneStore.listMilestones(); if (!msResult.ok || msResult.data.length === 0) throw new Error("No milestones seeded"); const milestoneId = msResult.data[0].id; const sliceResult = stores.sliceStore.createSlice({ milestoneId, number: 1, title: "Slice One", }); if (!sliceResult.ok) throw new Error("Failed to create slice"); sliceId = sliceResult.data.id; const taskResult = stores.taskStore.createTask({ sliceId, number: 1, title: "Task One", }); if (!taskResult.ok) throw new Error("Failed to create task"); const taskId = taskResult.data.id; // Claim task with executor "exec-A" — distinct from reviewers "rev-code" / "rev-sec" // so the D-3 fresh-reviewer invariant does not fire. const claimResult = stores.taskStore.claimTask(taskId, "exec-A"); if (!claimResult.ok) throw new Error("Failed to claim task"); // Seed a review first so the verifying → reviewing transition guard passes. const preReviewResult = stores.reviewStore.recordReview({ sliceId, reviewer: "rev-pre", type: "spec", verdict: "approved", commitSha: "abc", createdAt: new Date().toISOString(), }); if (!preReviewResult.ok) throw new Error("Failed to record pre-review"); // Drive the primary slice to `shipping` so close is a legal next transition. driveToShipping(sliceId); // Seed both approved code + security reviews so close succeeds (so we observe the listReviews call). const codeReviewResult = stores.reviewStore.recordReview({ sliceId, reviewer: "rev-code", type: "code", verdict: "approved", commitSha: "abc", createdAt: new Date().toISOString(), }); if (!codeReviewResult.ok) throw new Error("Failed to record code review"); const securityReviewResult = stores.reviewStore.recordReview({ sliceId, reviewer: "rev-sec", type: "security", verdict: "approved", commitSha: "abc", createdAt: new Date().toISOString(), }); if (!securityReviewResult.ok) throw new Error("Failed to record security review"); }); afterEach(() => stores.close()); it("transitionSlice(..., 'closed') calls listReviews(sliceId)", () => { const spy = vi.spyOn(stores.reviewStore, "listReviews"); stores.sliceStore.transitionSlice(sliceId, "closed"); expect(spy).toHaveBeenCalledWith(sliceId); spy.mockRestore(); }); it("transitionSlice to a non-terminal target does not call listReviews", () => { // Create a second slice in `created`; transition to `discussing`. const msResult = stores.milestoneStore.listMilestones(); if (!msResult.ok || msResult.data.length === 0) throw new Error("No milestones found"); const milestoneId = msResult.data[0].id; const slice2Result = stores.sliceStore.createSlice({ milestoneId, number: 2, title: "Slice Two", }); if (!slice2Result.ok) throw new Error("Failed to create second slice"); const slice2Id = slice2Result.data.id; // Spy BEFORE the transition call. const spy = vi.spyOn(stores.reviewStore, "listReviews"); stores.sliceStore.transitionSlice(slice2Id, "discussing"); // Assert the spy was NOT called for this new slice id. expect(spy).not.toHaveBeenCalledWith(slice2Id); spy.mockRestore(); }); it("transitionSlice(..., 'closed') rejects when required reviews are missing", () => { // Create a second slice with a claimed task, drive to shipping, but do NOT seed reviews. const msResult = stores.milestoneStore.listMilestones(); if (!msResult.ok || msResult.data.length === 0) throw new Error("No milestones found"); const milestoneId = msResult.data[0].id; const slice2Result = stores.sliceStore.createSlice({ milestoneId, number: 2, title: "Slice Two", }); if (!slice2Result.ok) throw new Error("Failed to create second slice"); const slice2Id = slice2Result.data.id; const task2Result = stores.taskStore.createTask({ sliceId: slice2Id, number: 1, title: "Task Two", }); if (!task2Result.ok) throw new Error("Failed to create task for second slice"); const claim2Result = stores.taskStore.claimTask(task2Result.data.id, "exec-B"); if (!claim2Result.ok) throw new Error("Failed to claim task for second slice"); // Seed a review so the verifying → reviewing transition guard passes. const preReview2Result = stores.reviewStore.recordReview({ sliceId: slice2Id, reviewer: "rev-pre", type: "spec", verdict: "approved", commitSha: "abc", createdAt: new Date().toISOString(), }); if (!preReview2Result.ok) throw new Error("Failed to record pre-review for slice two"); // Drive to shipping without seeding code/security reviews. driveToShipping(slice2Id); const result = stores.sliceStore.transitionSlice(slice2Id, "closed"); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.errorLabel).toBe("SHIP_COMPLETENESS_VIOLATION"); }); });