feat(backend): modèle unifié streaming/progress/events provider-agnostic + pont app-tauri — foundation #156 (QA verte)

This commit is contained in:
2026-08-06 13:13:17 +02:00
parent 40fa551f32
commit 918116664c
14 changed files with 630 additions and 59 deletions

View File

@ -65,6 +65,13 @@ mod tests {
// -- Helpers ----------------------------------------------------------
fn without_progress(events: Vec<ReplyEvent>) -> Vec<ReplyEvent> {
events
.into_iter()
.filter(|event| !matches!(event, ReplyEvent::Progress { .. }))
.collect()
}
fn prepared_ctx() -> PreparedContext {
PreparedContext {
content: MarkdownDoc::new("# ctx"),
@ -188,7 +195,7 @@ mod tests {
.expect("parse ok");
assert_eq!(parsed.session_id.as_deref(), Some("conv-123"));
// L'init capte le session_id ET émet un heartbeat (vivacité non terminale, lot 1).
assert_eq!(parsed.events, vec![ReplyEvent::Heartbeat]);
assert_eq!(without_progress(parsed.events), vec![ReplyEvent::Heartbeat]);
}
/// §21 (LS2) : un `rate_limit_event` n'est PLUS un heartbeat — il porte désormais
@ -226,7 +233,7 @@ mod tests {
)
.expect("parse ok");
assert_eq!(
tool.events,
without_progress(tool.events),
vec![ReplyEvent::ToolActivity {
label: "Read".to_owned()
}]
@ -245,7 +252,7 @@ mod tests {
)
.expect("parse ok");
assert_eq!(
parsed.events,
without_progress(parsed.events),
vec![
ReplyEvent::TextDelta {
text: "un".to_owned()
@ -308,9 +315,15 @@ mod tests {
// turn.started / turn.completed ⇒ heartbeat (vivacité non terminale, lot 1).
let started = codex::parse_event(r#"{"type":"turn.started"}"#).expect("ok");
assert_eq!(started.events, vec![ReplyEvent::Heartbeat]);
assert_eq!(
without_progress(started.events),
vec![ReplyEvent::Heartbeat]
);
let completed = codex::parse_event(r#"{"type":"turn.completed","usage":{}}"#).expect("ok");
assert_eq!(completed.events, vec![ReplyEvent::Heartbeat]);
assert_eq!(
without_progress(completed.events),
vec![ReplyEvent::Heartbeat]
);
let msg = codex::parse_event(
r#"{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"fini"}}"#,
@ -857,7 +870,7 @@ mod tests {
.unwrap();
assert_eq!(t1.events, vec![ReplyEvent::TextDelta { text: "a".into() }]);
assert_eq!(
t2.events,
without_progress(t2.events),
vec![ReplyEvent::ToolActivity {
label: "Bash".into()
}]
@ -876,7 +889,7 @@ mod tests {
)
.unwrap();
assert_eq!(
parsed.events,
without_progress(parsed.events),
vec![
ReplyEvent::TextDelta { text: "un".into() },
ReplyEvent::ToolActivity {
@ -897,7 +910,7 @@ mod tests {
)
.unwrap();
assert_eq!(
parsed.events,
without_progress(parsed.events),
vec![ReplyEvent::ToolActivity {
label: "outil".into()
}]
@ -936,7 +949,7 @@ mod tests {
)
.unwrap();
assert_eq!(
r.events,
without_progress(r.events),
vec![ReplyEvent::ToolActivity {
label: "reasoning".into()
}]
@ -945,7 +958,7 @@ mod tests {
codex::parse_event(r#"{"type":"item.completed","item":{"id":"i1","type":"command"}}"#)
.unwrap();
assert_eq!(
c.events,
without_progress(c.events),
vec![ReplyEvent::ToolActivity {
label: "command".into()
}]
@ -1138,7 +1151,7 @@ mod tests {
let events: Vec<_> = s.send("x").await.expect("send").collect();
assert_eq!(
events,
without_progress(events.clone()),
vec![
ReplyEvent::Announcement {
text: "je regarde".into()
@ -1169,7 +1182,7 @@ mod tests {
let events: Vec<_> = s.send("x").await.expect("send").collect();
assert_eq!(
events,
without_progress(events.clone()),
vec![ReplyEvent::Final {
content: "résultat".into()
}]
@ -1237,7 +1250,7 @@ mod tests {
"le tap live publie chaque agent_message, y compris celui qui deviendra Final"
);
assert_eq!(
events,
without_progress(events.clone()),
vec![ReplyEvent::Final {
content: "résultat".into()
}],
@ -1630,7 +1643,7 @@ mod tests {
);
let events: Vec<ReplyEvent> = session.send("x").await.expect("send ok").collect();
assert_eq!(
events,
without_progress(events.clone()),
vec![
// L'init `system` émet un heartbeat (vivacité non terminale, lot 1).
ReplyEvent::Heartbeat,
@ -2449,7 +2462,7 @@ mod tests {
);
let events: Vec<ReplyEvent> = session.send("x").await.expect("send ok").collect();
assert_eq!(
events,
without_progress(events.clone()),
vec![
ReplyEvent::Heartbeat,
ReplyEvent::RateLimited {