fix: dispose failed planning startup agents
This commit is contained in:
@@ -422,6 +422,41 @@ describe("Planning Mode Routes", () => {
|
|||||||
expect(res.body.sessionId).toBeDefined();
|
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 () => {
|
it("enforces rate limiting (1000 sessions per hour per IP)", async () => {
|
||||||
// Create 1000 sessions (should succeed)
|
// Create 1000 sessions (should succeed)
|
||||||
for (let i = 0; i < 1000; i++) {
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
|||||||
@@ -946,9 +946,21 @@ async function getFirstQuestionFromAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed) {
|
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);
|
sessions.delete(session.id);
|
||||||
unpersistSession(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(
|
throw new Error(
|
||||||
`Failed to get first question from AI: ${lastError?.message || "Unknown error"}`
|
`Failed to get first question from AI: ${lastError?.message || "Unknown error"}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe("desktop release workflow wiring", () => {
|
|||||||
expect(workflow).toContain("--x64");
|
expect(workflow).toContain("--x64");
|
||||||
expect(workflow).toContain("--arm64");
|
expect(workflow).toContain("--arm64");
|
||||||
expect(workflow).toContain("Fusion-*-linux-arm64.AppImage");
|
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("name: fusion-desktop-linux");
|
||||||
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
|
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user