Files
codex/codex-rs/core/src/event_mapping_tests.rs
T
Eric Traut 479a14cf59 [2 of 2] Finish moving goal runtime to extension (#26548)
## Stack

1. [#26547](https://github.com/openai/codex/pull/26547) - [1 of 2] Align
goal extension with core behavior
2. [#26548](https://github.com/openai/codex/pull/26548) - [2 of 2] Move
goal runtime to extension

## Why

This PR completes the switch of the goal behavior to the
extension-backed runtime and removes the old core goal implementation.

## What Changed

- Installs the goal extension for app-server `ThreadManager` sessions.
- Routes app-server thread goal `get`, `set`, and `clear` through
`GoalService`.
- Uses thread-idle lifecycle emission after goal resume and snapshot
ordering so the extension can decide whether to continue the goal.
- Forwards extension goal updates through a FIFO async app-server
notification path so backpressure does not drop them or reorder updates.
- Keeps review turns from enabling goal runtime behavior.
- Plans extension tools before dynamic tools so built-in goal tool names
keep their old precedence when goals are enabled.
- Removes the old core goal runtime, core goal tool handlers, and core
goal tool specs.
- Updates tests that were coupled to the core-owned goal runtime while
leaving the legacy `<goal_context>` compatibility path in core for old
threads.
- Removes the stale cargo-shear ignore now that `codex-goal-extension`
is used by the workspace.
- Keeps realtime event matching exhaustive after removing the old
goal-specific realtime text path.


## Validation

- Ran manual `/goal` runs in TUI. Validated time accounting matched
wall-clock time and goal lifecycle state transitions.
2026-06-05 14:17:30 -07:00

532 lines
17 KiB
Rust

use super::parse_turn_item;
use crate::context::ContextualUserFragment;
use crate::context::InternalContextSource;
use crate::context::InternalModelContextFragment;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::HookPromptFragment;
use codex_protocol::items::TurnItem;
use codex_protocol::items::WebSearchItem;
use codex_protocol::items::build_hook_prompt_message;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::WebSearchAction;
use codex_protocol::user_input::UserInput;
use pretty_assertions::assert_eq;
#[test]
fn parses_user_message_with_text_and_two_images() {
let img1 = "https://example.com/one.png".to_string();
let img2 = "https://example.com/two.jpg".to_string();
let item = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "Hello world".to_string(),
},
ContentItem::InputImage {
image_url: img1.clone(),
detail: Some(DEFAULT_IMAGE_DETAIL),
},
ContentItem::InputImage {
image_url: img2.clone(),
detail: Some(DEFAULT_IMAGE_DETAIL),
},
],
phase: None,
};
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
match turn_item {
TurnItem::UserMessage(user) => {
let expected_content = vec![
UserInput::Text {
text: "Hello world".to_string(),
text_elements: Vec::new(),
},
UserInput::Image {
image_url: img1,
detail: Some(DEFAULT_IMAGE_DETAIL),
},
UserInput::Image {
image_url: img2,
detail: Some(DEFAULT_IMAGE_DETAIL),
},
];
assert_eq!(user.content, expected_content);
}
other => panic!("expected TurnItem::UserMessage, got {other:?}"),
}
}
#[test]
fn skips_local_image_label_text() {
let image_url = "data:image/png;base64,abc".to_string();
let label = r#"<image name=[Image #1] path="/tmp/local.png">"#.to_string();
let user_text = "Please review this image.".to_string();
let item = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText { text: label },
ContentItem::InputImage {
image_url: image_url.clone(),
detail: Some(DEFAULT_IMAGE_DETAIL),
},
ContentItem::InputText {
text: "</image>".to_string(),
},
ContentItem::InputText {
text: user_text.clone(),
},
],
phase: None,
};
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
match turn_item {
TurnItem::UserMessage(user) => {
let expected_content = vec![
UserInput::Image {
image_url,
detail: Some(DEFAULT_IMAGE_DETAIL),
},
UserInput::Text {
text: user_text,
text_elements: Vec::new(),
},
];
assert_eq!(user.content, expected_content);
}
other => panic!("expected TurnItem::UserMessage, got {other:?}"),
}
}
#[test]
fn parses_assistant_message_input_text_for_backward_compatibility() {
let item = ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::InputText {
text: "author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: continue"
.to_string(),
}],
phase: None,
};
let turn_item = parse_turn_item(&item).expect("expected assistant message turn item");
match turn_item {
TurnItem::AgentMessage(message) => {
let rendered = message
.content
.into_iter()
.map(|content| {
let AgentMessageContent::Text { text } = content;
text
})
.collect::<Vec<_>>();
assert_eq!(
rendered,
vec![
"author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: continue"
.to_string()
]
);
}
other => panic!("expected TurnItem::AgentMessage, got {other:?}"),
}
}
#[test]
fn skips_unnamed_image_label_text() {
let image_url = "data:image/png;base64,abc".to_string();
let label = codex_protocol::models::image_open_tag_text();
let user_text = "Please review this image.".to_string();
let item = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText { text: label },
ContentItem::InputImage {
image_url: image_url.clone(),
detail: Some(DEFAULT_IMAGE_DETAIL),
},
ContentItem::InputText {
text: codex_protocol::models::image_close_tag_text(),
},
ContentItem::InputText {
text: user_text.clone(),
},
],
phase: None,
};
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
match turn_item {
TurnItem::UserMessage(user) => {
let expected_content = vec![
UserInput::Image {
image_url,
detail: Some(DEFAULT_IMAGE_DETAIL),
},
UserInput::Text {
text: user_text,
text_elements: Vec::new(),
},
];
assert_eq!(user.content, expected_content);
}
other => panic!("expected TurnItem::UserMessage, got {other:?}"),
}
}
#[test]
fn skips_user_instructions_and_env() {
let items = vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<environment_context>test_text</environment_context>".to_string(),
}],
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
.to_string(),
}],
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
}],
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "<environment_context>ctx</environment_context>".to_string(),
},
ContentItem::InputText {
text:
"# AGENTS.md instructions for dir\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
.to_string(),
},
],
phase: None,
},
];
for item in items {
let turn_item = parse_turn_item(&item);
assert!(turn_item.is_none(), "expected none, got {turn_item:?}");
}
}
#[test]
fn parses_hook_prompt_message_as_distinct_turn_item() {
let item = build_hook_prompt_message(&[HookPromptFragment::from_single_hook(
"Retry with exactly the phrase meow meow meow.",
"hook-run-1",
)])
.expect("hook prompt message");
let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
match turn_item {
TurnItem::HookPrompt(hook_prompt) => {
assert_eq!(hook_prompt.fragments.len(), 1);
assert_eq!(
hook_prompt.fragments[0],
HookPromptFragment {
text: "Retry with exactly the phrase meow meow meow.".to_string(),
hook_run_id: "hook-run-1".to_string(),
}
);
}
other => panic!("expected TurnItem::HookPrompt, got {other:?}"),
}
}
#[test]
fn parses_hook_prompt_and_hides_other_contextual_fragments() {
let item = ResponseItem::Message {
id: Some("msg-1".to_string()),
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "<environment_context>ctx</environment_context>".to_string(),
},
ContentItem::InputText {
text:
"<hook_prompt hook_run_id=\"hook-run-1\">Retry with care &amp; joy.</hook_prompt>"
.to_string(),
},
],
phase: None,
};
let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
match turn_item {
TurnItem::HookPrompt(hook_prompt) => {
assert_eq!(hook_prompt.id, "msg-1");
assert_eq!(
hook_prompt.fragments,
vec![HookPromptFragment {
text: "Retry with care & joy.".to_string(),
hook_run_id: "hook-run-1".to_string(),
}]
);
}
other => panic!("expected TurnItem::HookPrompt, got {other:?}"),
}
}
#[test]
fn internal_model_context_does_not_parse_as_visible_turn_item() {
let item = ResponseItem::Message {
id: Some("msg-1".to_string()),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: InternalModelContextFragment::new(
InternalContextSource::from_static("extension"),
"Internal steering.",
)
.render(),
}],
phase: None,
};
assert!(parse_turn_item(&item).is_none());
}
#[test]
fn parses_agent_message() {
let item = ResponseItem::Message {
id: Some("msg-1".to_string()),
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: "Hello from Codex".to_string(),
}],
phase: None,
};
let turn_item = parse_turn_item(&item).expect("expected agent message turn item");
match turn_item {
TurnItem::AgentMessage(message) => {
let Some(AgentMessageContent::Text { text }) = message.content.first() else {
panic!("expected agent message text content");
};
assert_eq!(text, "Hello from Codex");
}
other => panic!("expected TurnItem::AgentMessage, got {other:?}"),
}
}
#[test]
fn parses_reasoning_summary_and_raw_content() {
let item = ResponseItem::Reasoning {
id: "reasoning_1".to_string(),
summary: vec![
ReasoningItemReasoningSummary::SummaryText {
text: "Step 1".to_string(),
},
ReasoningItemReasoningSummary::SummaryText {
text: "Step 2".to_string(),
},
],
content: Some(vec![ReasoningItemContent::ReasoningText {
text: "raw details".to_string(),
}]),
encrypted_content: None,
};
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
match turn_item {
TurnItem::Reasoning(reasoning) => {
assert_eq!(
reasoning.summary_text,
vec!["Step 1".to_string(), "Step 2".to_string()]
);
assert_eq!(reasoning.raw_content, vec!["raw details".to_string()]);
}
other => panic!("expected TurnItem::Reasoning, got {other:?}"),
}
}
#[test]
fn parses_reasoning_including_raw_content() {
let item = ResponseItem::Reasoning {
id: "reasoning_2".to_string(),
summary: vec![ReasoningItemReasoningSummary::SummaryText {
text: "Summarized step".to_string(),
}],
content: Some(vec![
ReasoningItemContent::ReasoningText {
text: "raw step".to_string(),
},
ReasoningItemContent::Text {
text: "final thought".to_string(),
},
]),
encrypted_content: None,
};
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
match turn_item {
TurnItem::Reasoning(reasoning) => {
assert_eq!(reasoning.summary_text, vec!["Summarized step".to_string()]);
assert_eq!(
reasoning.raw_content,
vec!["raw step".to_string(), "final thought".to_string()]
);
}
other => panic!("expected TurnItem::Reasoning, got {other:?}"),
}
}
#[test]
fn parses_web_search_call() {
let item = ResponseItem::WebSearchCall {
id: Some("ws_1".to_string()),
status: Some("completed".to_string()),
action: Some(WebSearchAction::Search {
query: Some("weather".to_string()),
queries: None,
}),
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
match turn_item {
TurnItem::WebSearch(search) => assert_eq!(
search,
WebSearchItem {
id: "ws_1".to_string(),
query: "weather".to_string(),
action: WebSearchAction::Search {
query: Some("weather".to_string()),
queries: None,
},
}
),
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
}
}
#[test]
fn parses_web_search_open_page_call() {
let item = ResponseItem::WebSearchCall {
id: Some("ws_open".to_string()),
status: Some("completed".to_string()),
action: Some(WebSearchAction::OpenPage {
url: Some("https://example.com".to_string()),
}),
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
match turn_item {
TurnItem::WebSearch(search) => assert_eq!(
search,
WebSearchItem {
id: "ws_open".to_string(),
query: "https://example.com".to_string(),
action: WebSearchAction::OpenPage {
url: Some("https://example.com".to_string()),
},
}
),
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
}
}
#[test]
fn parses_web_search_find_in_page_call() {
let item = ResponseItem::WebSearchCall {
id: Some("ws_find".to_string()),
status: Some("completed".to_string()),
action: Some(WebSearchAction::FindInPage {
url: Some("https://example.com".to_string()),
pattern: Some("needle".to_string()),
}),
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
match turn_item {
TurnItem::WebSearch(search) => assert_eq!(
search,
WebSearchItem {
id: "ws_find".to_string(),
query: "'needle' in https://example.com".to_string(),
action: WebSearchAction::FindInPage {
url: Some("https://example.com".to_string()),
pattern: Some("needle".to_string()),
},
}
),
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
}
}
#[test]
fn parses_partial_web_search_call_without_action_as_other() {
let item = ResponseItem::WebSearchCall {
id: Some("ws_partial".to_string()),
status: Some("in_progress".to_string()),
action: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
match turn_item {
TurnItem::WebSearch(search) => assert_eq!(
search,
WebSearchItem {
id: "ws_partial".to_string(),
query: String::new(),
action: WebSearchAction::Other,
}
),
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
}
}