fix(FN-1187): integration fixes for mission health and type updates

- Add /missions/health endpoint handling to MissionManager test mocks
- Add listMissionsWithSummaries to mission-e2e test mock
- Add planState to Slice type and mock factories
- Add stuckKillCount to retry task test assertions
- Update log message for stuck-killed retry
This commit is contained in:
gsxdsm
2026-04-09 12:21:51 -07:00
parent 850939d268
commit ced3ad3be6
10 changed files with 210 additions and 3 deletions

View File

@@ -265,6 +265,11 @@ class MockEventSource {
/** Fetch mock that returns mission list, detail, health, autopilot, and events endpoints. */
function createFetchMock() {
return vi.fn().mockImplementation((url: string) => {
// Handle batched health endpoint before individual health endpoint
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
@@ -289,6 +294,11 @@ function createFetchMock() {
/** Fetch mock for navigating into a mission detail */
function createDetailFetchMock(events = mockMissionEvents) {
return vi.fn().mockImplementation((url: string) => {
// Handle batched health endpoint before individual health endpoint
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, events)));
}

View File

@@ -102,6 +102,21 @@ function createMockMissionStore() {
)
),
listMissionsWithSummaries: vi.fn(() =>
Array.from(missions.values())
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.map((m) => ({
...m,
summary: {
totalMilestones: 0,
completedMilestones: 0,
totalFeatures: 0,
completedFeatures: 0,
progressPercent: 0,
},
}))
),
getMissionSummary: vi.fn((_missionId: string) => ({
totalMilestones: 0,
completedMilestones: 0,

View File

@@ -1050,6 +1050,7 @@ describe("POST /tasks/:id/retry", () => {
error: null,
worktree: null,
branch: null,
stuckKillCount: 0,
});
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
});
@@ -1083,6 +1084,7 @@ describe("POST /tasks/:id/retry", () => {
error: null,
worktree: null,
branch: null,
stuckKillCount: 0,
});
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
});
@@ -1104,9 +1106,10 @@ describe("POST /tasks/:id/retry", () => {
error: null,
worktree: null,
branch: null,
stuckKillCount: 0,
});
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard");
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (stuck kill budget reset)");
});
});

View File

@@ -2095,8 +2095,44 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
error: null,
worktree: null,
branch: null,
stuckKillCount: 0,
});
await scopedStore.logEntry(req.params.id, "Retry requested from dashboard");
// Reset steps if the branch has no unique commits (work was lost with worktree)
const completedSteps = task.steps.filter(
(s: { status: string }) => s.status === "done" || s.status === "in-progress",
);
if (completedSteps.length > 0) {
const branchName = task.branch || `fusion/${task.id.toLowerCase()}`;
try {
const { execSync } = await import("node:child_process");
const rootDir = scopedStore.getRootDir();
const mergeBase = execSync(
`git merge-base "${branchName}" HEAD 2>/dev/null`,
{ cwd: rootDir, stdio: "pipe", encoding: "utf-8" },
).trim();
const branchHead = execSync(
`git rev-parse "${branchName}" 2>/dev/null`,
{ cwd: rootDir, stdio: "pipe", encoding: "utf-8" },
).trim();
if (mergeBase === branchHead) {
for (let i = 0; i < task.steps.length; i++) {
if (task.steps[i].status === "done" || task.steps[i].status === "in-progress") {
await scopedStore.updateStep(req.params.id, i, "pending");
}
}
await scopedStore.logEntry(
req.params.id,
`Reset ${completedSteps.length} step(s) to pending — branch had no commits (uncommitted work lost)`,
);
}
} catch {
// Branch may not exist — non-fatal, steps keep their status
}
}
await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (stuck kill budget reset)");
const updated = await scopedStore.moveTask(req.params.id, "todo");
res.json(updated);
} catch (err: any) {