Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/dashboard/src/__tests__/routes-planning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,41 @@ describe("Planning Mode Routes", () => {
expect(res.body.sessionId).toBeDefined();
});

it("returns an API error instead of terminating when the first AI response is unparsable", async () => {
const messages: Array<{ role: string; content: string }> = [];
const dispose = vi.fn();
__setCreateFnAgent(async () => ({
session: {
state: { messages },
prompt: vi.fn(async (msg: string) => {
messages.push({ role: "user", content: msg });
messages.push({ role: "assistant", content: "" });
}),
dispose,
},
}));
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
throw new Error("process.exit should not be called");
}) as never);

try {
const res = await REQUEST(
buildApp(),
"POST",
"/api/planning/start",
JSON.stringify({ initialPlan: "Investigate dashboard crash" }),
{ "Content-Type": "application/json" },
);

expect(res.status).toBe(500);
expect(res.body.error).toContain("Failed to get first question from AI");
expect(exitSpy).not.toHaveBeenCalled();
expect(dispose).toHaveBeenCalledTimes(1);
} finally {
exitSpy.mockRestore();
}
});

it("enforces rate limiting (1000 sessions per hour per IP)", async () => {
// Create 1000 sessions (should succeed)
for (let i = 0; i < 1000; i++) {
Expand Down
14 changes: 13 additions & 1 deletion packages/dashboard/src/planning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,9 +946,21 @@ async function getFirstQuestionFromAgent(
}

if (!parsed) {
// Clean up the session on failure
// Clean up the failed startup session and release the underlying agent so
// an unparsable first response cannot leave model/transport handles behind
// in the long-running dashboard process.
sessions.delete(session.id);
unpersistSession(session.id);
try {
await session.agent.session.dispose?.();
} catch (disposeErr) {
diagnostics.warn("Failed to dispose planning agent after first question failure", {
sessionId: session.id,
message: disposeErr instanceof Error ? disposeErr.message : String(disposeErr),
operation: "dispose-after-first-question-failure",
});
}
session.agent = undefined;
throw new Error(
`Failed to get first question from AI: ${lastError?.message || "Unknown error"}`
);
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/src/__tests__/release-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("desktop release workflow wiring", () => {
expect(workflow).toContain("--x64");
expect(workflow).toContain("--arm64");
expect(workflow).toContain("Fusion-*-linux-arm64.AppImage");
expect(workflow).toContain("Fusion-*-linux-x64.AppImage");
expect(workflow).toMatch(/Fusion-\*-linux-(x64|x86_64)\.AppImage/);
expect(workflow).toContain("name: fusion-desktop-linux");
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
}
Expand Down
Loading