fix(FN-2350): harden agent-generation session disposal diagnostics

- Use shared nonfatal diagnostics wrapper when disposing agent-generation AI sessions
- Emit structured error context (sessionId and operation) instead of silently swallowing dispose failures
- Add regression coverage verifying generation succeeds while dispose failures are logged as nonfatal diagnostics
This commit is contained in:
Fusion
2026-04-23 14:53:49 -07:00
committed by gsxdsm
parent 94473c864a
commit 824d678b9a
2 changed files with 74 additions and 6 deletions

View File

@@ -118,6 +118,70 @@ describe("agent-generation module", () => {
).rejects.toThrow(SessionNotFoundError);
});
it("emits a nonfatal diagnostic when session dispose throws after successful generation", async () => {
vi.resetModules();
const diagnostics: CapturedDiagnostic[] = [];
const diagnosticsModule = await import("./ai-session-diagnostics.js");
diagnosticsModule.setDiagnosticsSink((level, scope, message, context) => {
diagnostics.push({ level, scope, message, context });
});
vi.doMock("@fusion/engine", () => ({
createFnAgent: vi.fn(async () => {
const messages: Array<{ role: string; content: string }> = [];
return {
session: {
state: { messages },
prompt: vi.fn(async () => {
messages.push({
role: "assistant",
content: JSON.stringify({
title: "Generated Agent",
icon: "🤖",
role: "custom",
description: "Generated description",
systemPrompt: "Generated prompt",
thinkingLevel: "low",
maxTurns: 12,
}),
});
}),
dispose: vi.fn(() => {
throw new Error("dispose failed");
}),
},
};
}),
}));
const agentGenerationModule = await import("./agent-generation.js");
const session = await agentGenerationModule.startAgentGeneration(getUniqueIp(), "Role requiring generation");
const spec = await agentGenerationModule.generateAgentSpec(session.id, "/tmp");
expect(spec).toMatchObject({
title: "Generated Agent",
role: "custom",
});
expect(diagnostics).toContainEqual(
expect.objectContaining({
level: "error",
scope: "agent-generation",
message: "Failed to dispose agent-generation session",
context: expect.objectContaining({
sessionId: session.id,
operation: "dispose-agent-session",
error: expect.objectContaining({
message: "dispose failed",
}),
}),
})
);
diagnosticsModule.resetDiagnosticsSink();
});
it("logs structured error diagnostics with sessionId and rethrows when AI generation fails", async () => {
vi.resetModules();

View File

@@ -13,7 +13,7 @@
*/
import { randomUUID } from "node:crypto";
import { createSessionDiagnostics } from "./ai-session-diagnostics.js";
import { createSessionDiagnostics, nonfatal } from "./ai-session-diagnostics.js";
// Dynamic import for @fusion/core to get prompt override resolution
@@ -543,11 +543,15 @@ async function generateSpecWithAI(
return parseGenerationResponse(responseText);
} finally {
try {
agent.session.dispose?.();
} catch {
// Ignore cleanup errors
}
nonfatal(
() => agent.session.dispose?.(),
diagnostics,
"Failed to dispose agent-generation session",
{
sessionId: session.id,
operation: "dispose-agent-session",
}
);
}
}