fix(domain,application,infrastructure,app-tauri,frontend): conversation_id redevient un pivot logique IdeA/log/handoff, reprise provider réservée à la sélection explicite — lifecycle: LaunchAgentInput::conversation_id documenté comme sélection explicite (Some = reprise native si disponible sinon handoff portable, None = conversation provider neuve), plus de copie implicite du pivot logique de la leaf au reboot ; fallback silencieux: provider_resume_fallback résolu via resolve_handoff et câblé jusqu'aux adaptateurs Claude/Codex/OpenCode via AgentSessionFactory::create resume_fallback_preamble + with_resume_fallback_preamble — le préambule reste dormant si la session native répond et n'est injecté que si la session provider s'avère introuvable/périmée (fake stale-then-fresh en test, relance fraîche sans erreur utilisateur) ; app-tauri launch_agent: contrat documenté, l'activation ordinaire d'une cellule doit envoyer None même si la leaf conserve un pivot logique d'avant reboot ; frontend CustomAgentChatView: freshOnOpen réinitialise l'état conversation complet (scrollback, view state, conversationId publié, bannière reprise, pagination historique) quand le runtime structuré n'existe plus au remontage — pas de fausse « Conversation reprise » après reboot ; tests: structured_launch_d3 étendu, structured_session_d0, agent_lifecycle structured, CustomAgentChatView freshOnOpen — #372 (QA verte: cargo check --tests -p domain -p application -p infrastructure, cargo test -p application --test structured_launch_d3, cargo test -p domain --test structured_session_d0, cargo test -p application --test agent_lifecycle structured, npm test -- CustomAgentChatView, npm run typecheck, git diff --check ; fichiers .ideai/* et scripts/ hors périmètre ; aucun push)
This commit is contained in:
@ -6751,6 +6751,79 @@ describe("CustomAgentChatView", () => {
|
||||
));
|
||||
});
|
||||
|
||||
it("starts fresh after reboot when both persisted session and conversation are stale (#372)", async () => {
|
||||
const conversation = {
|
||||
readPage: vi.fn(),
|
||||
};
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(async () => ({
|
||||
sessionId: "fresh-session",
|
||||
assignedConversationId: "fresh-conversation",
|
||||
})),
|
||||
reattachAgentChat: vi.fn()
|
||||
.mockRejectedValueOnce({ code: "NOT_FOUND", message: "session not found" })
|
||||
.mockRejectedValueOnce({ code: "NOT_FOUND", message: "session not found" })
|
||||
.mockResolvedValueOnce({ sessionId: "fresh-session", scrollback: [] }),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => ({
|
||||
sessionId: "fresh-session",
|
||||
outcome: "cancelled" as const,
|
||||
})),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
const onSessionId = vi.fn();
|
||||
const onConversationId = vi.fn();
|
||||
|
||||
render(
|
||||
<DIProvider gateways={{
|
||||
agent,
|
||||
conversation,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
} as unknown as Gateways}>
|
||||
<CustomAgentChatView
|
||||
freshOnOpen
|
||||
projectId="project-1"
|
||||
agentId="agent-1"
|
||||
agentName="Worker"
|
||||
profile={profile}
|
||||
cwd="/repo"
|
||||
nodeId="node-1"
|
||||
sessionId="stale-session"
|
||||
conversationId="stale-conversation"
|
||||
onSessionId={onSessionId}
|
||||
onConversationId={onConversationId}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(agent.launchAgentChat).toHaveBeenCalledTimes(1));
|
||||
expect(agent.launchAgentChat).toHaveBeenCalledWith("project-1", "agent-1", {
|
||||
cwd: "/repo",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
conversationId: undefined,
|
||||
freshStart: true,
|
||||
nodeId: "node-1",
|
||||
});
|
||||
expect(conversation.readPage).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("Conversation reprise")).toBeNull();
|
||||
expect(screen.queryByText("session not found")).toBeNull();
|
||||
expect(await screen.findByText("Nouvelle conversation")).toBeTruthy();
|
||||
expect(onSessionId).toHaveBeenCalledWith(null);
|
||||
expect(onConversationId).toHaveBeenCalledWith(null);
|
||||
expect(onConversationId).toHaveBeenCalledWith("fresh-conversation");
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: "Ready now" } });
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("button", { name: "Envoyer" }).hasAttribute("disabled")).toBe(false),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith(
|
||||
"fresh-session",
|
||||
"Ready now",
|
||||
expect.any(Function),
|
||||
));
|
||||
});
|
||||
|
||||
it("uses /clean for a new conversation and disables conversation changes during a turn", async () => {
|
||||
let finishSend: (() => void) | undefined;
|
||||
const agent = {
|
||||
|
||||
@ -2238,6 +2238,20 @@ export function CustomAgentChatView({
|
||||
setExternalSessionId(null);
|
||||
setLiveSessionAttached(false);
|
||||
publishSessionId(null);
|
||||
if (freshOnOpen) {
|
||||
// After an application reboot, the leaf can still contain both
|
||||
// ids even though the structured runtime no longer exists. A
|
||||
// normal cell opening must not turn that stale runtime pivot
|
||||
// into an implicit conversation resume (#372).
|
||||
scrollbackChunkCountRef.current = 0;
|
||||
applyChatViewState(emptyChatViewState());
|
||||
publishConversationId(null);
|
||||
setSavedConversationLoadState("idle");
|
||||
setResumedConversation(false);
|
||||
setHistoryHasMore(false);
|
||||
setHistoryNextAnchor(undefined);
|
||||
setHistoryError(null);
|
||||
}
|
||||
}
|
||||
console.debug("[ticket149] openOrAttach:reattach-existing:not-found", {
|
||||
timestamp: new Date().toISOString(),
|
||||
@ -2397,6 +2411,7 @@ export function CustomAgentChatView({
|
||||
reattachStructuredSession,
|
||||
recoverStructuredSession,
|
||||
publishSessionId,
|
||||
publishConversationId,
|
||||
applyChatViewState,
|
||||
conversation,
|
||||
agent,
|
||||
|
||||
Reference in New Issue
Block a user