test(custom-cli): tests à deux callbacks couvrant le streaming progress/outils sur la seconde requête au même agent — #252 (QA verte)
This commit is contained in:
@ -72,6 +72,45 @@ function deferred<T>() {
|
|||||||
return { promise, resolve };
|
return { promise, resolve };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createStreamingAgentHarness() {
|
||||||
|
const emits: Array<((chunk: ReplyChunk) => void) | null> = [];
|
||||||
|
const sendDone = new Map<number, () => void>();
|
||||||
|
const sendAgentChat = vi.fn(
|
||||||
|
(_sessionId: string, _prompt: string, onChunk: (chunk: ReplyChunk) => void) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
const index = emits.length;
|
||||||
|
emits.push((chunk: ReplyChunk) => {
|
||||||
|
onChunk(chunk);
|
||||||
|
if (chunk.kind === "final" || chunk.kind === "error") {
|
||||||
|
sendDone.get(index)?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sendDone.set(index, resolve);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
agent: {
|
||||||
|
launchAgentChat: vi.fn(),
|
||||||
|
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||||
|
sessionId,
|
||||||
|
scrollback: [],
|
||||||
|
})),
|
||||||
|
sendAgentChat,
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
},
|
||||||
|
sendAgentChat,
|
||||||
|
emit(index: number, chunk: ReplyChunk) {
|
||||||
|
const emit = emits[index];
|
||||||
|
if (!emit) throw new Error(`Missing emitter for send call #${index + 1}`);
|
||||||
|
emit(chunk);
|
||||||
|
},
|
||||||
|
emitterCount() {
|
||||||
|
return emits.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function expectedChatTime(ms: number): string {
|
function expectedChatTime(ms: number): string {
|
||||||
const parts = new Intl.DateTimeFormat(undefined, {
|
const parts = new Intl.DateTimeFormat(undefined, {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@ -1489,6 +1528,352 @@ describe("CustomAgentChatView", () => {
|
|||||||
expect(screen.getAllByText("idea_context_read")).toHaveLength(2);
|
expect(screen.getAllByText("idea_context_read")).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("surfaces second-turn streaming progress through a distinct callback after the first turn completed", async () => {
|
||||||
|
const harness = createStreamingAgentHarness();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent: harness.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(harness.agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
expect.any(Function),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||||
|
target: { value: "first request" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(harness.emitterCount()).toBe(1));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(0, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "providerNative",
|
||||||
|
kind: "tool",
|
||||||
|
stage: "started",
|
||||||
|
label: "shell",
|
||||||
|
toolName: "shell",
|
||||||
|
text: "Running first request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByText("Running first request")).toBeTruthy();
|
||||||
|
expect(screen.getByText("Tool").parentElement?.textContent).toContain("En cours");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(0, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "providerNative",
|
||||||
|
kind: "tool",
|
||||||
|
stage: "completed",
|
||||||
|
label: "shell",
|
||||||
|
toolName: "shell",
|
||||||
|
text: "Done first request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.emit(0, { kind: "final", content: "final first request" });
|
||||||
|
});
|
||||||
|
await screen.findByText("final first request");
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||||
|
target: { value: "second request" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(harness.emitterCount()).toBe(2));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(1, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "providerNative",
|
||||||
|
kind: "message",
|
||||||
|
stage: "delta",
|
||||||
|
label: "Analyse",
|
||||||
|
text: "Second request context loaded.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByText("Second request context loaded.")).toBeTruthy();
|
||||||
|
expect(screen.getByText("Progress").parentElement?.textContent).toContain("En cours");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(1, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "ideaLocal",
|
||||||
|
kind: "mcp",
|
||||||
|
stage: "started",
|
||||||
|
label: "idea_ticket_read",
|
||||||
|
toolName: "idea_ticket_read",
|
||||||
|
text: "Reading ticket for second request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByText("Reading ticket for second request")).toBeTruthy();
|
||||||
|
expect(screen.getAllByTestId("chat-tool-progress")).toHaveLength(2);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(1, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "ideaLocal",
|
||||||
|
kind: "mcp",
|
||||||
|
stage: "completed",
|
||||||
|
label: "idea_ticket_read",
|
||||||
|
toolName: "idea_ticket_read",
|
||||||
|
text: "Ticket read complete",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.emit(1, { kind: "final", content: "final second request" });
|
||||||
|
});
|
||||||
|
|
||||||
|
await screen.findByText("final second request");
|
||||||
|
expect(screen.getByText("Ticket read complete")).toBeTruthy();
|
||||||
|
expect(screen.getByText("Second request context loaded.")).toBeTruthy();
|
||||||
|
expect(screen.getAllByText("Terminé")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats same-tool progress on the second turn as fresh streaming activity with a distinct callback", async () => {
|
||||||
|
const harness = createStreamingAgentHarness();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent: harness.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(harness.agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
expect.any(Function),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||||
|
target: { value: "first request" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(harness.emitterCount()).toBe(1));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(0, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "ideaLocal",
|
||||||
|
kind: "mcp",
|
||||||
|
stage: "started",
|
||||||
|
label: "idea_context_read",
|
||||||
|
toolName: "idea_context_read",
|
||||||
|
text: "Reading shared context for first request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.emit(0, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "ideaLocal",
|
||||||
|
kind: "mcp",
|
||||||
|
stage: "completed",
|
||||||
|
label: "idea_context_read",
|
||||||
|
toolName: "idea_context_read",
|
||||||
|
text: "Context read complete for first request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.emit(0, { kind: "final", content: "done first request" });
|
||||||
|
});
|
||||||
|
await screen.findByText("done first request");
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||||
|
target: { value: "second request" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(harness.emitterCount()).toBe(2));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(1, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "ideaLocal",
|
||||||
|
kind: "mcp",
|
||||||
|
stage: "started",
|
||||||
|
label: "idea_context_read",
|
||||||
|
toolName: "idea_context_read",
|
||||||
|
text: "Reading shared context for second request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByText("Reading shared context for second request")).toBeTruthy();
|
||||||
|
expect(screen.getAllByText("idea_context_read")).toHaveLength(2);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(1, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "ideaLocal",
|
||||||
|
kind: "mcp",
|
||||||
|
stage: "completed",
|
||||||
|
label: "idea_context_read",
|
||||||
|
toolName: "idea_context_read",
|
||||||
|
text: "Context read complete for second request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.emit(1, { kind: "final", content: "done second request" });
|
||||||
|
});
|
||||||
|
|
||||||
|
await screen.findByText("done second request");
|
||||||
|
expect(screen.getByText("Context read complete for second request")).toBeTruthy();
|
||||||
|
expect(screen.getByText("Context read complete for first request")).toBeTruthy();
|
||||||
|
expect(screen.getAllByTestId("chat-tool-progress")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores a late first-turn progress chunk but still shows the real second-turn progress from the next callback", async () => {
|
||||||
|
const harness = createStreamingAgentHarness();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent: harness.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(harness.agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
expect.any(Function),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||||
|
target: { value: "first request" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(harness.emitterCount()).toBe(1));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(0, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "providerNative",
|
||||||
|
kind: "tool",
|
||||||
|
stage: "started",
|
||||||
|
label: "shell",
|
||||||
|
toolName: "shell",
|
||||||
|
text: "Running first request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.emit(0, { kind: "final", content: "done first request" });
|
||||||
|
});
|
||||||
|
await screen.findByText("done first request");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(0, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "providerNative",
|
||||||
|
kind: "tool",
|
||||||
|
stage: "completed",
|
||||||
|
label: "shell",
|
||||||
|
toolName: "shell",
|
||||||
|
text: "Late first request completion",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.queryByText("Late first request completion")).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||||
|
target: { value: "second request" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
await waitFor(() => expect(harness.emitterCount()).toBe(2));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(1, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "providerNative",
|
||||||
|
kind: "tool",
|
||||||
|
stage: "started",
|
||||||
|
label: "shell",
|
||||||
|
toolName: "shell",
|
||||||
|
text: "Running second request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(screen.getByText("Running second request")).toBeTruthy();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
harness.emit(1, {
|
||||||
|
kind: "progress",
|
||||||
|
progress: {
|
||||||
|
source: "providerNative",
|
||||||
|
kind: "tool",
|
||||||
|
stage: "completed",
|
||||||
|
label: "shell",
|
||||||
|
toolName: "shell",
|
||||||
|
text: "Done second request",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.emit(1, { kind: "final", content: "done second request" });
|
||||||
|
});
|
||||||
|
|
||||||
|
await screen.findByText("done second request");
|
||||||
|
expect(screen.getByText("Done second request")).toBeTruthy();
|
||||||
|
expect(screen.queryByText("Late first request completion")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders available pre-MCP progress before the first MCP card", async () => {
|
it("renders available pre-MCP progress before the first MCP card", async () => {
|
||||||
let emitChunk: ((chunk: unknown) => void) | null = null;
|
let emitChunk: ((chunk: unknown) => void) | null = null;
|
||||||
const agent = {
|
const agent = {
|
||||||
|
|||||||
Reference in New Issue
Block a user