Files
codex/codex-rs/tools/src/request_user_input_tool_tests.rs
T
Michael Bolin e6f5451a2c Extract tool spec helpers into codex-tools (#16471)
## Why

Follow-up to #16379.

`codex-rs/core/src/tools/spec.rs` and the corresponding handlers still
owned several pure tool-definition helpers even though they do not need
`codex-core` runtime state. Keeping that spec-only logic in `codex-core`
keeps the crate boundary blurry and works against the guidance in
`AGENTS.md` to keep shared tooling out of `codex-core` when possible.

This change takes another step toward a dedicated `codex-tools` crate by
moving more metadata and schema-building code behind the `codex-tools`
API while leaving the actual tool execution paths in `codex-core`.

## What Changed

- Added `codex-rs/tools/src/apply_patch_tool.rs` to own
`ApplyPatchToolArgs`, the freeform/json `apply_patch` tool specs, and
the moved `tool_apply_patch.lark` grammar.
- Updated `codex-rs/tools/BUILD.bazel` so Bazel exposes the moved
grammar file to `codex-tools`.
- Moved the `request_user_input` availability and description helpers
into `codex-rs/tools/src/request_user_input_tool.rs`, with the related
unit tests moved alongside that business logic.
- Moved `request_permissions_tool_description()` into
`codex-rs/tools/src/local_tool.rs`.
- Rewired `codex-rs/core/src/tools/spec.rs`,
`codex-rs/core/src/tools/handlers/apply_patch.rs`, and
`codex-rs/core/src/tools/handlers/request_user_input.rs` to consume the
new `codex-tools` exports instead of local helper code.
- Removed the now-redundant helper implementations and tests from
`codex-core`, plus a couple of stale `client_common` re-exports that
became unused after the move.

## Testing

- `cargo test -p codex-tools`
- `cargo test -p codex-core tools::spec::tests`
- `cargo test -p codex-core tools::handlers::apply_patch::tests`
2026-04-01 14:06:04 -07:00

155 lines
7.2 KiB
Rust

use super::*;
use codex_protocol::config_types::ModeKind;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
#[test]
fn request_user_input_tool_includes_questions_schema() {
assert_eq!(
create_request_user_input_tool("Ask the user to choose.".to_string()),
ToolSpec::Function(ResponsesApiTool {
name: "request_user_input".to_string(),
description: "Ask the user to choose.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::Object {
properties: BTreeMap::from([(
"questions".to_string(),
JsonSchema::Array {
description: Some(
"Questions to show the user. Prefer 1 and do not exceed 3".to_string(),
),
items: Box::new(JsonSchema::Object {
properties: BTreeMap::from([
(
"header".to_string(),
JsonSchema::String {
description: Some(
"Short header label shown in the UI (12 or fewer chars)."
.to_string(),
),
},
),
(
"id".to_string(),
JsonSchema::String {
description: Some(
"Stable identifier for mapping answers (snake_case)."
.to_string(),
),
},
),
(
"options".to_string(),
JsonSchema::Array {
description: Some(
"Provide 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option in this list; the client will add a free-form \"Other\" option automatically."
.to_string(),
),
items: Box::new(JsonSchema::Object {
properties: BTreeMap::from([
(
"description".to_string(),
JsonSchema::String {
description: Some(
"One short sentence explaining impact/tradeoff if selected."
.to_string(),
),
},
),
(
"label".to_string(),
JsonSchema::String {
description: Some(
"User-facing label (1-5 words)."
.to_string(),
),
},
),
]),
required: Some(vec![
"label".to_string(),
"description".to_string(),
]),
additional_properties: Some(false.into()),
}),
},
),
(
"question".to_string(),
JsonSchema::String {
description: Some(
"Single-sentence prompt shown to the user.".to_string(),
),
},
),
]),
required: Some(vec![
"id".to_string(),
"header".to_string(),
"question".to_string(),
"options".to_string(),
]),
additional_properties: Some(false.into()),
}),
},
)]),
required: Some(vec!["questions".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: None,
})
);
}
#[test]
fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() {
assert_eq!(
request_user_input_unavailable_message(
ModeKind::Plan,
/*default_mode_request_user_input*/ false
),
None
);
assert_eq!(
request_user_input_unavailable_message(
ModeKind::Default,
/*default_mode_request_user_input*/ false
),
Some("request_user_input is unavailable in Default mode".to_string())
);
assert_eq!(
request_user_input_unavailable_message(
ModeKind::Default,
/*default_mode_request_user_input*/ true
),
None
);
assert_eq!(
request_user_input_unavailable_message(
ModeKind::Execute,
/*default_mode_request_user_input*/ false
),
Some("request_user_input is unavailable in Execute mode".to_string())
);
assert_eq!(
request_user_input_unavailable_message(
ModeKind::PairProgramming,
/*default_mode_request_user_input*/ false
),
Some("request_user_input is unavailable in Pair Programming mode".to_string())
);
}
#[test]
fn request_user_input_tool_description_mentions_available_modes() {
assert_eq!(
request_user_input_tool_description(/*default_mode_request_user_input*/ false),
"Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string()
);
assert_eq!(
request_user_input_tool_description(/*default_mode_request_user_input*/ true),
"Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string()
);
}