fix(domain,application,infrastructure,backend,app-tauri,frontend): Stop demande agent préserve la conversation ouverte — arrêt sans fermeture/clôture du chat, états cohérents après cancel sur les 3 adapters de session, cellule et workstate synchronisés — #343 (QA verte)

This commit is contained in:
2026-09-13 10:03:36 +02:00
parent a1e8a6466f
commit c37638ff24
26 changed files with 1210 additions and 232 deletions

View File

@ -226,7 +226,15 @@ describe("TauriAgentGateway invoke payloads", () => {
});
it("cancelAgentChat invokes cancel_agent_chat without closing the session", async () => {
await new TauriAgentGateway().cancelAgentChat("chat-session-1");
const response = {
sessionId: "chat-session-1",
outcome: "cancelled" as const,
};
invoke.mockResolvedValueOnce(response);
await expect(
new TauriAgentGateway().cancelAgentChat("chat-session-1"),
).resolves.toEqual(response);
expect(invoke).toHaveBeenCalledWith("cancel_agent_chat", {
sessionId: "chat-session-1",

View File

@ -17,6 +17,7 @@ import { Channel, invoke } from "@tauri-apps/api/core";
import type {
Agent,
AgentContextDocument,
CancelAgentChatResult,
EffortSelection,
GatewayError,
ExecuteSlashCommandResult,
@ -364,8 +365,8 @@ export class TauriAgentGateway implements AgentGateway {
await invoke("close_agent_session", { sessionId });
}
async cancelAgentChat(sessionId: string): Promise<void> {
await invoke("cancel_agent_chat", { sessionId });
async cancelAgentChat(sessionId: string): Promise<CancelAgentChatResult> {
return invoke<CancelAgentChatResult>("cancel_agent_chat", { sessionId });
}
async reattach(

View File

@ -308,11 +308,16 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
baseUrl: "https://host",
fetchImpl: async (url, init) => {
calls.push({ url, init });
const command = JSON.parse((init as { body: string }).body).command;
const response =
command === "cancel_agent_chat"
? { sessionId: "chat-1", outcome: "cancelled" }
: undefined;
return {
ok: true,
status: 200,
json: async () => undefined,
text: async () => "",
json: async () => response,
text: async () => response ? JSON.stringify(response) : "",
};
},
}),
@ -402,7 +407,10 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
});
expect(chunks).toEqual([{ kind: "final", content: "Hello back" }]);
await gw.cancelAgentChat("chat-1");
await expect(gw.cancelAgentChat("chat-1")).resolves.toEqual({
sessionId: "chat-1",
outcome: "cancelled",
});
await gw.closeAgentChat("chat-1");
await gw.detachAgentChat("chat-1");

View File

@ -23,6 +23,7 @@ import type {
Agent,
AgentContextDocument,
AppExitWorkGuardState,
CancelAgentChatResult,
DomainEvent,
EffortSelection,
ExecuteSlashCommandResult,
@ -469,8 +470,8 @@ export class HttpAgentGateway implements AgentGateway {
await this.http.invoke("close_agent_session", { sessionId });
}
async cancelAgentChat(sessionId: string): Promise<void> {
await this.http.invoke("cancel_agent_chat", { sessionId });
async cancelAgentChat(sessionId: string): Promise<CancelAgentChatResult> {
return this.http.invoke<CancelAgentChatResult>("cancel_agent_chat", { sessionId });
}
}

View File

@ -9,6 +9,7 @@ import type {
AgentDrift,
AppExitWorkGuardState,
AgentProfile,
CancelAgentChatResult,
DiagnosticWarning,
DomainEvent,
EffortSelection,
@ -1133,13 +1134,14 @@ export class MockAgentGateway implements AgentGateway {
}
}
async cancelAgentChat(sessionId: string): Promise<void> {
async cancelAgentChat(sessionId: string): Promise<CancelAgentChatResult> {
if (!this.chatScrollback.has(sessionId)) {
throw {
code: "NOT_FOUND",
message: `agent chat session ${sessionId} not found`,
} as GatewayError;
}
return { sessionId, outcome: "cancelled" };
}
async reattach(