L'adaptateur Codex n'émettait Progress/ToolActivity qu'à item.completed. On capte désormais conversation.item.added / item.started / response.output_item.added (stage Started), item.updated (Delta) et conversation.item.done / response.output_item.done (Completed), en préservant le nom réel de l'outil (name / function.name / function_call.name). Les outils idea_* sont taggés ReplyProgressSource::IdeaLocal / Kind::Mcp pour les distinguer du tooling provider. Corrige #93 (l'activité MCP idea_ask_agent devient visible avant le Final, nom non corrompu) et #171 (pipeline progress -> UI temps réel) ; base du rendu live #58. Tests : 3 unitaires (started live, tag IdeaLocal/Mcp, done avec nom) + assertion d'intégration (MCP started tapé avant le Final). QA : verte (live).
This commit is contained in:
@ -50,7 +50,8 @@ mod tests {
|
||||
use domain::ids::ProfileId;
|
||||
use domain::ports::{
|
||||
AgentSession, AgentSessionError, AgentSessionFactory, ContextInjectionPlan,
|
||||
PreparedContext, ReplyEvent, SessionPlan,
|
||||
PreparedContext, ReplyEvent, ReplyProgressKind, ReplyProgressSource, ReplyProgressStage,
|
||||
SessionPlan,
|
||||
};
|
||||
use domain::profile::{AgentProfile, ContextInjection, HttpChatConfig, StructuredAdapter};
|
||||
use domain::project::ProjectPath;
|
||||
@ -965,6 +966,88 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_conversation_item_added_emits_live_started_progress() {
|
||||
let r = codex::parse_event(
|
||||
r#"{"type":"conversation.item.added","item":{"id":"call_1","type":"function_call","name":"read_file","arguments":"{}"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
r.events.first(),
|
||||
Some(ReplyEvent::Progress { progress })
|
||||
if progress.source == ReplyProgressSource::ProviderNative
|
||||
&& progress.kind == ReplyProgressKind::Tool
|
||||
&& progress.stage == ReplyProgressStage::Started
|
||||
&& progress.label == "read_file"
|
||||
&& progress.tool_name.as_deref() == Some("read_file")
|
||||
&& progress.native_event.as_deref() == Some("conversation.item.added")
|
||||
),
|
||||
"Codex item.added must surface tool start immediately: {:?}",
|
||||
r.events
|
||||
);
|
||||
assert!(
|
||||
r.events.iter().any(
|
||||
|event| matches!(event, ReplyEvent::ToolActivity { label } if label == "read_file")
|
||||
),
|
||||
"tool activity remains available for existing UI mapping"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_idea_tool_is_tagged_as_local_mcp_progress() {
|
||||
let r = codex::parse_event(
|
||||
r#"{"type":"conversation.item.added","item":{"id":"call_1","type":"function_call","name":"idea_ask_agent","arguments":"{}"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
r.events.first(),
|
||||
Some(ReplyEvent::Progress { progress })
|
||||
if progress.source == ReplyProgressSource::IdeaLocal
|
||||
&& progress.kind == ReplyProgressKind::Mcp
|
||||
&& progress.stage == ReplyProgressStage::Started
|
||||
&& progress.label == "idea_ask_agent"
|
||||
&& progress.tool_name.as_deref() == Some("idea_ask_agent")
|
||||
&& progress.native_event.as_deref() == Some("conversation.item.added")
|
||||
),
|
||||
"Codex MCP tool start must be tagged as IdeaLocal/Mcp: {:?}",
|
||||
r.events
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_conversation_item_done_completes_tool_with_name() {
|
||||
let r = codex::parse_event(
|
||||
r#"{"type":"conversation.item.done","item":{"id":"call_1","type":"function_call","name":"idea_ask_agent","status":"completed"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
r.events.first(),
|
||||
Some(ReplyEvent::Progress { progress })
|
||||
if progress.source == ReplyProgressSource::IdeaLocal
|
||||
&& progress.kind == ReplyProgressKind::Mcp
|
||||
&& progress.stage == ReplyProgressStage::Completed
|
||||
&& progress.label == "idea_ask_agent"
|
||||
&& progress.tool_name.as_deref() == Some("idea_ask_agent")
|
||||
&& progress.native_event.as_deref() == Some("conversation.item.done")
|
||||
),
|
||||
"Codex MCP tool completion must keep the real tool name: {:?}",
|
||||
r.events
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
r.events.get(1),
|
||||
Some(ReplyEvent::ToolActivity { label }) if label == "idea_ask_agent"
|
||||
),
|
||||
"completion still emits ToolActivity compatibility event"
|
||||
);
|
||||
}
|
||||
|
||||
/// `item.type == error` ⇒ Error terminal visible, avec message réel.
|
||||
#[test]
|
||||
fn codex_error_item_is_error_event_with_real_message() {
|
||||
@ -1220,6 +1303,7 @@ mod tests {
|
||||
let fake = FakeCli::printing(&[
|
||||
r#"{"type":"thread.started","thread_id":"c"}"#,
|
||||
r#"{"type":"turn.started"}"#,
|
||||
r#"{"type":"conversation.item.added","item":{"id":"tool0","type":"function_call","name":"idea_ask_agent","arguments":"{}"}}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"tool0","type":"command","text":"cargo test"}}"#,
|
||||
r#"{"type":"turn.completed"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i1","type":"agent_message","text":"résultat"}}"#,
|
||||
@ -1243,6 +1327,17 @@ mod tests {
|
||||
matches!(live.first(), Some(ReplyEvent::Progress { progress }) if progress.native_event.as_deref() == Some("turn.started")),
|
||||
"le démarrage du tour provider doit être tapé live en premier"
|
||||
);
|
||||
assert!(
|
||||
live.iter().any(|event| matches!(
|
||||
event,
|
||||
ReplyEvent::Progress { progress }
|
||||
if progress.source == ReplyProgressSource::IdeaLocal
|
||||
&& progress.kind == ReplyProgressKind::Mcp
|
||||
&& progress.stage == ReplyProgressStage::Started
|
||||
&& progress.tool_name.as_deref() == Some("idea_ask_agent")
|
||||
)),
|
||||
"le début MCP Codex doit être tapé live avant le Final"
|
||||
);
|
||||
assert!(
|
||||
live.iter().any(
|
||||
|event| matches!(event, ReplyEvent::ToolActivity { label } if label == "command")
|
||||
|
||||
Reference in New Issue
Block a user