@@ -1,665 +1,353 @@
/**
import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
* Mission Integration Tests
import { mkdtempSync } from "node:fs" ;
*
import { rm } from "node:fs/promises" ;
* Comprehensive integration tests for MissionStore working with TaskStore.
* Tests mission hierarchy, status rollup, cascade operations, and event emissions.
*/
import { describe , it , expect , vi , beforeEach , afterEach } from "vitest" ;
import { mkdtempSync , rmSync } from "node:fs" ;
import { tmpdir } from "node:os" ;
import { join } from "node:path" ;
import { join } from "node:path" ;
import { tmpdir } from "node:os" ;
import { TaskStore } from "./store.js" ;
import { TaskStore } from "./store.js" ;
import { MissionStor e } from "./mission-store .js" ;
import { Databas e } from "./db .js" ;
import type { Mission , Milestone , Slice , MissionFeature } from "./mission-types.js" ;
// Helper to create temp directory
function makeTmpDir ( ) : string {
function createTe mpD ir( ) : string {
return mkdtempSync ( join ( t mpd ir ( ) , "kb-mission-integration-" ) ) ;
return mkdtempSync ( join ( tmpdir ( ) , "mission-integration-" ) ) ;
}
}
// Helper to cleanup temp directory
function getPrivateDb ( store : TaskStore ) : Database | null {
function cleanupTempDir ( dir : string ) : void {
return ( store as unknown as { _db : Database | null } ) . _db ;
try {
}
rmSync ( dir , { recursive : true , force : true } ) ;
} catch {
function assertHierarchyIntegrity (
// Ignore cleanup errors
hierarchy : NonNullable < ReturnType < ReturnType < TaskStore [ "getMissionStore" ] > [ "getMissionWithHierarchy" ] > > ,
) {
expect ( hierarchy . milestones . every ( ( milestone , index ) = > milestone . orderIndex === index ) ) . toBe ( true ) ;
expect ( new Set ( hierarchy . milestones . map ( ( milestone ) = > milestone . id ) ) . size ) . toBe ( hierarchy . milestones . length ) ;
for ( const milestone of hierarchy . milestones ) {
expect ( milestone . slices . every ( ( slice , index ) = > slice . orderIndex === index ) ) . toBe ( true ) ;
expect ( new Set ( milestone . slices . map ( ( slice ) = > slice . id ) ) . size ) . toBe ( milestone . slices . length ) ;
for ( const slice of milestone . slices ) {
expect ( slice . milestoneId ) . toBe ( milestone . id ) ;
expect ( new Set ( slice . features . map ( ( feature ) = > feature . id ) ) . size ) . toBe ( slice . features . length ) ;
for ( const feature of slice . features ) {
expect ( feature . sliceId ) . toBe ( slice . id ) ;
}
}
}
}
}
}
describe ( "Mission Integration" , ( ) = > {
/**
let tempDir : string ;
* Creates a mission hierarchy large enough to exercise rollups, reorder logic,
* and cascade deletions in integration scenarios.
*/
async function createHierarchy ( store : TaskStore ) {
const missionStore = store . getMissionStore ( ) ;
const mission = missionStore . createMission ( {
title : "Launch authentication" ,
description : "Mission hierarchy integration test" ,
} ) ;
const milestones = Array . from ( { length : 3 } , ( _ , milestoneIndex ) = > {
const milestone = missionStore . addMilestone ( mission . id , {
title : ` Milestone ${ milestoneIndex + 1 } ` ,
description : ` Phase ${ milestoneIndex + 1 } ` ,
} ) ;
const slices = Array . from ( { length : 2 } , ( _ , sliceIndex ) = > {
const slice = missionStore . addSlice ( milestone . id , {
title : ` Slice ${ milestoneIndex + 1 } . ${ sliceIndex + 1 } ` ,
description : ` Slice ${ milestoneIndex + 1 } . ${ sliceIndex + 1 } ` ,
} ) ;
const features = Array . from ( { length : 3 } , ( _ , featureIndex ) = >
missionStore . addFeature ( slice . id , {
title : ` Feature ${ milestoneIndex + 1 } . ${ sliceIndex + 1 } . ${ featureIndex + 1 } ` ,
description : "Feature description" ,
acceptanceCriteria : "criterion" ,
} ) ,
) ;
return { . . . slice , features } ;
} ) ;
return { . . . milestone , slices } ;
} ) ;
return { missionStore , mission , milestones } ;
}
/**
* MissionStore integration tests verify the missions hierarchy when it shares
* the same SQLite database as TaskStore. These scenarios cover linking tasks
* to features, rollup state transitions, hierarchy integrity after reorders and
* deletions, foreign-key cleanup, and event emissions that other packages rely on.
*/
describe ( "MissionStore integration with TaskStore" , ( ) = > {
let rootDir : string ;
let taskStore : TaskStore ;
let taskStore : TaskStore ;
let missionStore : MissionStore ;
beforeEach ( async ( ) = > {
beforeEach ( async ( ) = > {
tempDir = createTempDir ( ) ;
vi . useFakeTimers ( ) ;
taskStore = new TaskStore ( tempDir ) ;
vi . setSystemTime ( new Date ( "2026-04-01T00:00:00.000Z" ) ) ;
rootDir = makeTmpDir ( ) ;
taskStore = new TaskStore ( rootDir ) ;
await taskStore . init ( ) ;
await taskStore . init ( ) ;
missionStore = taskStore . getMissionStore ( ) ;
} ) ;
} ) ;
afterEach ( ( ) = > {
afterEach ( async ( ) = > {
cleanupTempDir ( tempDir ) ;
vi . useRealTimers ( ) ;
await rm ( rootDir , { recursive : true , force : true } ) ;
} ) ;
} ) ;
describe ( "Mission Creation and Hierarchy" , ( ) = > {
it ( "creates and retrieves a full hierarchy through the shared MissionStore" , async ( ) = > {
it ( "should create mission with complete h ierarchy" , ( ) = > {
const { missionStore , mission } = await createH ierarchy( taskStore ) ;
// Create mission
const mission = missionStore . createMission ( {
title : "Build Auth System" ,
description : "Complete authentication system with login, signup, and password reset" ,
} ) ;
expect ( mission . id ) . toMat ch ( /^M-/ ) ;
const fullMission = missionStore . getMissionWithHierar chy ( mission . id ) ;
expect ( mission . title ) . toBe ( "Build Auth System" ) ;
expect ( mission . status ) . toBe ( "planning" ) ;
expect ( mission . interviewState ) . toBe ( "not_started" ) ;
// Add milestones
expect ( fullMission ) . toBeDefined ( ) ;
const milestone1 = m issionStore . addM ilestone( mission . id , {
expect ( fullM ission? . m ilestones ) . toHaveLength ( 3 ) ;
title : "Database Schema" ,
expect ( fullMission ? . milestones . every ( ( milestone ) = > milestone . slices . length === 2 ) ) . toBe ( true ) ;
description : "Design and implement database tables" ,
expect (
} ) ;
fullMission ? . milestones . every ( ( milestone ) = >
milestone . slices . every ( ( slice ) = > {
const hierarchySlice = slice as typeof slice & { features : Array < { id : string } > } ;
return hierarchySlice . features . length === 3 ;
} ) ,
) ,
) . toBe ( true ) ;
} ) ;
const mil estone2 = missionStore . addMilestone ( mission . id , {
it ( "links featur es to real TaskStore tasks and updates sliceId without populating missionId" , async ( ) = > {
title : "API Endpoints" ,
const { missionStore , mission , milestones } = await createHierarchy ( taskStore ) ;
description : "Build REST API endpoints" ,
const feature = milestones [ 0 ] . slices [ 0 ] . features [ 0 ] ;
} ) ;
expect ( milestone1 . orderIndex ) . toBe ( 0 ) ;
const linkedTask = await taskStore . createTask ( {
expect ( milestone2 . orderIndex ) . toBe ( 1 ) ;
title : "Build login form" ,
description : "Implement the login form task used for mission linking." ,
// Add slices to first milestone
column : "todo" ,
const slice1 = missionStore . addSlice ( milestone1 . id , {
title : "User Tables" ,
description : "Create user and session tables" ,
} ) ;
const slice2 = missionStore . addSlice ( milestone1 . id , {
title : "Token Storage" ,
description : "Implement refresh token storage" ,
} ) ;
expect ( slice1 . orderIndex ) . toBe ( 0 ) ;
expect ( slice2 . orderIndex ) . toBe ( 1 ) ;
// Add features to first slice
const feature1 = missionStore . addFeature ( slice1 . id , {
title : "User model" ,
description : "Define user database schema" ,
acceptanceCriteria : "Users can be created with email and password hash" ,
} ) ;
const feature2 = missionStore . addFeature ( slice1 . id , {
title : "Session table" ,
description : "Create session management table" ,
} ) ;
expect ( feature1 . status ) . toBe ( "defined" ) ;
expect ( feature2 . status ) . toBe ( "defined" ) ;
// Verify full hierarchy
const fullMission = missionStore . getMissionWithHierarchy ( mission . id ) ;
expect ( fullMission ) . toBeDefined ( ) ;
expect ( fullMission ! . milestones ) . toHaveLength ( 2 ) ;
expect ( fullMission ! . milestones [ 0 ] . slices ) . toHaveLength ( 2 ) ;
expect ( fullMission ! . milestones [ 0 ] . slices [ 0 ] . features ) . toHaveLength ( 2 ) ;
} ) ;
} ) ;
it ( "should compute orderIndex correctly for multiple items" , ( ) = > {
// TaskStore persists tasks to disk first, so create a DB-backed snapshot
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
// using a normal update path before MissionStore writes the linkage field.
await taskStore . moveTask ( linkedTask . id , "in-progress" ) ;
// Add 3 milestones
const linkedFeature = missionStore . linkFeatureToTask ( feature . id , linkedTask . id ) ;
const ms1 = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const storedTask = await taskStore . getTask ( linkedTask . id ) ;
const ms2 = missionStore . addMiles ton e( mission . id , { title : "Milestone 2" } ) ;
const taskRow = getPrivateDb ( taskS tor e) ? . prepare (
const ms3 = missionStore . addMilestone ( mission . id , { title : "Milestone 3" } ) ;
"SELECT missionId, sliceId FROM tasks WHERE id = ?" ,
) . get ( linkedTask . id ) as { missionId : string | null ; sliceId : string | null } | undefined ;
expect ( ms1 . orderIndex ) . toBe ( 0 ) ;
expect ( linkedFeature . taskId ) . toBe ( linkedTask . id ) ;
expect ( ms2 . orderIndex ) . toBe ( 1 ) ;
expect ( linkedFeature . status ) . toBe ( "triaged" ) ;
expect ( ms3 . orderIndex ) . toBe ( 2 ) ;
expect ( storedTask . sliceId ) . toBe ( milestones [ 0 ] . slices [ 0 ] . id ) ;
expect ( taskRow ? . sliceId ) . toBe ( milestones [ 0 ] . slices [ 0 ] . id ) ;
expect ( taskRow ? . missionId ) . toBeNull ( ) ;
// Add slices to first milestone
const linkedHierarchy = missionStore . getMissionWithHierarchy ( mission . id ) ;
const sl1 = missionStore . addS lice( ms1 . id , { title : "Slice 1" } ) ;
expect ( linkedHierarchy ? . milestones [ 0 ] . s lices [ 0 ] . features [ 0 ] . taskId ) . toBe ( linkedTask . id ) ;
const sl2 = missionStore . addSlice ( ms1 . id , { title : "Slice 2" } ) ;
expect ( sl1 . orderIndex ) . toBe ( 0 ) ;
expect ( sl2 . orderIndex ) . toBe ( 1 ) ;
} ) ;
} ) ;
} ) ;
describe ( "Task Linking and Feature Status" , ( ) = > {
it ( "rolls up status from features to slices, milestones, and mission" , async ( ) = > {
it ( "should update feature status when linked to task" , async ( ) = > {
const { missionStore , mission , milestones } = await createHierarchy ( taskStore ) ;
const mission = missionStore . createMissi on( { title : "Test Mission" } ) ;
const [ firstMilest one ] = milestones ;
const milestone = missionStore . add Milestone( mission . id , { title : "Milestone 1" } ) ;
const [ firstSlice ] = first Milestone. slices ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
// Create a task
const linkedFeatures : { featureId : string ; taskId : string } [ ] = [ ] ;
for ( const feature of firstSlice . features ) {
const task = await taskStore . createTask ( {
const task = await taskStore . createTask ( {
description : "Implement feature" ,
title : feature.title ,
title : "Feature implementation" ,
description : ` Task for ${ feature . title } ` ,
} ) ;
column : "todo" ,
// Link feature to task
const updatedFeature = missionStore . linkFeatureToTask ( feature . id , task . id ) ;
expect ( updatedFeature . taskId ) . toBe ( task . id ) ;
expect ( updatedFeature . status ) . toBe ( "triaged" ) ;
} ) ;
it ( "should update slice status when feature is linked" , async ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
// Slice starts as pending
expect ( slice . status ) . toBe ( "pending" ) ;
// Create and link task
const task = await taskStore . createTask ( {
description : "Implement feature" ,
title : "Feature implementation" ,
} ) ;
} ) ;
await taskStore . moveTask ( task . id , "in-progress" ) ;
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
linkedFeatures . push ( { featureId : feature.id , taskId : task.id } ) ;
}
// Slice should now be active
let updatedSlice = missionStore . getSlice ( firstSlice . id ) ;
cons t updatedSlic e = missionStore . getSlice ( slic e . id ) ;
le t updatedMileston e = missionStore . getMilestone ( firstMileston e . id ) ;
expect ( updatedSlice ! . status ) . toBe ( "active" ) ;
let updatedMission = missionStore . getMission ( mission . id ) ;
} ) ;
it ( "should find feature by task ID" , async ( ) = > {
expect ( updatedSlice ? . status ) . toBe ( "active" ) ;
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
expect ( updatedMilestone ? . status ) . toBe ( "active" ) ;
const milestone = missionStore . addMilestone ( m ission. id , { title : "Milestone 1" } ) ;
expect ( updatedM ission? . status ) . toBe ( "active" ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
const task = await taskStore . createTask ( {
for ( const { featureId , taskId } of linkedFeatures ) {
description : "Implement feature" ,
await taskStore . moveTask ( taskId , "in-review" ) ;
title : "Feature implementati on" ,
await taskStore . moveTask ( taskId , "d one ") ;
} ) ;
missionStore . updateFeature ( featureId , { taskId , status : "done" } ) ;
}
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
updatedSlice = missionStore . getSlice ( firstSlice . id ) ;
updatedMilestone = missionStore . getMilestone ( firstMilestone . id ) ;
updatedMission = missionStore . getMission ( mission . id ) ;
const found = missionStore . getFeatureByTaskId ( task . id ) ;
expect ( updatedSlice ? . status ) . toBe ( "complete" ) ;
expect ( found ) . toBeDefined ( ) ;
expect ( updatedMilestone ? . status ) . toBe ( "active" ) ;
expect ( found ! . id ) . toBe ( feature . id ) ;
expect ( updatedMission ? . status ) . toBe ( "active" ) ;
} ) ;
it ( "should set task.sliceId when linking feature to task" , async ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
const task = await taskStore . createTask ( {
description : "Implement feature" ,
title : "Feature implementation" ,
} ) ;
// Link feature to task
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
// Reload task and verify sliceId was set
const reloaded = await taskStore . getTask ( task . id ) ;
expect ( reloaded . sliceId ) . toBe ( slice . id ) ;
} ) ;
it ( "should clear task.sliceId when unlinking feature from task" , async ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
const task = await taskStore . createTask ( {
description : "Implement feature" ,
title : "Feature implementation" ,
} ) ;
// Link and then unlink
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
missionStore . unlinkFeatureFromTask ( feature . id ) ;
// Reload task and verify sliceId was cleared
const reloaded = await taskStore . getTask ( task . id ) ;
expect ( reloaded . sliceId ) . toBeUndefined ( ) ;
} ) ;
} ) ;
} ) ;
describe ( "Status Rollup" , ( ) = > {
it ( "cascades mission deletion across milestones, slices, and features" , async ( ) = > {
it ( "should compute slice status based on features" , async ( ) = > {
const { missionStore , mission , milestones } = await createHierarchy ( taskStore ) ;
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestoneIds = milestones . map ( ( milestone ) = > milestone . id ) ;
const milestone = missionStore . addMilestone ( missi on. id , { title : "Milestone 1" } ) ;
const sliceIds = milestones . flatMap ( ( milestone ) = > milest one . slices . map ( ( slice ) = > slice . id ) ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const featureIds = milestones . flatMap ( ( milestone ) = >
milestone . slices . flatMap ( ( slice ) = > slice . features . map ( ( feature ) = > feature . id ) ) ,
) ;
// Empty slice should be pending
missionStore . deleteMission ( mission . id ) ;
expect ( missionStore . computeSliceStatus ( slice . id ) ) . toBe ( "pending" ) ;
// Add features
expect ( missionStore . getMission ( mission . id ) ) . toBeUndefined ( ) ;
const f1 = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
expect ( milestoneIds . every ( ( id ) = > missionStore . getMilestone ( id ) === undefined ) ) . toBe ( true ) ;
const f2 = missionStore . addFeature ( slice . id , { title : "Feature 2" } ) ;
expect ( sliceIds . every ( ( id ) = > missionStore . getSlice ( id ) === undefined ) ) . toBe ( true ) ;
expect ( featureIds . every ( ( id ) = > missionStore . getFeature ( id ) === undefined ) ) . toBe ( true ) ;
// Still pending (no tasks linked)
expect ( missionStore . computeSliceStatus ( slice . id ) ) . toBe ( "pending" ) ;
// Link f1 to a task (makes it triaged/active)
const task1 = await taskStore . createTask ( {
description : "Implement feature 1" ,
title : "Feature 1 implementation" ,
} ) ;
missionStore . linkFeatureToTask ( f1 . id , task1 . id ) ;
// Should now be active since f1 has a task link
expect ( missionStore . computeSliceStatus ( slice . id ) ) . toBe ( "active" ) ;
// Mark both as done
missionStore . updateFeatureStatus ( f1 . id , "done" ) ;
missionStore . updateFeatureStatus ( f2 . id , "done" ) ;
// Should be complete
expect ( missionStore . computeSliceStatus ( slice . id ) ) . toBe ( "complete" ) ;
} ) ;
it ( "should compute milestone status based on slices" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
// Empty milestone should be planning
expect ( missionStore . computeMilestoneStatus ( milestone . id ) ) . toBe ( "planning" ) ;
// Add slices
const s1 = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const s2 = missionStore . addSlice ( milestone . id , { title : "Slice 2" } ) ;
// Still planning (all slices pending)
expect ( missionStore . computeMilestoneStatus ( milestone . id ) ) . toBe ( "planning" ) ;
// Activate first slice
missionStore . activateSlice ( s1 . id ) ;
// Should be active
expect ( missionStore . computeMilestoneStatus ( milestone . id ) ) . toBe ( "active" ) ;
// Complete first slice by marking features done
const f1 = missionStore . addFeature ( s1 . id , { title : "Feature 1" } ) ;
missionStore . updateFeatureStatus ( f1 . id , "done" ) ;
// Activate and complete second slice
missionStore . activateSlice ( s2 . id ) ;
const f2 = missionStore . addFeature ( s2 . id , { title : "Feature 2" } ) ;
missionStore . updateFeatureStatus ( f2 . id , "done" ) ;
// Milestone should be complete
const updatedMilestone = missionStore . getMilestone ( milestone . id ) ;
expect ( updatedMilestone ! . status ) . toBe ( "complete" ) ;
} ) ;
it ( "should compute mission status based on milestones" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
// Empty mission should be planning
expect ( missionStore . computeMissionStatus ( mission . id ) ) . toBe ( "planning" ) ;
// Add milestones
const m1 = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const m2 = missionStore . addMilestone ( mission . id , { title : "Milestone 2" } ) ;
// Still planning
expect ( missionStore . computeMissionStatus ( mission . id ) ) . toBe ( "planning" ) ;
// Complete first milestone via slice activation and feature completion
const s1 = missionStore . addSlice ( m1 . id , { title : "Slice 1" } ) ;
const f1 = missionStore . addFeature ( s1 . id , { title : "Feature 1" } ) ;
missionStore . activateSlice ( s1 . id ) ;
missionStore . updateFeatureStatus ( f1 . id , "done" ) ;
// Mission should be active (one milestone active/complete)
expect ( missionStore . computeMissionStatus ( mission . id ) ) . toBe ( "active" ) ;
// Complete second milestone
const s2 = missionStore . addSlice ( m2 . id , { title : "Slice 2" } ) ;
const f2 = missionStore . addFeature ( s2 . id , { title : "Feature 2" } ) ;
missionStore . activateSlice ( s2 . id ) ;
missionStore . updateFeatureStatus ( f2 . id , "done" ) ;
// Mission should be complete
const updatedMission = missionStore . getMission ( mission . id ) ;
expect ( updatedMission ! . status ) . toBe ( "complete" ) ;
} ) ;
} ) ;
} ) ;
describe ( "Cascade Delete" , ( ) = > {
it ( "recomputes order indexes after deleting a middle milestone and preserves child integrity after reorder" , async ( ) = > {
it ( "should delete mission and all children" , ( ) = > {
const { missionStore , mission , milestones } = await createHierarchy ( taskStore ) ;
const mission = missionStore . createMissi on( { title : "Test Mission" } ) ;
const [ firstMilestone , middleMilestone , lastMilest one ] = milestones ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
// Delete mission
missionStore . deleteMilestone ( middleMilestone . id ) ;
missionStore . deleteMission ( mission . id ) ;
// Everything should be gone
const afterDelete = missionStore . listMilestones ( mission . id ) ;
expect ( missionStore . getMissi on( missi on. id ) ) . toBeUndefine d ( ) ;
expect ( afterDelete . map ( ( milest one ) = > milest one . id ) ) . toEqual ( [ firstMilestone . id , lastMilestone . i d ] ) ;
expect ( missionStore . get Milestone( milest one . id ) ) . toBeUndefine d( ) ;
missionStore . reorder Milestones ( missi on . id , [ firstMilestone . id , lastMilestone . i d] ) ;
expect ( missionStore . getSlice ( slice . id ) ) . toBeUndefined ( ) ;
expect ( missionStore . getFeature ( feature . id ) ) . toBeUndefined ( ) ;
} ) ;
it ( "should delete milestone and its slices/featur es" , ( ) = > {
const afterRecompute = missionStore . listMileston es( mission . id ) ;
const mission = missionS tor e . createMission ( { title : "Test Mission" } ) ;
expect ( afterRecompute . map ( ( milestone ) = > miles ton e . orderIndex ) ) . toEqual ( [ 0 , 1 ] ) ;
const m1 = missionStore . add Milestone ( mission . id , { title : "Milestone 1" } ) ;
expect ( missionStore . getSlice ( middle Milestone. slices [ 0 ] . id ) ) . toBeUndefined ( ) ;
const m2 = missionStore . add Milestone ( mission . id , { title : "Milestone 2" } ) ;
expect ( missionStore . getFeature ( middle Milestone. slices [ 0 ] . features [ 0 ] . id ) ) . toBeUndefined ( ) ;
const slice = missionStore . addSlice ( m1 . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
// Delete first m ilestone
missionStore . reorderMilestones ( mission . id , [ lastMilestone . id , firstM ilestone. id ] ) ;
missionStore . del ete Milestone ( m1 . id ) ;
const reordered = missionStore . g etMissionWithHierarchy ( mission . id ) ;
// Second milestone and mission should still exist
expect ( reordered ? . milestones . map ( ( milestone ) = > milestone . id ) ) . toEqual ( [
expect ( missionStore . getMission ( mission . id ) ) . toBeDefined ( ) ;
lastMilestone . id ,
expect ( missionStore . getMilestone ( m2 . id ) ) . toBeDefined ( ) ;
firstMilestone . id ,
] ) ;
// First milestone's children should be gone
expect ( reordered ? . milestones [ 0 ] . slices . map ( ( slice ) = > slice . id ) ) . toEqual (
expect ( missionStore . getMilestone ( m1 . id ) ) . toBeUndefined ( ) ;
lastMilestone . slices . map ( ( slice ) = > slice . id ) ,
expect ( missionStore . getSlice ( slice . id ) ) . toBeUndefined ( ) ;
) ;
expect ( missionSto re. getFeature ( feature . id ) ) . toBeUndefined ( ) ;
expect ( reordered ? . milestones [ 1 ] . slices [ 0 ] . featu res . map ( ( feature ) = > feature . id ) ) . toEqual (
} ) ;
firstMilestone . slices [ 0 ] . features . map ( ( feature ) = > feature . id ) ,
) ;
it ( "should delete slice and its features" , ( ) = > {
expect ( reordered ) . toBeDefined ( ) ;
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
assertHierarchyIntegrity ( reordered ! ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const s1 = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const s2 = missionStore . addSlice ( milestone . id , { title : "Slice 2" } ) ;
const f1 = missionStore . addFeature ( s1 . id , { title : "Feature 1" } ) ;
// Delete first slice
missionStore . deleteSlice ( s1 . id ) ;
// Milestone and second slice should still exist
expect ( missionStore . getMilestone ( milestone . id ) ) . toBeDefined ( ) ;
expect ( missionStore . getSlice ( s2 . id ) ) . toBeDefined ( ) ;
// First slice and its feature should be gone
expect ( missionStore . getSlice ( s1 . id ) ) . toBeUndefined ( ) ;
expect ( missionStore . getFeature ( f1 . id ) ) . toBeUndefined ( ) ;
} ) ;
} ) ;
} ) ;
describe ( "Events" , ( ) = > {
it ( "emits mission lifecycle events for creation, linking, and slice activation in order" , async ( ) = > {
it ( "should emit mission:created event" , ( ) = > {
const missionStore = taskStore . getMissionStore ( ) ;
const handler = vi . fn ( ) ;
const events : string [ ] = [ ] ;
missionStore . on ( "mission:created" , handler ) ;
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
missionStore . on ( "mission:created" , ( ) = > events . push ( "mission:created" ) ) ;
missionStore . on ( "feature:linked" , ( ) = > events . push ( "feature:linked" ) ) ;
missionStore . on ( "slice:activated" , ( ) = > events . push ( "slice:activated" ) ) ;
expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
const mission = missionStore . createMission ( { title : "Event mission" } ) ;
expect ( handler ) . toHaveBeenCalledWith ( mission ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Event milestone" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Event slice" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Event feature" } ) ;
const task = await taskStore . createTask ( {
title : "Event task" ,
description : "Task for event assertions" ,
column : "todo" ,
} ) ;
} ) ;
await taskStore . moveTask ( task . id , "in-progress" ) ;
it ( "should emit milestone:created event" , ( ) = > {
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
missionStore . activateSlice ( slice . id ) ;
const handler = vi . fn ( ) ;
missionStore . on ( "milestone:created" , handler ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
expect ( events ) . toEqual ( [ "mission:created" , "feature:linked" , "slice:activated" ] ) ;
expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( handler ) . toHaveBeenCalledWith ( milestone ) ;
} ) ;
it ( "should emit slice:activated event" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const handler = vi . fn ( ) ;
missionStore . on ( "slice:activated" , handler ) ;
const activated = missionStore . activateSlice ( slice . id ) ;
expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( handler ) . toHaveBeenCalledWith ( activated ) ;
} ) ;
it ( "should emit feature:linked event" , async ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
const handler = vi . fn ( ) ;
missionStore . on ( "feature:linked" , handler ) ;
const task = await taskStore . createTask ( {
description : "Implement feature" ,
title : "Feature implementation" ,
} ) ;
const updated = missionStore . linkFeatureToTask ( feature . id , task . id ) ;
expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( handler ) . toHaveBeenCalledWith ( { feature : updated , taskId : task.id } ) ;
} ) ;
it ( "should emit delete events" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const handler = vi . fn ( ) ;
missionStore . on ( "mission:deleted" , handler ) ;
missionStore . deleteMission ( mission . id ) ;
expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( handler ) . toHaveBeenCalledWith ( mission . id ) ;
} ) ;
} ) ;
} ) ;
describe ( "Complex Scenarios ", ( ) = > {
it ( "uses the same Database instance for TaskStore and MissionStore ", ( ) = > {
it ( "should handle 3 milestones with 2 slices each with 3 features" , ( ) = > {
const missionStore = taskStore . getMissionStore ( ) ;
const mission = missionStore . createMission ( { title : "Complex Mission" } ) ;
const db = getPrivateDb ( taskStore ) ;
const missionStoreDb = ( missionStore as unknown as { db : Database } ) . db ;
// Create 3 milestones, each with 2 slices, each with 3 features
expect ( db ) . toBeDefined ( ) ;
for ( let m = 0 ; m < 3 ; m ++ ) {
expect ( missionStoreDb ) . toBe ( db ) ;
const milestone = missionStore . addMilestone ( mission . id , {
} ) ;
title : ` Milestone ${ m + 1 } ` ,
} ) ;
for ( let s = 0 ; s < 2 ; s ++ ) {
it ( "keeps hierarchy retrievable after repeated deterministic reorder operations" , async ( ) = > {
const slice = missionStore . addSlice ( milestone . id , {
const { missionStore , mission , milestones } = await createHierarchy ( taskStore ) ;
title : ` Milestone ${ m + 1 } - Slice ${ s + 1 } ` ,
} ) ;
for ( let f = 0 ; f < 3 ; f ++ ) {
missionStore . reorderMilestones ( mission . id , [ milestones [ 2 ] . id , milestones [ 0 ] . id , milestones [ 1 ] . id ] ) ;
missionStore . addFeature ( slice . id , {
missionStore . reorderMilestones ( mission . id , [ milestones [ 1 ] . id , milestones [ 2 ] . id , milestones [ 0 ] . id ] ) ;
title : ` Milestone ${ m + 1 } - Slice ${ s + 1 } - Feature ${ f + 1 } ` ,
} ) ;
for ( const milestone of missionStore . listMilestones ( mission . id ) ) {
}
const slices = missionStore . listSlices ( milestone . id ) ;
missionStore . reorderSlices (
milestone . id ,
slices
. map ( ( slice ) = > slice . id )
. reverse ( ) ,
) ;
}
const hierarchy = missionStore . getMissionWithHierarchy ( mission . id ) ;
expect ( hierarchy ? . milestones ) . toHaveLength ( 3 ) ;
expect ( hierarchy ) . toBeDefined ( ) ;
assertHierarchyIntegrity ( hierarchy ! ) ;
} ) ;
it ( "keeps hierarchy valid under overlapping reorder and lookup operations" , async ( ) = > {
const { missionStore , mission , milestones } = await createHierarchy ( taskStore ) ;
await Promise . all ( [
Promise . resolve ( ) . then ( ( ) = >
missionStore . reorderMilestones ( mission . id , [ milestones [ 1 ] . id , milestones [ 2 ] . id , milestones [ 0 ] . id ] ) ,
) ,
Promise . resolve ( ) . then ( ( ) = > {
const slices = missionStore . listSlices ( milestones [ 0 ] . id ) ;
missionStore . reorderSlices ( milestones [ 0 ] . id , slices . map ( ( slice ) = > slice . id ) . reverse ( ) ) ;
} ) ,
Promise . resolve ( ) . then ( ( ) = > missionStore . getMissionWithHierarchy ( mission . id ) ) ,
Promise . resolve ( ) . then ( ( ) = > missionStore . listMissions ( ) ) ,
] ) ;
const hierarchy = missionStore . getMissionWithHierarchy ( mission . id ) ;
expect ( hierarchy ) . toBeDefined ( ) ;
assertHierarchyIntegrity ( hierarchy ! ) ;
} ) ;
it ( "keeps all descendants retrievable after bulk feature completion updates" , async ( ) = > {
const { missionStore , mission } = await createHierarchy ( taskStore ) ;
const hierarchy = missionStore . getMissionWithHierarchy ( mission . id ) ! ;
for ( const milestone of hierarchy . milestones ) {
for ( const slice of milestone . slices ) {
for ( const feature of slice . features ) {
missionStore . updateFeature ( feature . id , { status : "done" } ) ;
}
}
}
}
}
// Verify full hierarchy
const refreshed = missionStore . getMissionWithHierarchy ( mission . id ) ! ;
const fullMission = missionStore . getMissionWithHierarchy ( mission . id ) ;
expect ( refreshed . milestones ) . toHaveLength ( 3 ) ;
expect ( fullMission ! . milestones ) . toHaveLength ( 3 ) ;
expect (
expect ( fullMission ! . milestones [ 0 ] . slices ) . toHaveLength ( 2 ) ;
refreshed . milestones . every ( ( milestone ) = >
expect ( fullMission ! . milestones [ 0 ] . slices [ 0 ] . features ) . toHaveLength ( 3 ) ;
milestone . slices . every ( ( slice ) = > {
const hierarchySlice = slice as typeof slice & { features : Array < { status : string } > } ;
// Total features
return hierarchySlice . features . every ( ( feature ) = > feature . status === "done" ) ;
let totalFeatures = 0 ;
} ) ,
for ( const m of fullMission ! . milestones ) {
) ,
for ( const s of m . slices ) {
) . toBe ( true ) ;
totalFeatures += s . features . length ;
}
}
expect ( totalFeatures ) . toBe ( 18 ) ; // 3 milestones × 2 slices × 3 features
} ) ;
it ( "should handle middle milestone deletion with orderIndex recomputation" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const m1 = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const m2 = missionStore . addMilestone ( mission . id , { title : "Milestone 2" } ) ;
const m3 = missionStore . addMilestone ( mission . id , { title : "Milestone 3" } ) ;
expect ( m1 . orderIndex ) . toBe ( 0 ) ;
expect ( m2 . orderIndex ) . toBe ( 1 ) ;
expect ( m3 . orderIndex ) . toBe ( 2 ) ;
// Delete middle milestone
missionStore . deleteMilestone ( m2 . id ) ;
// Remaining milestones should still be accessible
const remaining = missionStore . listMilestones ( mission . id ) ;
expect ( remaining ) . toHaveLength ( 2 ) ;
// Reordering doesn't happen automatically - orderIndex stays as is
// until explicit reorder is called
expect ( remaining [ 0 ] . id ) . toBe ( m1 . id ) ;
expect ( remaining [ 1 ] . id ) . toBe ( m3 . id ) ;
} ) ;
it ( "should handle reordering and verify integrity" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const m1 = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const m2 = missionStore . addMilestone ( mission . id , { title : "Milestone 2" } ) ;
const m3 = missionStore . addMilestone ( mission . id , { title : "Milestone 3" } ) ;
// Add slices with features to first milestone
const s1 = missionStore . addSlice ( m1 . id , { title : "Slice 1" } ) ;
const f1 = missionStore . addFeature ( s1 . id , { title : "Feature 1" } ) ;
// Reorder milestones: reverse order
missionStore . reorderMilestones ( mission . id , [ m3 . id , m2 . id , m1 . id ] ) ;
// Verify orderIndex updated
const milestones = missionStore . listMilestones ( mission . id ) ;
expect ( milestones [ 0 ] . id ) . toBe ( m3 . id ) ;
expect ( milestones [ 0 ] . orderIndex ) . toBe ( 0 ) ;
expect ( milestones [ 1 ] . id ) . toBe ( m2 . id ) ;
expect ( milestones [ 1 ] . orderIndex ) . toBe ( 1 ) ;
expect ( milestones [ 2 ] . id ) . toBe ( m1 . id ) ;
expect ( milestones [ 2 ] . orderIndex ) . toBe ( 2 ) ;
// Verify slice and feature still intact
const slices = missionStore . listSlices ( m1 . id ) ;
expect ( slices ) . toHaveLength ( 1 ) ;
expect ( slices [ 0 ] . id ) . toBe ( s1 . id ) ;
const features = missionStore . listFeatures ( s1 . id ) ;
expect ( features ) . toHaveLength ( 1 ) ;
expect ( features [ 0 ] . id ) . toBe ( f1 . id ) ;
} ) ;
} ) ;
} ) ;
describe ( "Concurrent Operations" , ( ) = > {
it ( "clears mission feature task links when a linked task is deleted" , async ( ) = > {
it ( "should handle rapid sequential modifications" , async ( ) = > {
const { missionStore , milestones } = await createHierarchy ( taskStore ) ;
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const feature = milestones [ 0 ] . slices [ 0 ] . features [ 0 ] ;
const task = await taskStore . createTask ( {
// Rapidly add many milestones
title : "Delete linked task" ,
const promises : Promise < Milestone > [ ] = [ ] ;
description : "Task used to verify foreign key cleanup." ,
for ( let i = 0 ; i < 10 ; i ++ ) {
column : "todo" ,
promises . push (
Promise . resolve (
missionStore . addMilestone ( mission . id , { title : ` Milestone ${ i } ` } )
)
) ;
}
const milestones = await Promise . all ( promises ) ;
// All should have unique orderIndex values
const orderIndices = milestones . map ( ( m ) = > m . orderIndex ) ;
const uniqueIndices = new Set ( orderIndices ) ;
expect ( uniqueIndices . size ) . toBe ( 10 ) ;
} ) ;
} ) ;
await taskStore . moveTask ( task . id , "in-progress" ) ;
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
it ( "should find next pending slice correctly" , ( ) = > {
await taskStore . deleteTask ( task . id ) ;
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const m1 = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const m2 = missionStore . addMilestone ( mission . id , { title : "Milestone 2" } ) ;
const s1 = missionStore . addSlice ( m1 . id , { title : "Slice 1" } ) ;
const refreshed = missionStore . getFeature ( feature . id ) ;
const s2 = missionStore . addSlice ( m1 . id , { title : "Slice 2" } ) ;
expect ( refreshed ? . taskId ) . toBeUndefined ( ) ;
const s3 = missionStore . addSlice ( m2 . id , { title : "Slice 3" } ) ;
// First pending should be s1
const first = missionStore . findNextPendingSlice ( mission . id ) ;
expect ( first ! . id ) . toBe ( s1 . id ) ;
// Activate s1
missionStore . activateSlice ( s1 . id ) ;
// Next pending should be s2
const second = missionStore . findNextPendingSlice ( mission . id ) ;
expect ( second ! . id ) . toBe ( s2 . id ) ;
// Mark s2 as complete
const f2 = missionStore . addFeature ( s2 . id , { title : "Feature" } ) ;
missionStore . updateFeatureStatus ( f2 . id , "done" ) ;
// Next pending should be s3
const third = missionStore . findNextPendingSlice ( mission . id ) ;
expect ( third ! . id ) . toBe ( s3 . id ) ;
} ) ;
} ) ;
describe ( "Edge Cases" , ( ) = > {
it ( "should handle mission with no milestones" , ( ) = > {
const mission = missionStore . createMission ( { title : "Empty Mission" } ) ;
expect ( missionStore . computeMissionStatus ( mission . id ) ) . toBe ( "planning" ) ;
const fullMission = missionStore . getMissionWithHierarchy ( mission . id ) ;
expect ( fullMission ! . milestones ) . toHaveLength ( 0 ) ;
} ) ;
it ( "should handle milestone with no slices" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Empty Milestone" } ) ;
expect ( missionStore . computeMilestoneStatus ( milestone . id ) ) . toBe ( "planning" ) ;
expect ( missionStore . listSlices ( milestone . id ) ) . toHaveLength ( 0 ) ;
} ) ;
it ( "should handle slice with no features" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Empty Slice" } ) ;
expect ( missionStore . computeSliceStatus ( slice . id ) ) . toBe ( "pending" ) ;
expect ( missionStore . listFeatures ( slice . id ) ) . toHaveLength ( 0 ) ;
} ) ;
it ( "should reject empty feature title" , ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
// Adding feature with empty title should still work at store level
// API layer handles validation
const feature = missionStore . addFeature ( slice . id , { title : "" } ) ;
expect ( feature . title ) . toBe ( "" ) ;
} ) ;
it ( "should handle unlinking feature from task" , async ( ) = > {
const mission = missionStore . createMission ( { title : "Test Mission" } ) ;
const milestone = missionStore . addMilestone ( mission . id , { title : "Milestone 1" } ) ;
const slice = missionStore . addSlice ( milestone . id , { title : "Slice 1" } ) ;
const feature = missionStore . addFeature ( slice . id , { title : "Feature 1" } ) ;
const task = await taskStore . createTask ( {
description : "Implement feature" ,
title : "Feature implementation" ,
} ) ;
// Link
missionStore . linkFeatureToTask ( feature . id , task . id ) ;
let updated = missionStore . getFeature ( feature . id ) ;
expect ( updated ! . taskId ) . toBe ( task . id ) ;
expect ( updated ! . status ) . toBe ( "triaged" ) ;
// Unlink
missionStore . unlinkFeatureFromTask ( feature . id ) ;
updated = missionStore . getFeature ( feature . id ) ;
expect ( updated ! . taskId ) . toBeUndefined ( ) ;
expect ( updated ! . status ) . toBe ( "defined" ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;