FN-5679: default new missions to stopped state
Ensure mission creation always starts in a stopped planning state with autopilot off. - Force MissionStore createMission to ignore create-time autopilotEnabled and persist stopped defaults - Update mission POST route behavior to stop auto-watching on create and keep optional create-time updates scoped - Add/adjust core and dashboard tests to assert stopped-by-default creation semantics - Document mission autopilot creation behavior and add a patch changeset for @runfusion/fusion Files changed: .changeset/fn-5679-missions-default-stopped.md | 7 +++++++ docs/missions.md | 4 +++- .../mission-factory-parity.integration.test.ts | 6 +++--- packages/core/src/__tests__/mission-store.test.ts | 18 ++++++++++++++++++ packages/core/src/mission-store.ts | 5 +++-- packages/dashboard/src/__tests__/mission-e2e.test.ts | 8 +++++--- packages/dashboard/src/mission-routes.ts | 9 +-------- 7 files changed, 40 insertions(+), 17 deletions(-) Fusion-Task-Id: FN-5679 Fusion-Task-Lineage: 05eff3cb-07d8-4b52-ac7f-065979e78816
This commit is contained in:
7
.changeset/fn-5679-missions-default-stopped.md
Normal file
7
.changeset/fn-5679-missions-default-stopped.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Mission creation now always returns a stopped mission. `POST /api/missions` and the mission store ignore create-time `autopilotEnabled` input, forcing new missions to `status: "planning"` with autopilot disabled and inactive.
|
||||
|
||||
Autopilot remains a post-creation action via explicit mission start/update flows.
|
||||
@@ -176,6 +176,8 @@ Manual activation is available through `fn mission activate-slice <slice-id>`.
|
||||
|
||||
## Mission Autopilot
|
||||
|
||||
Missions are always created stopped (`status: "planning"`, `autopilotEnabled: false`, `autoAdvance: false`).
|
||||
Autopilot must be enabled explicitly after creation (for example via start/update actions).
|
||||
When `autopilotEnabled` is on, Fusion can watch completion events and progress missions automatically.
|
||||
|
||||
State machine:
|
||||
@@ -187,7 +189,7 @@ State machine:
|
||||
|
||||
Typical flow:
|
||||
|
||||
1. Mission is watched (missions created/updated with `autopilotEnabled: true` are watched immediately)
|
||||
1. Mission is watched (missions updated with `autopilotEnabled: true` or explicitly started are watched)
|
||||
2. Task completion updates feature status
|
||||
3. If a slice is complete, autopilot activates next pending slice
|
||||
4. When milestones are all complete, mission transitions to complete
|
||||
|
||||
@@ -377,7 +377,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
|
||||
});
|
||||
|
||||
describe("Parity Matrix: Autopilot Configuration", () => {
|
||||
it("autopilotEnabled persists across restart", async () => {
|
||||
it("missions created with autopilotEnabled start disabled across restart", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({
|
||||
@@ -387,7 +387,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
|
||||
|
||||
// Verify initial state
|
||||
let retrieved = missionStore.getMission(mission.id);
|
||||
expect(retrieved!.autopilotEnabled).toBe(true);
|
||||
expect(retrieved!.autopilotEnabled).toBe(false);
|
||||
|
||||
// Restart
|
||||
taskStore.close();
|
||||
@@ -397,7 +397,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
|
||||
|
||||
// Verify persistence
|
||||
retrieved = missionStore2.getMission(mission.id);
|
||||
expect(retrieved!.autopilotEnabled).toBe(true);
|
||||
expect(retrieved!.autopilotEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("autopilotEnabled can be toggled", async () => {
|
||||
|
||||
@@ -68,6 +68,24 @@ describe("MissionStore", () => {
|
||||
expect(mission.updatedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("ignores autopilotEnabled on create and persists stopped defaults", () => {
|
||||
const mission = store.createMission({
|
||||
title: "Stopped by default",
|
||||
autopilotEnabled: true,
|
||||
});
|
||||
|
||||
expect(mission.autopilotEnabled).toBe(false);
|
||||
expect(mission.autoAdvance).toBe(false);
|
||||
expect(mission.status).toBe("planning");
|
||||
expect(mission.autopilotState).toBe("inactive");
|
||||
|
||||
const persisted = store.getMission(mission.id);
|
||||
expect(persisted?.autopilotEnabled).toBe(false);
|
||||
expect(persisted?.autoAdvance).toBe(false);
|
||||
expect(persisted?.status).toBe("planning");
|
||||
expect(persisted?.autopilotState).toBe("inactive");
|
||||
});
|
||||
|
||||
it("gets a mission by id", () => {
|
||||
const created = store.createMission({ title: "Get Test" });
|
||||
const retrieved = store.getMission(created.id);
|
||||
|
||||
@@ -567,7 +567,8 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
|
||||
/**
|
||||
* Create a new mission.
|
||||
* The mission starts in "planning" status with "not_started" interview state.
|
||||
* Missions are always created stopped (status `planning`, autopilot disabled).
|
||||
* Autopilot is only enabled via an explicit start/update after creation.
|
||||
*
|
||||
* @param input - Mission creation input
|
||||
* @returns The created mission
|
||||
@@ -586,7 +587,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
branchStrategy: input.branchStrategy,
|
||||
autoMerge: input.autoMerge,
|
||||
autoAdvance: false,
|
||||
autopilotEnabled: input.autopilotEnabled ?? false,
|
||||
autopilotEnabled: false,
|
||||
autopilotState: "inactive",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
||||
@@ -793,7 +793,7 @@ describe("Mission API", () => {
|
||||
expect(missionStore.updateMission).toHaveBeenCalledWith(res.body.id, { autoAdvance: true });
|
||||
});
|
||||
|
||||
it("watches mission immediately when created with autopilotEnabled true", async () => {
|
||||
it("creates missions stopped even when autopilotEnabled is passed", async () => {
|
||||
const missionAutopilot = createMockMissionAutopilot();
|
||||
const { app } = buildApp({ missionAutopilot });
|
||||
|
||||
@@ -806,8 +806,10 @@ describe("Mission API", () => {
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(missionAutopilot.watchMission).toHaveBeenCalledTimes(1);
|
||||
expect(missionAutopilot.watchMission).toHaveBeenCalledWith(res.body.id);
|
||||
expect(res.body.status).toBe("planning");
|
||||
expect(res.body.autopilotEnabled).toBe(false);
|
||||
expect(res.body.autoAdvance).toBe(false);
|
||||
expect(missionAutopilot.watchMission).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -332,7 +332,7 @@ export function createMissionRouter(
|
||||
router.post(
|
||||
"/",
|
||||
catchTypedHandler(async (req, res) => {
|
||||
const { title, description, autoAdvance, autopilotEnabled, baseBranch, branchStrategy } = req.body;
|
||||
const { title, description, autoAdvance, baseBranch, branchStrategy } = req.body;
|
||||
|
||||
const validatedTitle = validateTitle(title);
|
||||
const validatedDescription = validateDescription(description);
|
||||
@@ -350,15 +350,8 @@ export function createMissionRouter(
|
||||
if (autoAdvance !== undefined) {
|
||||
updates.autoAdvance = validateBoolean(autoAdvance, "autoAdvance");
|
||||
}
|
||||
if (autopilotEnabled !== undefined) {
|
||||
updates.autopilotEnabled = validateBoolean(autopilotEnabled, "autopilotEnabled");
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
const updatedMission = missionStore.updateMission(mission.id, updates);
|
||||
if (missionAutopilot && updatedMission.autopilotEnabled) {
|
||||
missionAutopilot.watchMission(updatedMission.id);
|
||||
}
|
||||
res.status(201).json(updatedMission);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user