fix(frontend): le changement d'onglet ne déclenche plus de reprise de la conversation active — au remontage d'une cellule avec conversationId, réattache d'abord la session structurée vivante (sessionId du cache de conversation, puis registre listLiveAgents/attachLiveAgent avec rebinding de node) via reattachStructuredSession preserveViewOnEmptyScrollback avant tout chargement d'historique durable ; le transcript persisté (conversation.readPage) n'est consulté qu'en l'absence de runtime live et la bannière « Conversation reprise » reste réservée à la reprise explicite depuis le menu Conversations — éviction du cache de session morte (chatSessionViewCache.delete) réconciliée avec le cache de conversation pour ne pas rejouer un transcript obsolète ; tests: réattach live au remontage sans readPage ni bannière (#370), retour depuis une vue non conversationnelle = replay live complet (second reattach, pas de readPage, pas de bannière) — #370 (QA verte: npm ci, npm run typecheck, npx vitest run src/features/agents/CustomAgentChatView.test.tsx src/features/layout/LayoutGrid.chat.test.tsx, git diff --check ; fichiers .ideai/* et scripts/ hors périmètre ; aucun push)
This commit is contained in:
@ -433,6 +433,73 @@ describe("CustomAgentChatView", () => {
|
||||
expect(agent.sendAgentChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reattaches a live conversation on remount without reading durable history (#370)", async () => {
|
||||
let receive: ((chunk: ReplyChunk) => void) | undefined;
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
listLiveAgents: vi.fn(async () => [{
|
||||
agentId: "agent-1",
|
||||
nodeId: "node-1",
|
||||
sessionId: "live-session",
|
||||
kind: "structured" as const,
|
||||
runtimeState: "running" as const,
|
||||
}]),
|
||||
reattachAgentChat: vi.fn(async (
|
||||
sessionId: string,
|
||||
onChunk: (chunk: ReplyChunk) => void,
|
||||
) => {
|
||||
receive = onChunk;
|
||||
return {
|
||||
sessionId,
|
||||
conversationId: "active-conversation",
|
||||
scrollback: [
|
||||
{ kind: "userPrompt" as const, text: "Question active" },
|
||||
{ kind: "textDelta" as const, text: "Réponse en cours" },
|
||||
],
|
||||
};
|
||||
}),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
const conversation = {
|
||||
readPage: vi.fn(async () => ({ turns: [], hasMore: false })),
|
||||
};
|
||||
const onSessionId = vi.fn();
|
||||
|
||||
render(
|
||||
<DIProvider gateways={{
|
||||
agent,
|
||||
conversation,
|
||||
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={null}
|
||||
conversationId="active-conversation"
|
||||
onSessionId={onSessionId}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Question active")).toBeTruthy();
|
||||
expect(screen.getAllByText("Réponse en cours").length).toBeGreaterThan(0);
|
||||
expect(conversation.readPage).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("Conversation reprise")).toBeNull();
|
||||
expect(onSessionId).toHaveBeenCalledWith("live-session");
|
||||
|
||||
act(() => receive?.({ kind: "final", content: "Réponse terminée" }));
|
||||
expect(await screen.findByText("Réponse terminée")).toBeTruthy();
|
||||
expect(screen.getByText("Question active")).toBeTruthy();
|
||||
expect(screen.getAllByText("Réponse en cours").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("keeps a durable load failure distinct and retries without clearing its conversation id", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
@ -1602,7 +1669,7 @@ describe("CustomAgentChatView", () => {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi
|
||||
.fn()
|
||||
.mockImplementationOnce(async (sessionId: string) => ({
|
||||
.mockImplementation(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback,
|
||||
})),
|
||||
@ -1639,6 +1706,11 @@ describe("CustomAgentChatView", () => {
|
||||
expect(await screen.findByText("Initial reply")).toBeTruthy();
|
||||
first.unmount();
|
||||
await waitFor(() => expect(agent.detachAgentChat).toHaveBeenCalledWith("chat-session-1"));
|
||||
scrollback = [
|
||||
...scrollback,
|
||||
{ kind: "userPrompt", text: "Question during graph view" },
|
||||
{ kind: "final", content: "Answer restored from replay" },
|
||||
];
|
||||
|
||||
render(
|
||||
<DIProvider
|
||||
@ -1659,7 +1731,9 @@ describe("CustomAgentChatView", () => {
|
||||
expect(await screen.findByText("Answer restored from replay")).toBeTruthy();
|
||||
expect(screen.getByText("Question during graph view")).toBeTruthy();
|
||||
expect(agent.launchAgentChat).not.toHaveBeenCalled();
|
||||
expect(agent.reattachAgentChat).toHaveBeenCalledTimes(1);
|
||||
expect(agent.reattachAgentChat).toHaveBeenCalledTimes(2);
|
||||
expect(conversation.readPage).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("Conversation reprise")).toBeNull();
|
||||
});
|
||||
|
||||
it("drops a stale no-response chunk when replay contains a later real answer (#190)", async () => {
|
||||
|
||||
Reference in New Issue
Block a user