feat(web): heure de réception des messages custom CLI — #210
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { useState } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@ -71,6 +71,17 @@ function deferred<T>() {
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function expectedChatTime(ms: number): string {
|
||||
const parts = new Intl.DateTimeFormat(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
}).formatToParts(new Date(ms));
|
||||
const hour = parts.find((part) => part.type === "hour")?.value;
|
||||
const minute = parts.find((part) => part.type === "minute")?.value;
|
||||
return `${hour}:${minute}`;
|
||||
}
|
||||
|
||||
describe("CustomAgentChatView", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@ -122,6 +133,161 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getByText("Fichier: clipboard.png")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders local reception time in message meta without changing streamed turn grouping", async () => {
|
||||
const userReceivedAtMs = new Date("2026-08-15T13:05:00.000Z").getTime();
|
||||
const firstDeltaReceivedAtMs = new Date("2026-08-15T13:06:00.000Z").getTime();
|
||||
const finalChunkReceivedAtMs = new Date("2026-08-15T13:07:00.000Z").getTime();
|
||||
let nowMs = userReceivedAtMs;
|
||||
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => nowMs);
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback: [],
|
||||
})),
|
||||
sendAgentChat: vi.fn(async (_sessionId: string, _prompt: string, onChunk) => {
|
||||
nowMs = firstDeltaReceivedAtMs;
|
||||
onChunk({ kind: "textDelta", text: "Streaming reply" });
|
||||
nowMs = finalChunkReceivedAtMs;
|
||||
onChunk({ kind: "final", content: "Completed reply" });
|
||||
}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
try {
|
||||
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="chat-session-1"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||
"chat-session-1",
|
||||
expect.any(Function),
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
target: { value: "Initial prompt" },
|
||||
});
|
||||
nowMs = userReceivedAtMs;
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
const userBubble = (await screen.findByText("Initial prompt")).closest("div");
|
||||
const agentBubble = screen.getByText("Streaming reply").closest("div");
|
||||
const finalBubble = screen.getByText("Completed reply").closest("div");
|
||||
|
||||
expect(userBubble).toBeTruthy();
|
||||
expect(agentBubble).toBeTruthy();
|
||||
expect(finalBubble).toBeTruthy();
|
||||
expect(within(userBubble!).getByText(expectedChatTime(userReceivedAtMs))).toBeTruthy();
|
||||
expect(
|
||||
within(agentBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(finalBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("Streaming reply")).toBeTruthy();
|
||||
expect(screen.getByText("Completed reply")).toBeTruthy();
|
||||
expect(screen.queryByText(expectedChatTime(finalChunkReceivedAtMs))).toBeNull();
|
||||
} finally {
|
||||
nowSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a single reception time on error bubbles using the first streamed chunk time", async () => {
|
||||
const userReceivedAtMs = new Date("2026-08-15T13:05:00.000Z").getTime();
|
||||
const firstDeltaReceivedAtMs = new Date("2026-08-15T13:06:00.000Z").getTime();
|
||||
const errorReceivedAtMs = new Date("2026-08-15T13:07:00.000Z").getTime();
|
||||
let nowMs = userReceivedAtMs;
|
||||
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => nowMs);
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback: [],
|
||||
})),
|
||||
sendAgentChat: vi.fn(async (_sessionId: string, _prompt: string, onChunk) => {
|
||||
nowMs = firstDeltaReceivedAtMs;
|
||||
onChunk({ kind: "textDelta", text: "Streaming reply" });
|
||||
nowMs = errorReceivedAtMs;
|
||||
onChunk({ kind: "error", message: "Broken reply" });
|
||||
}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
try {
|
||||
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="chat-session-1"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||
"chat-session-1",
|
||||
expect.any(Function),
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
target: { value: "Initial prompt" },
|
||||
});
|
||||
nowMs = userReceivedAtMs;
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
const agentBubble = (await screen.findByText("Streaming reply")).closest("div");
|
||||
const errorBubble = screen.getByText("Broken reply").closest("div");
|
||||
|
||||
expect(agentBubble).toBeTruthy();
|
||||
expect(errorBubble).toBeTruthy();
|
||||
expect(
|
||||
within(agentBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(errorBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText(expectedChatTime(errorReceivedAtMs))).toBeNull();
|
||||
} finally {
|
||||
nowSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("resizes the composer vertically with mouse, keyboard, and double-click reset", async () => {
|
||||
const rectSpy = vi
|
||||
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
|
||||
@ -607,9 +773,7 @@ describe("CustomAgentChatView", () => {
|
||||
await screen.findByText("done");
|
||||
expect(hasProjectCliInteraction("project-1")).toBe(false);
|
||||
expect(screen.getAllByText("hello agent")).toHaveLength(1);
|
||||
expect(screen.getByText("Terminé").parentElement?.parentElement?.className).toContain(
|
||||
"justify-end",
|
||||
);
|
||||
expect(screen.getByText("Terminé").closest(".justify-end")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders canonical progress chunks as live agent, MCP, and delegation activity", async () => {
|
||||
|
||||
Reference in New Issue
Block a user