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),
|
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);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
|
try {
|
||||||
const reattached = await agent.reattachAgentChat!(sessionId, receive);
|
const reattached = await agent.reattachAgentChat!(sessionId, receive);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setCurrentSession(reattached.sessionId);
|
setCurrentSession(reattached.sessionId);
|
||||||
setTurns(reattached.scrollback.reduce(foldChunk, [] as ChatTurn[]));
|
setTurns(reattached.scrollback.reduce(foldChunk, [] as ChatTurn[]));
|
||||||
return;
|
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, {
|
const launched = await agent.launchAgentChat!(projectId, agentId, {
|
||||||
cwd,
|
cwd,
|
||||||
|
|||||||
Reference in New Issue
Block a user