From 8dfd3975f5a7c3847dde13f80a949e8fab0d3bae Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 8 Jul 2026 15:59:04 +0100 Subject: [PATCH 1/2] Stabilize encrypted MAv2 spawn request test (#31586) ## Why The encrypted MAv2 spawn test often reads the parent follow-up request before the child has sent its first request. The response mock records candidate requests before applying its specific matcher, so the test can see an empty `agent_message` list even though delivery happens a moment later. ## What changed Wait for the recorded child request that contains `agent_message`, using the same short bounded polling pattern already used in this test module. The exact encrypted payload and communication-log assertions stay unchanged. This is test-only: it does not add product delays, loosen the assertion, or change multi-agent behavior. No follow-up is expected. --- .../tests/suite/subagent_notifications.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 7424d3f360..fdaed25623 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -1120,10 +1120,22 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result test.submit_turn(TURN_1_PROMPT).await?; - let child_request = wait_for_requests(&child_request_log) - .await? - .pop() - .expect("child request"); + // The response mock records candidate requests before its request matcher runs, so wait for + // the child request instead of assuming the latest recorded request is already it. + let deadline = Instant::now() + Duration::from_secs(2); + let child_request = loop { + if let Some(request) = child_request_log + .requests() + .into_iter() + .find(|request| !request.inputs_of_type("agent_message").is_empty()) + { + break request; + } + if Instant::now() >= deadline { + anyhow::bail!("timed out waiting for child agent message request"); + } + sleep(Duration::from_millis(10)).await; + }; assert_eq!( strip_metadata_from_json(Value::Array(child_request.inputs_of_type("agent_message"))), Value::Array(vec![json!({ From f17a57b7d5a1d1002bca37a57228d8214b862f00 Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 8 Jul 2026 15:59:36 +0100 Subject: [PATCH 2/2] Stabilize remote compaction parity against dynamic skill catalogs (#31585) ## Why The remote compaction parity test compares legacy and v2 sessions created with separate temporary homes. Those sessions can discover different model-visible skill catalogs, so the request comparison can fail even when the compaction and service-tier behavior matches. This is the most frequent retry-saved full-CI failure in the recent JUnit history. ## What changed Normalize only the contents of `` before comparing the captured requests. The opening and closing tags remain in the comparison, so the test still catches a missing or misplaced skills block. The service-tier, compacted input, follow-up request, and replacement-history assertions are unchanged. A focused normalizer test covers the new behavior. ## Scope This is test-only. It does not change runtime compaction or skill behavior. Exact skill-catalog rendering remains covered by the dedicated skills tests. --- .../core/tests/suite/compact_remote_parity.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/codex-rs/core/tests/suite/compact_remote_parity.rs b/codex-rs/core/tests/suite/compact_remote_parity.rs index 6f56ce016d..d4042a8c2e 100644 --- a/codex-rs/core/tests/suite/compact_remote_parity.rs +++ b/codex-rs/core/tests/suite/compact_remote_parity.rs @@ -941,6 +941,19 @@ fn normalize_string(value: &str) -> String { normalize_tmp_prefix_before_marker(&mut text, "/skills/"); normalize_tmp_prefix_before_marker(&mut text, "\\skills\\"); + let skills_open_tag = ""; + let skills_close_tag = ""; + let mut search_start = 0; + while let Some(relative_start) = text[search_start..].find(skills_open_tag) { + let body_start = search_start + relative_start + skills_open_tag.len(); + let Some(relative_end) = text[body_start..].find(skills_close_tag) else { + break; + }; + let body_end = body_start + relative_end; + text.replace_range(body_start..body_end, "\n...\n"); + search_start = body_start + "\n...\n".len() + skills_close_tag.len(); + } + let mut search_start = 0; let wall_time_prefix = "Wall time: "; let wall_time_suffix = " seconds"; @@ -964,6 +977,19 @@ fn normalize_string(value: &str) -> String { text } +#[test] +fn normalize_string_rewrites_dynamic_skill_instructions() { + let text = normalize_string( + "before\n\n## Skills\n- demo: Dynamic description\n\ + \nafter", + ); + + assert_eq!( + text, + "before\n\n...\n\nafter" + ); +} + fn is_uuid_like(value: &str) -> bool { let bytes = value.as_bytes(); bytes.len() == 36