feat(FN-4054): surface inline chat failures with interactive error referenc
Implements inline chat failure rendering in the ChatView with interactive reference affordances, proper attribution preservation in failure bubbles, and aligned error typing across the chat stream pipeline, with corresponding tests and documentation updates. Fusion-Task-Id: FN-4054
This commit is contained in:
@@ -604,7 +604,7 @@ describe("ChatManager.sendMessage", () => {
|
||||
unsubscribe();
|
||||
|
||||
const assistantCalls = mockChatStore.addMessage.mock.calls.filter((call) => call[1].role === "assistant");
|
||||
expect(assistantCalls).toHaveLength(1);
|
||||
expect(assistantCalls).toHaveLength(2);
|
||||
expect(assistantCalls[0]).toEqual([
|
||||
"chat-001",
|
||||
expect.objectContaining({
|
||||
@@ -614,10 +614,20 @@ describe("ChatManager.sendMessage", () => {
|
||||
metadata: { interrupted: true },
|
||||
}),
|
||||
]);
|
||||
expect(events).toContainEqual({ type: "error", data: "Tool execution failed" });
|
||||
expect(assistantCalls[1]).toEqual([
|
||||
"chat-001",
|
||||
expect.objectContaining({
|
||||
role: "assistant",
|
||||
content: "Tool execution failed",
|
||||
metadata: expect.objectContaining({
|
||||
failureInfo: expect.objectContaining({ summary: "Tool execution failed" }),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(events).toContainEqual({ type: "error", data: expect.objectContaining({ summary: "Tool execution failed" }) });
|
||||
});
|
||||
|
||||
it("does not persist empty assistant response on immediate failure", async () => {
|
||||
it("persists a structured assistant failure message on immediate failure", async () => {
|
||||
__setCreateFnAgent(async () => {
|
||||
return {
|
||||
session: {
|
||||
@@ -632,10 +642,20 @@ describe("ChatManager.sendMessage", () => {
|
||||
await chatManager.sendMessage("chat-001", "Hello");
|
||||
|
||||
const assistantCalls = mockChatStore.addMessage.mock.calls.filter((call) => call[1].role === "assistant");
|
||||
expect(assistantCalls).toHaveLength(0);
|
||||
expect(assistantCalls).toHaveLength(1);
|
||||
expect(assistantCalls[0]).toEqual([
|
||||
"chat-001",
|
||||
expect.objectContaining({
|
||||
role: "assistant",
|
||||
content: "Immediate failure",
|
||||
metadata: expect.objectContaining({
|
||||
failureInfo: expect.objectContaining({ summary: "Immediate failure" }),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("surfaces provider errors stored on session.state.errorMessage instead of persisting a blank assistant reply", async () => {
|
||||
it("surfaces provider errors stored on session.state.errorMessage and persists a failure bubble", async () => {
|
||||
const events: Array<{ type: string; data: unknown }> = [];
|
||||
const unsubscribe = chatStreamManager.subscribe("chat-001", (event) => {
|
||||
events.push(event);
|
||||
@@ -657,8 +677,18 @@ describe("ChatManager.sendMessage", () => {
|
||||
unsubscribe();
|
||||
|
||||
const assistantCalls = mockChatStore.addMessage.mock.calls.filter((call) => call[1].role === "assistant");
|
||||
expect(assistantCalls).toHaveLength(0);
|
||||
expect(events).toContainEqual({ type: "error", data: "Codex error: provider request failed" });
|
||||
expect(assistantCalls).toHaveLength(1);
|
||||
expect(assistantCalls[0]).toEqual([
|
||||
"chat-001",
|
||||
expect.objectContaining({
|
||||
role: "assistant",
|
||||
content: "Codex error: provider request failed",
|
||||
metadata: expect.objectContaining({
|
||||
failureInfo: expect.objectContaining({ summary: "Codex error: provider request failed" }),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(events).toContainEqual({ type: "error", data: expect.objectContaining({ summary: "Codex error: provider request failed" }) });
|
||||
});
|
||||
|
||||
it("uses the agent runtime path when the agent has a runtimeHint configured", async () => {
|
||||
@@ -960,7 +990,7 @@ describe("ChatManager.sendMessage", () => {
|
||||
await chatManager.sendMessage("chat-001", "Hello");
|
||||
|
||||
const assistantCalls = mockChatStore.addMessage.mock.calls.filter((call) => call[1].role === "assistant");
|
||||
expect(assistantCalls).toHaveLength(1);
|
||||
expect(assistantCalls).toHaveLength(2);
|
||||
expect(assistantCalls[0]).toEqual([
|
||||
"chat-001",
|
||||
expect.objectContaining({
|
||||
@@ -970,6 +1000,16 @@ describe("ChatManager.sendMessage", () => {
|
||||
metadata: { interrupted: true },
|
||||
}),
|
||||
]);
|
||||
expect(assistantCalls[1]).toEqual([
|
||||
"chat-001",
|
||||
expect.objectContaining({
|
||||
role: "assistant",
|
||||
content: "Interrupted during tool call",
|
||||
metadata: expect.objectContaining({
|
||||
failureInfo: expect.objectContaining({ summary: "Interrupted during tool call" }),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses accumulated text as primary source over state.messages extraction", async () => {
|
||||
|
||||
@@ -1324,6 +1324,43 @@ describe("Chat API Routes", () => {
|
||||
expect(output).toContain('"content":"Final reply"');
|
||||
});
|
||||
|
||||
it("SSE route forwards structured error payloads", async () => {
|
||||
mockGetSession.mockReturnValue(sampleSession);
|
||||
|
||||
const chatModule = await import("../chat.js");
|
||||
vi.mocked(chatModule.checkRateLimit).mockReturnValue(true);
|
||||
|
||||
mockSendMessage.mockImplementation(async (sessionId: string) => {
|
||||
mockChatStreamManager.broadcast(sessionId, {
|
||||
type: "error",
|
||||
data: {
|
||||
summary: "Model request failed",
|
||||
errorClass: "ProviderError",
|
||||
code: "E_MODEL",
|
||||
detail: "ProviderError: Model request failed",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const req = createSSERequest();
|
||||
const { res, chunks } = createSSEResponse();
|
||||
|
||||
req.body = { content: "Hello" };
|
||||
req.params = { id: "chat-abc123" };
|
||||
req.query = {} as any;
|
||||
req.headers = {} as any;
|
||||
req.ip = "127.0.0.1";
|
||||
req.socket = { remoteAddress: "127.0.0.1" } as any;
|
||||
|
||||
await invokeSSEHandler(req, res, store, mockChatStore, mockChatManager);
|
||||
|
||||
const output = chunks.join("");
|
||||
expect(output).toContain("event: error");
|
||||
expect(output).toContain('"summary":"Model request failed"');
|
||||
expect(output).toContain('"errorClass":"ProviderError"');
|
||||
expect(output).toContain('"code":"E_MODEL"');
|
||||
});
|
||||
|
||||
it("uses the same generation id for subscription and sendMessage", async () => {
|
||||
mockGetSession.mockReturnValue(sampleSession);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user