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:
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user