fix(frontend): restaure l'historique durable au retour sur une conversation existante sans session live — freshOnOpen réservé au reset explicite (retiré de LayoutGrid et WebAgentCell), conversationId persistant de la cellule réutilisé au remontage ordinaire avec chargement de l'historique via conversation.readPage (machine savedConversationLoadState idle/loading/loaded/error/notFound, pagination ancrée conservée, écran vide faussement « Nouvelle conversation » éliminé), bannière d'annonces live et fallback de délégation masqués sans session réellement rattachée (liveSessionAttached), /clean et le reset ouvrent explicitement une nouvelle conversation (conversationId: null), erreurs de chargement traitées avec réessai et bouton « Nouvelle conversation » à la place du déliage, composer et pièces jointes désactivés en échec de chargement — #366 (QA verte: npm test ciblé CustomAgentChatView + LayoutGrid.chat + WebAgentCell 3 fichiers 119 tests OK, npm run build OK, git diff --check OK; warning jsdom/canvas connu non bloquant, npm audit: vulnérabilités préexistantes non liées)
This commit is contained in:
@ -47,6 +47,7 @@ import type { AgentProfile } from "@/domain";
|
||||
import type { Gateways } from "@/ports";
|
||||
import {
|
||||
MockAgentGateway,
|
||||
MockConversationGateway,
|
||||
MockLayoutGateway,
|
||||
MockProfileGateway,
|
||||
MockSystemGateway,
|
||||
@ -97,6 +98,7 @@ async function seeded(profile: AgentProfile): Promise<Gateways> {
|
||||
const terminal = new MockTerminalGateway();
|
||||
const system = new MockSystemGateway();
|
||||
const workState = new MockWorkStateGateway();
|
||||
const conversation = new MockConversationGateway();
|
||||
await profileGateway.configureProfiles([profile]);
|
||||
const created = await agent.createAgent("p1", {
|
||||
name: "Worker",
|
||||
@ -116,6 +118,7 @@ async function seeded(profile: AgentProfile): Promise<Gateways> {
|
||||
terminal,
|
||||
system,
|
||||
workState,
|
||||
conversation,
|
||||
} as unknown as Gateways;
|
||||
}
|
||||
|
||||
@ -175,6 +178,9 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "hello there" },
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("button", { name: "Envoyer" }).hasAttribute("disabled")).toBe(false),
|
||||
);
|
||||
fireEvent.click(screen.getByText("Envoyer"));
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Terminé")).toBeTruthy());
|
||||
@ -204,13 +210,14 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
|
||||
});
|
||||
|
||||
it("restores a project custom CLI cell after switching away and back without rewriting durable ids", async () => {
|
||||
it("restores durable messages after project A → B → A without starting a provider", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const profileGateway = new MockProfileGateway();
|
||||
const terminal = new MockTerminalGateway();
|
||||
const system = new MockSystemGateway();
|
||||
const workState = new MockWorkStateGateway();
|
||||
const conversation = new MockConversationGateway();
|
||||
await profileGateway.configureProfiles([structuredProfile]);
|
||||
|
||||
const agentA = await agent.createAgent("p1", {
|
||||
@ -229,6 +236,15 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
target: leafA.id,
|
||||
preferredView: "chat",
|
||||
});
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setCellConversation",
|
||||
target: leafA.id,
|
||||
conversationId: "saved-a",
|
||||
});
|
||||
conversation._setTurns("p1", "saved-a", [
|
||||
{ id: "p1", atMs: 1, role: "prompt", source: { kind: "human" }, text: "Question projet A", textLen: 17 },
|
||||
{ id: "r1", atMs: 2, role: "response", source: { kind: "agent", agentId: agentA.id }, text: "Réponse projet A", textLen: 16 },
|
||||
]);
|
||||
|
||||
const agentB = await agent.createAgent("p2", {
|
||||
name: "Worker B",
|
||||
@ -249,17 +265,14 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
terminal,
|
||||
system,
|
||||
workState,
|
||||
conversation,
|
||||
} as unknown as Gateways;
|
||||
const launchChat = vi.spyOn(agent, "launchAgentChat");
|
||||
const view = renderGrid(gateways, "p1");
|
||||
|
||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
await waitFor(async () => {
|
||||
const updated = await layout.loadLayout("p1");
|
||||
const leaf = leaves(updated)[0];
|
||||
expect(leaf.preferredView).toBe("chat");
|
||||
expect(leaf.session).toMatch(/^mock-agent-chat-/);
|
||||
expect(leaf.conversationId).toMatch(/^mock-conversation-mock-agent-chat-/);
|
||||
});
|
||||
expect(await screen.findByText("Question projet A")).toBeTruthy();
|
||||
expect(screen.getByText("Réponse projet A")).toBeTruthy();
|
||||
const persistedA = leaves(await layout.loadLayout("p1"))[0];
|
||||
|
||||
view.rerender(
|
||||
@ -280,10 +293,13 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
expect(screen.queryByRole("alertdialog", { name: "Confirmer le changement de CLI" }))
|
||||
.toBeNull();
|
||||
expect(screen.queryByText(/Aucune conversation reprenable/)).toBeNull();
|
||||
expect(await screen.findByText("Question projet A")).toBeTruthy();
|
||||
expect(screen.getByText("Réponse projet A")).toBeTruthy();
|
||||
const restoredA = leaves(await layout.loadLayout("p1"))[0];
|
||||
expect(restoredA.preferredView).toBe("chat");
|
||||
expect(restoredA.session).toBe(persistedA.session);
|
||||
expect(restoredA.conversationId).toBe(persistedA.conversationId);
|
||||
expect(launchChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps custom CLI and composer active while busy, then queues without sending (#247/#352)", async () => {
|
||||
@ -343,7 +359,7 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
expect(sendAgentChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders requester-side delegation fallback in custom mode after remount (#232)", async () => {
|
||||
it("does not render requester announcements without a live attached session (#366)", async () => {
|
||||
const gateways = await seeded(structuredProfile);
|
||||
const layout = gateways.layout as MockLayoutGateway;
|
||||
const workState = gateways.workState as MockWorkStateGateway;
|
||||
@ -386,13 +402,13 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
const view = renderGrid(gateways);
|
||||
|
||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
expect(await screen.findByText("Worker attend Architect")).toBeTruthy();
|
||||
expect(screen.queryByText("Worker attend Architect")).toBeNull();
|
||||
|
||||
view.unmount();
|
||||
renderGrid(gateways);
|
||||
|
||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
expect(await screen.findByText("Worker attend Architect")).toBeTruthy();
|
||||
expect(screen.queryByText("Worker attend Architect")).toBeNull();
|
||||
});
|
||||
|
||||
it("persists custom CLI composer height per layout cell", async () => {
|
||||
@ -438,7 +454,7 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("opens the custom CLI even when the agent already has a structured background session (#169)", async () => {
|
||||
it("does not attach an unrelated structured background session implicitly (#169/#366)", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const profileGateway = new MockProfileGateway();
|
||||
@ -476,17 +492,18 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(),
|
||||
);
|
||||
await waitFor(() => expect(launchChat).toHaveBeenCalled());
|
||||
expect(launchChat.mock.calls.at(-1)?.[2]).toMatchObject({ nodeId: leafId });
|
||||
expect(await screen.findByText("Nouvelle conversation")).toBeTruthy();
|
||||
expect(launchChat).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText(/déjà actif dans une autre cellule/)).toBeNull();
|
||||
});
|
||||
|
||||
it("starts a fresh custom CLI conversation when returning without a live session (#363)", async () => {
|
||||
it("restores a saved custom CLI conversation when returning without a live session (#366)", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const profileGateway = new MockProfileGateway();
|
||||
const terminal = new MockTerminalGateway();
|
||||
const system = new MockSystemGateway();
|
||||
const conversation = new MockConversationGateway();
|
||||
await profileGateway.configureProfiles([structuredProfile]);
|
||||
const created = await agent.createAgent("p1", {
|
||||
name: "Worker",
|
||||
@ -505,6 +522,10 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
conversationId: "conversation-known",
|
||||
});
|
||||
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom");
|
||||
conversation._setTurns("p1", "conversation-known", [
|
||||
{ id: "p1", atMs: 1, role: "prompt", source: { kind: "human" }, text: "known prompt", textLen: 12 },
|
||||
{ id: "r1", atMs: 2, role: "response", source: { kind: "agent", agentId: created.id }, text: "known response", textLen: 14 },
|
||||
]);
|
||||
const launchChat = vi.spyOn(agent, "launchAgentChat").mockResolvedValue({
|
||||
sessionId: "chat-fresh",
|
||||
cellKind: "chat" as const,
|
||||
@ -521,27 +542,17 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
profile: profileGateway,
|
||||
terminal,
|
||||
system,
|
||||
conversation,
|
||||
} as unknown as Gateways);
|
||||
|
||||
expect(await screen.findByText("Nouvelle conversation")).toBeTruthy();
|
||||
expect(screen.queryByText("known prompt")).toBeNull();
|
||||
expect(launchChat).toHaveBeenCalledWith(
|
||||
"p1",
|
||||
created.id,
|
||||
expect.objectContaining({
|
||||
nodeId: leafId,
|
||||
conversationId: undefined,
|
||||
}),
|
||||
);
|
||||
expect(reattachChat).toHaveBeenCalledWith("chat-fresh", expect.any(Function));
|
||||
await waitFor(async () => {
|
||||
const updated = await layout.loadLayout("p1");
|
||||
const leaf = leaves(updated)[0];
|
||||
expect(leaf.conversationId).toBe("conversation-fresh");
|
||||
expect(leaf.session).toBe("chat-fresh");
|
||||
});
|
||||
expect(screen.getByText("Envoyez un message pour démarrer la conversation structurée."))
|
||||
.toBeTruthy();
|
||||
expect(await screen.findByText("known prompt")).toBeTruthy();
|
||||
expect(screen.getByText("known response")).toBeTruthy();
|
||||
expect(screen.queryByText("Nouvelle conversation")).toBeNull();
|
||||
expect(launchChat).not.toHaveBeenCalled();
|
||||
expect(reattachChat).not.toHaveBeenCalled();
|
||||
const updated = await layout.loadLayout("p1");
|
||||
expect(leaves(updated)[0].conversationId).toBe("conversation-known");
|
||||
expect(leaves(updated)[0].session).toBeNull();
|
||||
});
|
||||
|
||||
it("relaunches a stopped custom CLI agent from scratch (#197)", async () => {
|
||||
@ -646,6 +657,10 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(),
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "Start live chat" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(async () => {
|
||||
const updated = await layout.loadLayout("p1");
|
||||
expect(leaves(updated)[0].session).toMatch(/^mock-agent-chat-/);
|
||||
@ -701,6 +716,10 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
);
|
||||
expect(stopLive).not.toHaveBeenCalled();
|
||||
expect(closeTerminal).not.toHaveBeenCalled();
|
||||
fireEvent.change(screen.getByLabelText(/Message CLI personnalisée/), {
|
||||
target: { value: "Start structured chat" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(() => expect(launchChat).toHaveBeenCalled());
|
||||
expect(launchChat.mock.calls.at(-1)?.[2]).toMatchObject({ nodeId: leafId });
|
||||
await waitFor(async () => {
|
||||
|
||||
Reference in New Issue
Block a user