Files
fusion/packages/core/src/mission-planning-context.integration.test.ts
2026-04-19 10:13:34 -07:00

544 lines
21 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { TaskStore } from "./store.js";
function makeTmpDir(): string {
return mkdtempSync(join(tmpdir(), "kb-mission-planning-"));
}
/**
* MissionStore planning context integration tests verify the enriched triage flow
* that adds mission hierarchy context to task descriptions. These scenarios cover:
* - Full hierarchy context enrichment in task descriptions
* - Omission of empty hierarchy sections
* - Custom description override bypassing enrichment
* - Bulk triage with enrichment
* - Enrichment after interview updates
* - Plan state transitions
*/
describe("MissionStore planning context integration", () => {
let rootDir: string;
let taskStore: TaskStore;
beforeEach(async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-01T00:00:00.000Z"));
rootDir = makeTmpDir();
taskStore = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore.init();
});
afterEach(async () => {
vi.useRealTimers();
await rm(rootDir, { recursive: true, force: true });
});
describe("buildEnrichedDescription", () => {
it("enriches task description with full hierarchy context", async () => {
const missionStore = taskStore.getMissionStore();
// Create full hierarchy with rich context
const mission = missionStore.createMission({
title: "Launch Authentication",
description: "Build a complete auth system",
});
const milestone = missionStore.addMilestone(mission.id, {
title: "Core Auth",
description: "Implement core authentication",
verification: "Users can log in and log out",
planningNotes: "Decided on JWT strategy",
});
const slice = missionStore.addSlice(milestone.id, {
title: "Login Page",
description: "Build the login UI",
verification: "Login form accepts valid credentials",
planningNotes: "Use existing design system",
});
const feature = missionStore.addFeature(slice.id, {
title: "Login Form",
description: "Standard login form with email/password",
acceptanceCriteria: "Form validates input and shows errors",
});
// Build enriched description
const enriched = missionStore.buildEnrichedDescription(feature.id);
expect(enriched).toBeDefined();
// Mission context
expect(enriched).toContain("Launch Authentication");
expect(enriched).toContain("Build a complete auth system");
// Milestone context
expect(enriched).toContain("Core Auth");
expect(enriched).toContain("Implement core authentication");
expect(enriched).toContain("Users can log in and log out");
expect(enriched).toContain("Decided on JWT strategy");
// Slice context
expect(enriched).toContain("Login Page");
expect(enriched).toContain("Build the login UI");
expect(enriched).toContain("Login form accepts valid credentials");
expect(enriched).toContain("Use existing design system");
// Feature context
expect(enriched).toContain("Login Form");
expect(enriched).toContain("Standard login form with email/password");
expect(enriched).toContain("Form validates input and shows errors");
});
it("omits empty hierarchy sections from enriched description", async () => {
const missionStore = taskStore.getMissionStore();
// Create minimal hierarchy
const mission = missionStore.createMission({
title: "Minimal Mission",
description: "Just a title and description",
});
const milestone = missionStore.addMilestone(mission.id, {
title: "Minimal Milestone",
// No description, verification, or planningNotes
});
const slice = missionStore.addSlice(milestone.id, {
title: "Minimal Slice",
// No description, verification, or planningNotes
});
const feature = missionStore.addFeature(slice.id, {
title: "Minimal Feature",
description: "Feature with description only",
// No acceptance criteria
});
const enriched = missionStore.buildEnrichedDescription(feature.id);
expect(enriched).toBeDefined();
// Mission title should be present
expect(enriched).toContain("Minimal Mission");
expect(enriched).toContain("Just a title and description");
// Milestone title should be present but description/verification/notes sections should not be empty
expect(enriched).toContain("Minimal Milestone");
// Should not have empty sections like "Description: undefined"
expect(enriched).not.toMatch(/Description:\s*undefined/);
expect(enriched).not.toMatch(/Verification:\s*undefined/);
expect(enriched).not.toMatch(/Planning Notes:\s*undefined/);
// Feature context
expect(enriched).toContain("Minimal Feature");
expect(enriched).toContain("Feature with description only");
});
it("returns undefined for non-existent feature", async () => {
const missionStore = taskStore.getMissionStore();
const enriched = missionStore.buildEnrichedDescription("non-existent-id");
expect(enriched).toBeUndefined();
});
it("returns undefined when slice is not found", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({ title: "Test Mission" });
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
const feature = missionStore.addFeature(slice.id, { title: "Test Feature" });
// Manually delete the slice to simulate orphan feature
missionStore.deleteSlice(slice.id);
const enriched = missionStore.buildEnrichedDescription(feature.id);
expect(enriched).toBeUndefined();
});
});
describe("triageFeature with enrichment", () => {
it("triageFeature enriches task description with full hierarchy context", async () => {
const missionStore = taskStore.getMissionStore();
// Create full hierarchy
const mission = missionStore.createMission({
title: "Authentication System",
description: "Implement complete auth",
});
const milestone = missionStore.addMilestone(mission.id, {
title: "User Management",
description: "Handle user accounts",
verification: "Users can manage accounts",
planningNotes: "Use PostgreSQL for user data",
});
const slice = missionStore.addSlice(milestone.id, {
title: "User Registration",
description: "Build registration flow",
verification: "Users can register",
planningNotes: "Add email verification",
});
const feature = missionStore.addFeature(slice.id, {
title: "Registration Form",
description: "Create registration form",
acceptanceCriteria: "Form submits successfully",
});
// Triage the feature (no custom description override)
await missionStore.triageFeature(feature.id);
// Get the linked task
const updatedFeature = missionStore.getFeature(feature.id);
expect(updatedFeature?.taskId).toBeDefined();
const task = await taskStore.getTask(updatedFeature!.taskId!);
expect(task.description).toContain("Authentication System");
expect(task.description).toContain("Implement complete auth");
expect(task.description).toContain("User Management");
expect(task.description).toContain("Handle user accounts");
expect(task.description).toContain("Users can manage accounts");
expect(task.description).toContain("Use PostgreSQL for user data");
expect(task.description).toContain("User Registration");
expect(task.description).toContain("Build registration flow");
expect(task.description).toContain("Users can register");
expect(task.description).toContain("Add email verification");
expect(task.description).toContain("Registration Form");
expect(task.description).toContain("Create registration form");
expect(task.description).toContain("Form submits successfully");
});
it("triageFeature with custom description override skips enrichment", async () => {
const missionStore = taskStore.getMissionStore();
// Create full hierarchy
const mission = missionStore.createMission({
title: "Full Mission",
description: "Full mission description",
});
const milestone = missionStore.addMilestone(mission.id, {
title: "Full Milestone",
description: "Full milestone description",
verification: "Full verification",
planningNotes: "Full notes",
});
const slice = missionStore.addSlice(milestone.id, {
title: "Full Slice",
description: "Full slice description",
verification: "Full slice verification",
planningNotes: "Full slice notes",
});
const feature = missionStore.addFeature(slice.id, {
title: "Custom Feature",
description: "Custom feature description",
});
// Triage with custom description override
await missionStore.triageFeature(
feature.id,
undefined, // title uses default
"Custom description override", // description override
);
const updatedFeature = missionStore.getFeature(feature.id);
const task = await taskStore.getTask(updatedFeature!.taskId!);
// Custom description should be used exactly
expect(task.description).toBe("Custom description override");
// Mission context should NOT be present
expect(task.description).not.toContain("Full Mission");
expect(task.description).not.toContain("Full mission description");
expect(task.description).not.toContain("Full Milestone");
});
it("triageSlice enriches all feature tasks with hierarchy context", async () => {
const missionStore = taskStore.getMissionStore();
// Create hierarchy with multiple features
const mission = missionStore.createMission({
title: "Multi Feature Mission",
description: "Testing multiple features",
});
const milestone = missionStore.addMilestone(mission.id, {
title: "Multi Feature Milestone",
description: "Multiple features milestone",
verification: "All features complete",
planningNotes: "Coordinate development",
});
const slice = missionStore.addSlice(milestone.id, {
title: "Multi Feature Slice",
description: "Multiple features slice",
verification: "Slice verification",
planningNotes: "Slice planning",
});
// Add 3 features
const feature1 = missionStore.addFeature(slice.id, {
title: "Feature One",
description: "First feature description",
acceptanceCriteria: "First criterion",
});
const feature2 = missionStore.addFeature(slice.id, {
title: "Feature Two",
description: "Second feature description",
acceptanceCriteria: "Second criterion",
});
const feature3 = missionStore.addFeature(slice.id, {
title: "Feature Three",
description: "Third feature description",
acceptanceCriteria: "Third criterion",
});
// Triage all features in the slice
await missionStore.triageSlice(slice.id);
// Check all 3 tasks have enriched descriptions
for (const feature of [feature1, feature2, feature3]) {
const updatedFeature = missionStore.getFeature(feature.id);
const task = await taskStore.getTask(updatedFeature!.taskId!);
// All tasks should have hierarchy context
expect(task.description).toContain("Multi Feature Mission");
expect(task.description).toContain("Multi Feature Milestone");
expect(task.description).toContain("Multi Feature Slice");
// Each task should have its own feature-specific content
expect(task.description).toContain(feature.title);
expect(task.description).toContain(feature.description!);
expect(task.description).toContain(feature.acceptanceCriteria!);
}
});
it("enriched description reflects updates after interview", async () => {
const missionStore = taskStore.getMissionStore();
// Create initial hierarchy
const mission = missionStore.createMission({
title: "Evolving Mission",
description: "Initial mission",
});
const milestone = missionStore.addMilestone(mission.id, {
title: "Evolving Milestone",
description: "Initial milestone",
planningNotes: "Initial notes",
});
const slice = missionStore.addSlice(milestone.id, {
title: "Evolving Slice",
description: "Initial slice",
planningNotes: "Initial slice notes",
});
const feature1 = missionStore.addFeature(slice.id, {
title: "Feature Alpha",
description: "First feature",
});
const feature2 = missionStore.addFeature(slice.id, {
title: "Feature Beta",
description: "Second feature",
});
// Triage first feature
await missionStore.triageFeature(feature1.id);
const task1 = await taskStore.getTask(missionStore.getFeature(feature1.id)!.taskId!);
// Verify initial enrichment
expect(task1.description).toContain("Initial notes");
expect(task1.description).toContain("Initial slice notes");
// Update milestone and slice after "interview"
missionStore.updateMilestone(milestone.id, {
planningNotes: "Revised milestone planning: Use JWT tokens, add refresh token support",
});
missionStore.updateSlice(slice.id, {
planningNotes: "Revised slice planning: Use React Hook Form, add validation",
});
// Triage second feature
await missionStore.triageFeature(feature2.id);
const task2 = await taskStore.getTask(missionStore.getFeature(feature2.id)!.taskId!);
// Second task should have updated planning notes
expect(task2.description).toContain("Revised milestone planning");
expect(task2.description).toContain("Revised slice planning");
// First task should still have original notes (historical)
expect(task1.description).toContain("Initial notes");
});
});
describe("planState transitions", () => {
it("defaults planState to not_started for new slices", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({ title: "Plan State Test" });
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
expect(slice.planState).toBe("not_started");
});
it("transitions planState to planned after interview", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({ title: "Plan State Test" });
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
// Simulate interview completion by updating planState
const updated = missionStore.updateSlice(slice.id, {
planState: "planned",
planningNotes: "Interview completed with decisions documented",
verification: "All acceptance criteria met",
});
expect(updated.planState).toBe("planned");
expect(updated.planningNotes).toBe("Interview completed with decisions documented");
expect(updated.verification).toBe("All acceptance criteria met");
});
it("transitions planState to needs_update when revisions needed", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({ title: "Plan State Test" });
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
const slice = missionStore.addSlice(milestone.id, {
title: "Test Slice",
});
// Slice should default to not_started
expect(slice.planState).toBe("not_started");
// Simulate interview completion by updating planState
let updated = missionStore.updateSlice(slice.id, {
planState: "planned",
planningNotes: "Interview completed with decisions documented",
verification: "All acceptance criteria met",
});
expect(updated.planState).toBe("planned");
// Simulate requesting updates
updated = missionStore.updateSlice(slice.id, {
planState: "needs_update",
});
expect(updated.planState).toBe("needs_update");
});
it("planState changes do not affect milestone or mission status", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({ title: "Status Test" });
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
const slice = missionStore.addSlice(milestone.id, {
title: "Test Slice",
});
// New missions are "planning" status
expect(mission.status).toBe("planning");
// New milestones are "planning" status
expect(milestone.status).toBe("planning");
expect(slice.status).toBe("pending");
expect(slice.status).toBe("pending");
// Change planState multiple times
missionStore.updateSlice(slice.id, { planState: "planned" });
missionStore.updateSlice(slice.id, { planState: "needs_update" });
missionStore.updateSlice(slice.id, { planState: "planned" });
// Status should remain unchanged
const refreshedMission = missionStore.getMission(mission.id);
const refreshedMilestone = missionStore.getMilestone(milestone.id);
const refreshedSlice = missionStore.getSlice(slice.id);
expect(refreshedMission?.status).toBe("planning");
expect(refreshedMilestone?.status).toBe("planning");
expect(refreshedSlice?.status).toBe("pending");
});
});
describe("milestone interview state integration", () => {
it("milestone interviewState transitions work correctly", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({ title: "Interview Test" });
const milestone = missionStore.addMilestone(mission.id, {
title: "Test Milestone",
});
// interviewState defaults to not_started
expect(milestone.interviewState).toBe("not_started");
// Transition to in_progress
let updated = missionStore.updateMilestone(milestone.id, {
interviewState: "in_progress",
});
expect(updated.interviewState).toBe("in_progress");
// Complete the interview
updated = missionStore.updateMilestone(milestone.id, {
interviewState: "completed",
planningNotes: "Interview completed successfully",
verification: "All requirements captured",
});
expect(updated.interviewState).toBe("completed");
expect(updated.planningNotes).toBe("Interview completed successfully");
expect(updated.verification).toBe("All requirements captured");
// Request update
updated = missionStore.updateMilestone(milestone.id, {
interviewState: "needs_update",
});
expect(updated.interviewState).toBe("needs_update");
});
it("enriched description includes milestone interview state", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({
title: "Interview Context Test",
description: "Mission with interview context",
});
// First create milestone, then update with interview results
const milestone = missionStore.addMilestone(mission.id, {
title: "Interviewed Milestone",
description: "Milestone after interview",
});
// Simulate interview completion
missionStore.updateMilestone(milestone.id, {
interviewState: "completed",
verification: "Verified criteria",
planningNotes: "Key decisions from interview",
});
const slice = missionStore.addSlice(milestone.id, {
title: "Test Slice",
});
const feature = missionStore.addFeature(slice.id, {
title: "Test Feature",
description: "Feature description",
});
const enriched = missionStore.buildEnrichedDescription(feature.id);
expect(enriched).toContain("Interviewed Milestone");
expect(enriched).toContain("Key decisions from interview");
expect(enriched).toContain("Verified criteria");
});
});
});