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

@@ -1234,6 +1234,120 @@ describe("Scheduler", () => {
expect(mockMissionStore.activateSlice).not.toHaveBeenCalled();
});
// ── autopilotEnabled as primary control for onSliceComplete fallback ─────────
it("onSliceComplete auto-advances when autopilotEnabled is true (autoAdvance false)", async () => {
const missionHierarchy = {
id: "M-001",
status: "active",
milestones: [
{
id: "MS-001",
dependencies: [],
slices: [
{ id: "SL-001", status: "complete" },
{ id: "SL-002", status: "pending" },
],
},
],
};
const mockMissionStore = createMockMissionStore({
getMilestone: vi.fn().mockReturnValue({ id: "MS-001", missionId: "M-001" }),
getMission: vi.fn().mockReturnValue({ id: "M-001", status: "active", autopilotEnabled: true, autoAdvance: false }),
getMissionWithHierarchy: vi.fn().mockReturnValue(missionHierarchy),
activateSlice: vi.fn().mockReturnValue({ id: "SL-002", status: "active" }),
});
const store = createMockStore();
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
const slice = { id: "SL-001", milestoneId: "MS-001", status: "complete" } as any;
await scheduler.onSliceComplete(slice);
expect(mockMissionStore.getMission).toHaveBeenCalledWith("M-001");
expect(mockMissionStore.activateSlice).toHaveBeenCalledWith("SL-002");
});
it("onSliceComplete auto-advances when autopilotEnabled is true (autoAdvance unset)", async () => {
const missionHierarchy = {
id: "M-001",
status: "active",
milestones: [
{
id: "MS-001",
dependencies: [],
slices: [
{ id: "SL-001", status: "complete" },
{ id: "SL-002", status: "pending" },
],
},
],
};
const mockMissionStore = createMockMissionStore({
getMilestone: vi.fn().mockReturnValue({ id: "MS-001", missionId: "M-001" }),
getMission: vi.fn().mockReturnValue({ id: "M-001", status: "active", autopilotEnabled: true }),
getMissionWithHierarchy: vi.fn().mockReturnValue(missionHierarchy),
activateSlice: vi.fn().mockReturnValue({ id: "SL-002", status: "active" }),
});
const store = createMockStore();
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
const slice = { id: "SL-001", milestoneId: "MS-001", status: "complete" } as any;
await scheduler.onSliceComplete(slice);
expect(mockMissionStore.getMission).toHaveBeenCalledWith("M-001");
expect(mockMissionStore.activateSlice).toHaveBeenCalledWith("SL-002");
});
it("onSliceComplete does not auto-advance when both autopilotEnabled and autoAdvance are false", async () => {
const mockMissionStore = createMockMissionStore({
getMilestone: vi.fn().mockReturnValue({ id: "MS-001", missionId: "M-001" }),
getMission: vi.fn().mockReturnValue({ id: "M-001", status: "active", autopilotEnabled: false, autoAdvance: false }),
});
const store = createMockStore();
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
const slice = { id: "SL-001", milestoneId: "MS-001", status: "complete" } as any;
await scheduler.onSliceComplete(slice);
expect(mockMissionStore.getMission).toHaveBeenCalledWith("M-001");
expect(mockMissionStore.activateSlice).not.toHaveBeenCalled();
});
it("onSliceComplete auto-advances when autopilotEnabled is false but autoAdvance is true (legacy compat)", async () => {
const missionHierarchy = {
id: "M-001",
status: "active",
milestones: [
{
id: "MS-001",
dependencies: [],
slices: [
{ id: "SL-001", status: "complete" },
{ id: "SL-002", status: "pending" },
],
},
],
};
const mockMissionStore = createMockMissionStore({
getMilestone: vi.fn().mockReturnValue({ id: "MS-001", missionId: "M-001" }),
getMission: vi.fn().mockReturnValue({ id: "M-001", status: "active", autoAdvance: true }),
getMissionWithHierarchy: vi.fn().mockReturnValue(missionHierarchy),
activateSlice: vi.fn().mockReturnValue({ id: "SL-002", status: "active" }),
});
const store = createMockStore();
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
const slice = { id: "SL-001", milestoneId: "MS-001", status: "complete" } as any;
await scheduler.onSliceComplete(slice);
expect(mockMissionStore.getMission).toHaveBeenCalledWith("M-001");
expect(mockMissionStore.activateSlice).toHaveBeenCalledWith("SL-002");
});
it("skips mission progression when task sliceId mismatches linked feature sliceId", async () => {
const mockMissionStore = createMockMissionStore({
getFeatureByTaskId: vi.fn().mockReturnValue({ id: "F-001", sliceId: "SL-OTHER" }),

View File

@@ -795,7 +795,10 @@ export class Scheduler {
}
const mission = missionStore.getMission(milestone.missionId);
if (!mission || mission.status !== "active" || !mission.autoAdvance) {
// Use autopilotEnabled as canonical, fall back to autoAdvance for backward compat
const shouldAutoAdvance =
mission?.autopilotEnabled === true || mission?.autoAdvance === true;
if (!mission || mission.status !== "active" || !shouldAutoAdvance) {
return;
}