fix(chat): handle raw structured session NOT_FOUND errors from Tauri

Elargit isNotFound() pour reconnaitre aussi la forme brute
'not found: structured session <uuid>' que Tauri peut retourner,
en plus de la forme typée {code: 'NOT_FOUND'}.

Ajoute deux tests couvrant:
- fallback sur reattach avec erreur brute
- retry prompt sur send avec erreur brute

QA: tests passés sur cette correction.
This commit is contained in:
2026-08-05 16:00:37 +02:00
parent 62ac05a080
commit 7071c53bb2
5 changed files with 168 additions and 7 deletions

View File

@ -152,6 +152,65 @@ describe("CustomAgentChatView", () => {
expect(screen.queryByText(/structured session gone/)).toBeNull();
});
it("falls back when Tauri surfaces structured session NOT_FOUND as a raw string", async () => {
const agent = {
launchAgentChat: vi.fn(async () => ({
sessionId: "chat-session-2",
assignedConversationId: "conversation-2",
})),
reattachAgentChat: vi
.fn()
.mockRejectedValueOnce(
"not found: structured session 69b57638-0ac7-4a79-8252-e0436a3265f6",
)
.mockImplementationOnce(async (sessionId: string) => ({
sessionId,
scrollback: [],
})),
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
const onSessionId = vi.fn();
render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView
projectId="project-1"
agentId="agent-1"
agentName="Worker"
profile={profile}
cwd="/repo"
nodeId="node-1"
sessionId="69b57638-0ac7-4a79-8252-e0436a3265f6"
conversationId="conversation-1"
onSessionId={onSessionId}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
await waitFor(() => expect(agent.launchAgentChat).toHaveBeenCalledTimes(1));
expect(agent.reattachAgentChat).toHaveBeenNthCalledWith(
1,
"69b57638-0ac7-4a79-8252-e0436a3265f6",
expect.any(Function),
);
expect(agent.reattachAgentChat).toHaveBeenNthCalledWith(
2,
"chat-session-2",
expect.any(Function),
);
expect(onSessionId).toHaveBeenCalledWith(null);
expect(onSessionId).toHaveBeenCalledWith("chat-session-2");
expect(screen.queryByText(/not found: structured session/)).toBeNull();
});
it("does not reopen when the parent echoes a launched session id", async () => {
const agent = {
launchAgentChat: vi.fn(async () => ({
@ -286,6 +345,74 @@ describe("CustomAgentChatView", () => {
expect(screen.queryByText(/not found: structured session/)).toBeNull();
});
it("recovers and retries the prompt when send reports structured session NOT_FOUND as a raw string", async () => {
const agent = {
launchAgentChat: vi.fn(async () => ({
sessionId: "chat-session-2",
assignedConversationId: "conversation-2",
})),
reattachAgentChat: vi.fn(async (sessionId: string) => ({
sessionId,
scrollback: [],
})),
sendAgentChat: vi
.fn()
.mockRejectedValueOnce("not found: structured session stale-session")
.mockImplementationOnce(async (_sessionId: string, _prompt: string, onChunk) => {
onChunk({ kind: "final", content: "Recovered reply" });
}),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
const onSessionId = vi.fn();
render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView
projectId="project-1"
agentId="agent-1"
agentName="Worker"
profile={profile}
cwd="/repo"
nodeId="node-1"
sessionId="stale-session"
conversationId="conversation-1"
onSessionId={onSessionId}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
await waitFor(() =>
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
"stale-session",
expect.any(Function),
),
);
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
target: { value: "please recover" },
});
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(2));
expect(agent.sendAgentChat).toHaveBeenNthCalledWith(
2,
"chat-session-2",
"please recover",
expect.any(Function),
);
expect(onSessionId).toHaveBeenCalledWith(null);
expect(onSessionId).toHaveBeenCalledWith("chat-session-2");
expect(await screen.findByText("Recovered reply")).toBeTruthy();
expect(screen.queryByText(/not found: structured session/)).toBeNull();
});
it("treats NOT_FOUND during cancel as an already-gone session", async () => {
const agent = {
launchAgentChat: vi.fn(),

View File

@ -54,7 +54,16 @@ function instrumentationError(e: unknown): Record<string, unknown> {
}
function isNotFound(e: unknown): boolean {
return Boolean(e && typeof e === "object" && (e as GatewayError).code === "NOT_FOUND");
if (e && typeof e === "object" && (e as GatewayError).code === "NOT_FOUND") {
return true;
}
const message =
typeof e === "string"
? e
: e && typeof e === "object" && "message" in e
? String((e as GatewayError).message)
: "";
return /^not found: structured session\b/i.test(message);
}
function unknownChunkLabel(chunk: unknown): string {