fix(FN-2053): enforce mission status consistency from feature state

- Recompute slice status when adding a feature so stale "complete" slices cascade back to milestone and mission statuses
- Require checkMissionCompletion to verify every feature is done before marking a mission complete
- Add mission-wide status-chain recomputation when stale complete slices or milestones are detected during reconciliation
- Expand core and engine tests to cover addFeature downgrades, stale complete states, and feature-level completion checks
This commit is contained in:
Fusion
2026-04-18 10:41:34 -07:00
committed by gsxdsm
parent 7575ae46bf
commit fad2c68ba2
4 changed files with 303 additions and 3 deletions

View File

@@ -1371,6 +1371,90 @@ describe("MissionStore", () => {
expect(store.computeMilestoneStatus(milestone.id)).toBe("complete");
});
});
describe("addFeature status cascade", () => {
it("addFeature triggers status recompute and downgrades slice from complete to pending", () => {
const mission = store.createMission({ title: "Mission" });
const milestone = store.addMilestone(mission.id, { title: "Milestone" });
const slice = store.addSlice(milestone.id, { title: "Slice" });
const feature = store.addFeature(slice.id, { title: "Feature" });
// Initially slice should be pending (one defined feature)
expect(store.getSlice(slice.id)?.status).toBe("pending");
// Mark feature done
store.updateFeature(feature.id, { status: "done" });
// Slice should now be complete
expect(store.getSlice(slice.id)?.status).toBe("complete");
expect(store.getMilestone(milestone.id)?.status).toBe("complete");
expect(store.getMission(mission.id)?.status).toBe("complete");
// Add a new feature → slice should downgrade
const newFeature = store.addFeature(slice.id, { title: "New Feature" });
// New feature is "defined", so slice should no longer be complete
expect(newFeature.status).toBe("defined");
expect(store.getSlice(slice.id)?.status).toBe("pending");
// Milestone with only "pending" slices becomes "planning"
expect(store.getMilestone(milestone.id)?.status).toBe("planning");
// Mission with only "planning" milestones becomes "planning"
expect(store.getMission(mission.id)?.status).toBe("planning");
});
it("adding feature to complete slice downgrades mission from complete to active", () => {
const mission = store.createMission({ title: "Mission" });
const milestone = store.addMilestone(mission.id, { title: "Milestone" });
const slice = store.addSlice(milestone.id, { title: "Slice" });
const f1 = store.addFeature(slice.id, { title: "Feature 1" });
const f2 = store.addFeature(slice.id, { title: "Feature 2" });
// Both features done → all complete
store.updateFeature(f1.id, { status: "done" });
store.updateFeature(f2.id, { status: "done" });
expect(store.getMission(mission.id)?.status).toBe("complete");
// Add third feature → mission should no longer be complete
const f3 = store.addFeature(slice.id, { title: "Feature 3" });
expect(f3.status).toBe("defined");
expect(store.getMission(mission.id)?.status).not.toBe("complete");
});
it("computeSliceStatus returns pending when features are mixed defined/done", () => {
const mission = store.createMission({ title: "Mission" });
const milestone = store.addMilestone(mission.id, { title: "Milestone" });
const slice = store.addSlice(milestone.id, { title: "Slice" });
const f1 = store.addFeature(slice.id, { title: "F1" });
const f2 = store.addFeature(slice.id, { title: "F2" });
// Mark f1 done (has taskId), f2 stays defined
store.updateFeature(f1.id, { status: "done" });
// f2 is still "defined"
// computeSliceStatus: allDone=false, anyActive=false (no taskId on any feature)
// → returns "pending"
const status = store.computeSliceStatus(slice.id);
expect(status).toBe("pending");
});
it("computeSliceStatus returns active when a feature has taskId linked", () => {
createTaskInDb(db, "FN-001");
const mission = store.createMission({ title: "Mission" });
const milestone = store.addMilestone(mission.id, { title: "Milestone" });
const slice = store.addSlice(milestone.id, { title: "Slice" });
const f1 = store.addFeature(slice.id, { title: "F1" });
const f2 = store.addFeature(slice.id, { title: "F2" });
// f1 has taskId → anyActive=true → slice is "active"
store.linkFeatureToTask(f1.id, "FN-001");
// f2 stays "defined"
const status = store.computeSliceStatus(slice.id);
expect(status).toBe("active");
});
});
});
// ── Mission With Hierarchy Tests ──────────────────────────────────────

View File

@@ -1507,6 +1507,13 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
this.db.bumpLastModified();
this.emit("feature:created", feature);
// Cascade status recompute upward: a newly added feature with status "defined"
// may downgrade the slice from "complete" → "pending", which in turn should
// update the parent milestone and mission statuses. Calling recomputeSliceStatus
// here ensures the full chain is updated atomically when a feature is added.
this.recomputeSliceStatus(sliceId);
return feature;
}