feat(FN-1453): fix mission autopilot progression and state refresh

- Fix mission autopilot slice activation when tasks complete
- Fix state refresh in MissionManager to properly reflect autopilot status
- Fix scheduler to correctly trigger autopilot progression events
- Add unit tests for mission store autopilot state transitions
- Add component tests for MissionManager autopilot toggle and state display
- Add e2e tests for mission autopilot lifecycle (enable → task completion → slice progression)
- Update documentation with autopilot state machine details
This commit is contained in:
gsxdsm
2026-04-09 16:12:58 -07:00
parent c38e254258
commit 8ba375549c
9 changed files with 456 additions and 13 deletions

View File

@@ -1829,6 +1829,93 @@ describe("MissionStore", () => {
"Slice SL-NONEXISTENT not found",
);
});
// ── autopilotEnabled as primary control ──────────────────────────────────
it("triages features when autopilotEnabled is true (autoAdvance false)", async () => {
const { ts, ms } = await createStoreWithTaskStore();
const mission = ms.createMission({ title: "Mission" });
// autopilotEnabled is primary control; autoAdvance=false/unset should still work
ms.updateMission(mission.id, { autopilotEnabled: true, autoAdvance: false });
const milestone = ms.addMilestone(mission.id, { title: "Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Slice" });
const f1 = ms.addFeature(slice.id, { title: "Feature 1" });
const f2 = ms.addFeature(slice.id, { title: "Feature 2" });
const activated = await ms.activateSlice(slice.id);
expect(activated.status).toBe("active");
// Both features should be triaged because autopilotEnabled=true
const updatedF1 = ms.getFeature(f1.id)!;
const updatedF2 = ms.getFeature(f2.id)!;
expect(updatedF1.status).toBe("triaged");
expect(updatedF1.taskId).toBeTruthy();
expect(updatedF2.status).toBe("triaged");
expect(updatedF2.taskId).toBeTruthy();
// Tasks should exist and be linked
const task1 = await ts.getTask(updatedF1.taskId!);
const task2 = await ts.getTask(updatedF2.taskId!);
expect(task1).toBeDefined();
expect(task1!.sliceId).toBe(slice.id);
expect(task2).toBeDefined();
expect(task2!.sliceId).toBe(slice.id);
});
it("triages features when autopilotEnabled is true (autoAdvance unset)", async () => {
const { ts, ms } = await createStoreWithTaskStore();
const mission = ms.createMission({ title: "Mission" });
// autopilotEnabled=true, autoAdvance undefined (neither true nor false)
ms.updateMission(mission.id, { autopilotEnabled: true });
const milestone = ms.addMilestone(mission.id, { title: "Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Slice" });
const f1 = ms.addFeature(slice.id, { title: "Feature 1" });
await ms.activateSlice(slice.id);
// Feature should be triaged because autopilotEnabled=true
const updatedF1 = ms.getFeature(f1.id)!;
expect(updatedF1.status).toBe("triaged");
expect(updatedF1.taskId).toBeTruthy();
});
it("does not triage features when autopilotEnabled is false and autoAdvance is false", async () => {
const { ms } = await createStoreWithTaskStore();
const mission = ms.createMission({ title: "Mission" });
ms.updateMission(mission.id, { autopilotEnabled: false, autoAdvance: false });
const milestone = ms.addMilestone(mission.id, { title: "Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Slice" });
const f1 = ms.addFeature(slice.id, { title: "Feature 1" });
await ms.activateSlice(slice.id);
// Feature should NOT be triaged
const updatedF1 = ms.getFeature(f1.id)!;
expect(updatedF1.status).toBe("defined");
expect(updatedF1.taskId).toBeUndefined();
});
it("triages features when autopilotEnabled is false but autoAdvance is true (legacy compat)", async () => {
const { ms } = await createStoreWithTaskStore();
const mission = ms.createMission({ title: "Mission" });
// Legacy case: autoAdvance=true, autopilotEnabled=false/unset
ms.updateMission(mission.id, { autoAdvance: true });
const milestone = ms.addMilestone(mission.id, { title: "Milestone" });
const slice = ms.addSlice(milestone.id, { title: "Slice" });
const f1 = ms.addFeature(slice.id, { title: "Feature 1" });
await ms.activateSlice(slice.id);
// Feature should be triaged because autoAdvance=true (legacy compat)
const updatedF1 = ms.getFeature(f1.id)!;
expect(updatedF1.status).toBe("triaged");
expect(updatedF1.taskId).toBeTruthy();
});
});
});

View File

@@ -1252,7 +1252,9 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
const milestone = this.getMilestone(slice.milestoneId);
const mission = milestone ? this.getMission(milestone.missionId) : undefined;
const shouldAutoTriage = mission?.autoAdvance === true;
// Use autopilotEnabled as canonical, fall back to autoAdvance for backward compat
const shouldAutoTriage =
mission?.autopilotEnabled === true || mission?.autoAdvance === true;
const now = new Date().toISOString();
const updated = this.updateSlice(id, {
@@ -1260,7 +1262,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
activatedAt: now,
});
// Auto-triage features if autoAdvance is enabled
// Auto-triage features if autopilot is enabled (or legacy autoAdvance)
if (shouldAutoTriage) {
try {
await this.triageSlice(id);