merge(chat): intègre fix/ticket147-structured-session-reattach-fallback — fallback fresh launch sur reattach mort (#147, QA verte)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -94,4 +94,100 @@ describe("CustomAgentChatView", () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to a fresh launch when reattach reports NOT_FOUND", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(async () => ({
|
||||
sessionId: "chat-session-2",
|
||||
assignedConversationId: "conversation-2",
|
||||
})),
|
||||
reattachAgentChat: vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ code: "NOT_FOUND", message: "structured session gone" })
|
||||
.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="stale-session"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={onSessionId}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.launchAgentChat).toHaveBeenCalledWith("project-1", "agent-1", {
|
||||
cwd: "/repo",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
conversationId: "conversation-1",
|
||||
nodeId: "node-1",
|
||||
}),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(onSessionId).toHaveBeenCalledWith("chat-session-2"),
|
||||
);
|
||||
expect(screen.queryByText(/structured session gone/)).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces non-NOT_FOUND reattach errors instead of launching a new session", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn().mockRejectedValueOnce({
|
||||
code: "PROCESS",
|
||||
message: "structured session failed to attach",
|
||||
}),
|
||||
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
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="existing-session"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect((await screen.findByRole("alert")).textContent).toContain(
|
||||
"structured session failed to attach",
|
||||
);
|
||||
expect(agent.launchAgentChat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -154,11 +154,18 @@ export function CustomAgentChatView({
|
||||
setError(null);
|
||||
try {
|
||||
if (sessionId) {
|
||||
try {
|
||||
const reattached = await agent.reattachAgentChat!(sessionId, receive);
|
||||
if (cancelled) return;
|
||||
setCurrentSession(reattached.sessionId);
|
||||
setTurns(reattached.scrollback.reduce(foldChunk, [] as ChatTurn[]));
|
||||
return;
|
||||
} catch (e) {
|
||||
// The backend contract for `reattach_agent_chat` (NOT_FOUND) is that
|
||||
// the session is gone (closed/never live) and the caller falls back
|
||||
// to a fresh launch — any other error still surfaces to the user.
|
||||
if ((e as GatewayError)?.code !== "NOT_FOUND") throw e;
|
||||
}
|
||||
}
|
||||
const launched = await agent.launchAgentChat!(projectId, agentId, {
|
||||
cwd,
|
||||
|
||||
Reference in New Issue
Block a user