fix(ticket-assistant): rendre visible l'échec « aucune réponse » de l'assistant de ticket (#60)
Introduit un variant terminal `ReplyEvent::Error { message }` (domain/ports)
propagé jusqu'au chat : l'adapter OpenAI-compat récupère `reasoning_content`
quand `content` est vide, et un `Final` vide est converti en `Error` visible
plutôt qu'un tour silencieux qui pend. Le front (`ReplyChunk` + useTicketAssistant)
rend ce message d'erreur dans la cellule.
- domain: variant `ReplyEvent::Error`, bras non terminal (readiness, structured drain)
- infra/session: parse `reasoning_content` (delta/completion), fallback visible
- app-tauri: mapping ReplyEvent::Error -> ReplyChunk::Error, Final vide -> Error
- frontend: ReplyChunk `error`, rendu dans useTicketAssistant (+ test .test.tsx)
NB: commit sur la feature branch, PAS un merge. #60 reste inProgress.
Les 10 tests `cargo test -p infrastructure --lib session` échouent SOUS SANDBOX
uniquement (bind loopback 127.0.0.1:0 interdit -> Os PermissionDenied), pas une
régression : revérification hors-sandbox requise avant tout merge vers develop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -887,6 +887,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `item.type == error` ⇒ Error terminal visible, avec message réel.
|
||||
#[test]
|
||||
fn codex_error_item_is_error_event_with_real_message() {
|
||||
let r = codex::parse_event(
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"error","text":"auth failed"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r.events,
|
||||
vec![ReplyEvent::Error {
|
||||
message: "auth failed".into()
|
||||
}]
|
||||
);
|
||||
|
||||
let nested = codex::parse_event(
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"error","error":{"message":"quota exceeded"}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
nested.events,
|
||||
vec![ReplyEvent::Error {
|
||||
message: "quota exceeded".into()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
/// `item.type == error` sans champ exploitable ⇒ fallback stable.
|
||||
#[test]
|
||||
fn codex_error_item_without_message_uses_fallback() {
|
||||
let r =
|
||||
codex::parse_event(r#"{"type":"item.completed","item":{"id":"i0","type":"error"}}"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r.events,
|
||||
vec![ReplyEvent::Error {
|
||||
message: "Codex a renvoyé une erreur.".into()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
/// `agent_message` ⇒ Final (porte le texte de réponse).
|
||||
#[test]
|
||||
fn codex_agent_message_is_final() {
|
||||
@ -982,6 +1022,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_error_item_surfaces_error_without_final() {
|
||||
let fake = FakeCli::printing(&[
|
||||
r#"{"type":"thread.started","thread_id":"c"}"#,
|
||||
r#"{"type":"turn.started"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"error","text":"auth failed"}}"#,
|
||||
r#"{"type":"turn.completed","usage":{}}"#,
|
||||
]);
|
||||
let s = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let events: Vec<_> = s.send("x").await.expect("send").collect();
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
ReplyEvent::Error { message } if message.contains("auth failed")
|
||||
)));
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, ReplyEvent::Final { .. })),
|
||||
"Codex error must not be fabricated as a successful Final"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_two_agent_messages_preserve_first_as_announcement_and_last_as_final() {
|
||||
let fake = FakeCli::printing(&[
|
||||
|
||||
Reference in New Issue
Block a user