fix(cli): progress agent_message émis en live + anti-doublon running/done par progressId — #187 (QA verte)
- Codex: les items agent_message sont désormais tapés en live comme progress (Message/Delta) dès item.completed, au lieu d'être bufferisés dans pending_final jusqu'à l'événement tool/MCP suivant. - Corrélation stable progressId (codex:item:<id>) ajoutée sur ReplyProgress, propagée jusqu'au DTO (ReplyProgressDto.progressId) et au frontend ; le ProgressSettler et la vue procèdent par upsert running -> done au lieu d'empiler des lignes dupliquées. - Tests: chat_bridge (settler par progressId), dto_chat, session codex, CustomAgentChatView (upsert par progressId).
This commit is contained in:
@ -1259,6 +1259,92 @@ describe("CustomAgentChatView", () => {
|
||||
expect(screen.getByText("Progress").parentElement?.textContent).toContain("done");
|
||||
});
|
||||
|
||||
it("updates message progress by progressId instead of appending running and done rows", async () => {
|
||||
let emitChunk: ((chunk: unknown) => void) | null = null;
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback: [],
|
||||
})),
|
||||
sendAgentChat: vi.fn(
|
||||
(_sessionId: string, _prompt: string, onChunk: (chunk: unknown) => void) =>
|
||||
new Promise<void>((resolve) => {
|
||||
emitChunk = (chunk: unknown) => {
|
||||
onChunk(chunk);
|
||||
if ((chunk as { kind?: string }).kind === "final") resolve();
|
||||
};
|
||||
}),
|
||||
),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
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: "ship progress ids" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(() => expect(emitChunk).not.toBeNull());
|
||||
|
||||
const progress = (stage: "delta" | "completed", text: string) => ({
|
||||
kind: "progress" as const,
|
||||
progress: {
|
||||
progressId: "codex:message:item-1",
|
||||
source: "providerNative" as const,
|
||||
kind: "message" as const,
|
||||
stage,
|
||||
label: "Analyse",
|
||||
text,
|
||||
},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
emitChunk?.(progress("delta", "Lecture du contexte disponible."));
|
||||
});
|
||||
|
||||
expect(screen.getAllByTestId("chat-progress-status")).toHaveLength(1);
|
||||
expect(screen.getByText("Lecture du contexte disponible.")).toBeTruthy();
|
||||
expect(screen.getByText("Progress").parentElement?.textContent).toContain("running");
|
||||
|
||||
act(() => {
|
||||
emitChunk?.(progress("completed", "Contexte lu."));
|
||||
});
|
||||
|
||||
expect(screen.getAllByTestId("chat-progress-status")).toHaveLength(1);
|
||||
expect(screen.queryByText("Lecture du contexte disponible.")).toBeNull();
|
||||
expect(screen.getByText("Contexte lu.")).toBeTruthy();
|
||||
expect(screen.getByText("Progress").parentElement?.textContent).toContain("done");
|
||||
});
|
||||
|
||||
it("does not append late progress chunks after the final answer", async () => {
|
||||
let emitChunk: ((chunk: unknown) => void) | null = null;
|
||||
const agent = {
|
||||
|
||||
Reference in New Issue
Block a user