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 };
|
||||
}
|
||||
|
||||
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 {
|
||||
const parts = new Intl.DateTimeFormat(undefined, {
|
||||
hour: "2-digit",
|
||||
@ -1489,6 +1528,352 @@ describe("CustomAgentChatView", () => {
|
||||
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 () => {
|
||||
let emitChunk: ((chunk: unknown) => void) | null = null;
|
||||
const agent = {
|
||||
|
||||
Reference in New Issue
Block a user