fix(chat): le contenu partiel streamé n'est plus masqué par l'erreur terminale de finalisation — #239
assistantFailureText conserve le texte déjà streamé puis classe l'échec (sans contenu, tronqué, fin non reçue, transitoire) avec conseil de reprise adapté, au lieu d'écraser la bulle par « Échec — … finalisée. ».
This commit is contained in:
@ -94,6 +94,32 @@ describe("useTicketAssistant — ticket #60 (no-reply terminal)", () => {
|
|||||||
expect(last.role).toBe("assistant");
|
expect(last.role).toBe("assistant");
|
||||||
expect(last.pending).toBeFalsy();
|
expect(last.pending).toBeFalsy();
|
||||||
expect(last.text).toContain("Le modèle n'a renvoyé aucune réponse.");
|
expect(last.text).toContain("Le modèle n'a renvoyé aucune réponse.");
|
||||||
|
expect(last.text).toContain("Échec sans contenu");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves streamed partial content when the terminal error says finalization failed", async () => {
|
||||||
|
const gw = new FakeTicketGateway();
|
||||||
|
const { result } = await openAndSend(gw);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
gw.captured!({ kind: "textDelta", text: "Réponse partielle utile." });
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
gw.captured!({
|
||||||
|
kind: "error",
|
||||||
|
message: "La réponse du modèle n'a pas pu être finalisée.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.busy).toBe(false));
|
||||||
|
const last = result.current.turns.at(-1)!;
|
||||||
|
expect(last.role).toBe("assistant");
|
||||||
|
expect(last.pending).toBeFalsy();
|
||||||
|
expect(last.text).toContain("Réponse partielle utile.");
|
||||||
|
expect(last.text).toContain("Réponse partielle — fin non reçue.");
|
||||||
|
expect(last.text).toContain(
|
||||||
|
"demandez à l'assistant de continuer à partir de la réponse partielle",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("a non-empty final renders the answer and clears busy (nominal, no regression)", async () => {
|
it("a non-empty final renders the answer and clears busy (nominal, no regression)", async () => {
|
||||||
|
|||||||
@ -52,6 +52,31 @@ function describe(e: unknown): string {
|
|||||||
return String(e);
|
return String(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assistantFailureText(message: string, partialText: string): string {
|
||||||
|
const partial = partialText.trim();
|
||||||
|
const normalized = message.toLowerCase();
|
||||||
|
if (!partial) {
|
||||||
|
return [
|
||||||
|
`Échec sans contenu — ${message}`,
|
||||||
|
"Reprise: renvoyez le message ou reformulez-le en plusieurs parties.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const label =
|
||||||
|
normalized.includes("tronqu") || normalized.includes("truncat")
|
||||||
|
? "Réponse tronquée."
|
||||||
|
: normalized.includes("finalis") || normalized.includes("fin ")
|
||||||
|
? "Réponse partielle — fin non reçue."
|
||||||
|
: "Réponse partielle — échec transitoire possible.";
|
||||||
|
return [
|
||||||
|
partialText,
|
||||||
|
"",
|
||||||
|
`Échec — ${label}`,
|
||||||
|
message,
|
||||||
|
"Reprise: demandez à l'assistant de continuer à partir de la réponse partielle ci-dessus.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
export function useTicketAssistant(
|
export function useTicketAssistant(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
issueRef: string,
|
issueRef: string,
|
||||||
@ -136,12 +161,11 @@ export function useTicketAssistant(
|
|||||||
// Best-effort inline activity badge.
|
// Best-effort inline activity badge.
|
||||||
next[i] = { ...next[i], text: `${next[i].text}\n· ${chunk.label}` };
|
next[i] = { ...next[i], text: `${next[i].text}\n· ${chunk.label}` };
|
||||||
} else if (chunk.kind === "error") {
|
} else if (chunk.kind === "error") {
|
||||||
// Terminal fallback (ticket #60): the model returned no usable answer.
|
// Terminal fallback: preserve any streamed content, then classify the
|
||||||
// Freeze the turn with the readable explanation so the bubble is never
|
// failure so a partial answer is not hidden by the terminal error.
|
||||||
// left empty and pending — the assistant no longer hangs silently.
|
|
||||||
next[i] = {
|
next[i] = {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
text: `⚠️ ${chunk.message}`,
|
text: assistantFailureText(chunk.message, next[i].text),
|
||||||
pending: false,
|
pending: false,
|
||||||
};
|
};
|
||||||
} else if (chunk.kind === "final") {
|
} else if (chunk.kind === "final") {
|
||||||
|
|||||||
Reference in New Issue
Block a user