fix(chat): reprise robuste au retour d'un layout non conversationnel (composer verrouillé selon l'état réel, replay complet du scrollback, plus de fausse terminaison 'no reply') (#190)
This commit is contained in:
@ -59,6 +59,14 @@ function addPluginCommand(
|
||||
});
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
const promise = new Promise<T>((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe("CustomAgentChatView", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@ -174,8 +182,136 @@ describe("CustomAgentChatView", () => {
|
||||
expect(await screen.findByText("Background tool")).toBeTruthy();
|
||||
expect(screen.getByText("Completed away")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText("Reprise effectuée: 2 événements rattrapés pendant l'absence."),
|
||||
).toBeTruthy();
|
||||
screen.queryByText(/Reprise effectuée:/),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the composer locked on layout return until reattach replay settles (#190)", async () => {
|
||||
let scrollback: ReplyChunk[] = [
|
||||
{ kind: "userPrompt" as const, text: "Initial prompt" },
|
||||
{ kind: "final" as const, content: "Initial reply" },
|
||||
];
|
||||
const secondReattach = deferred<{ sessionId: string; scrollback: ReplyChunk[] }>();
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi
|
||||
.fn()
|
||||
.mockImplementationOnce(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback,
|
||||
}))
|
||||
.mockImplementationOnce(() => secondReattach.promise),
|
||||
detachAgentChat: vi.fn(async () => {}),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
const props = {
|
||||
projectId: "project-1",
|
||||
agentId: "agent-1",
|
||||
agentName: "Worker",
|
||||
profile,
|
||||
cwd: "/repo",
|
||||
nodeId: "node-1",
|
||||
sessionId: "chat-session-1",
|
||||
conversationId: "conversation-1",
|
||||
onSessionId: vi.fn(),
|
||||
onConversationId: vi.fn(),
|
||||
};
|
||||
|
||||
const first = render(
|
||||
<DIProvider
|
||||
gateways={{
|
||||
agent,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
} as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView {...props} />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Initial reply")).toBeTruthy();
|
||||
first.unmount();
|
||||
await waitFor(() => expect(agent.detachAgentChat).toHaveBeenCalledWith("chat-session-1"));
|
||||
|
||||
scrollback = [
|
||||
{ kind: "userPrompt" as const, text: "Initial prompt" },
|
||||
{ kind: "final" as const, content: "Initial reply" },
|
||||
{ kind: "userPrompt" as const, text: "Question during graph view" },
|
||||
{ kind: "textDelta" as const, text: "Partial answer while away" },
|
||||
];
|
||||
|
||||
render(
|
||||
<DIProvider
|
||||
gateways={{
|
||||
agent,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
} as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView {...props} />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Initial reply")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Envoyer" }).hasAttribute("disabled")).toBe(true);
|
||||
expect(screen.getByLabelText(/message CLI custom/).hasAttribute("disabled")).toBe(true);
|
||||
|
||||
act(() => {
|
||||
secondReattach.resolve({ sessionId: "chat-session-1", scrollback });
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Question during graph view")).toBeTruthy();
|
||||
expect(screen.getByText("Partial answer while away")).toBeTruthy();
|
||||
expect(screen.queryByText(/Reprise effectuée:/)).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Stop" })).toBeTruthy();
|
||||
expect(screen.getByLabelText(/message CLI custom/).hasAttribute("disabled")).toBe(true);
|
||||
});
|
||||
|
||||
it("drops a stale no-response chunk when replay contains a later real answer (#190)", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback: [
|
||||
{ kind: "userPrompt" as const, text: "resume the turn" },
|
||||
{
|
||||
kind: "error" as const,
|
||||
message: "Le modèle n'a renvoyé aucune réponse.",
|
||||
},
|
||||
{ kind: "textDelta" as const, text: "Real answer " },
|
||||
{ kind: "final" as const, content: "Real answer completed" },
|
||||
],
|
||||
})),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
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>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Real answer completed")).toBeTruthy();
|
||||
expect(screen.getByText("Real answer")).toBeTruthy();
|
||||
expect(screen.queryByText("Le modèle n'a renvoyé aucune réponse.")).toBeNull();
|
||||
});
|
||||
|
||||
it("stops the live agent with cascade feedback and releases the structured session", async () => {
|
||||
|
||||
Reference in New Issue
Block a user