mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
WIll make it easier to uprev when the new draft spec is supported. Also updates reqwest where needed for compatibility but doesn't update it everywhere since this is already a large diff. The new version of rmcp handles certain kinds of authentication failures differently, this patch includes support for identifying the failing scope in a WWW-Authenticate header.
219 lines
7.1 KiB
Rust
219 lines
7.1 KiB
Rust
use codex_tools::ToolName;
|
|
use codex_tools::mcp_tool_to_responses_api_tool;
|
|
use pretty_assertions::assert_eq;
|
|
use serde::Deserialize;
|
|
use serde::de::DeserializeOwned;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
use std::fs;
|
|
use std::sync::Arc;
|
|
|
|
const FIXTURE_PATHS: [&str; 5] = [
|
|
"tests/fixtures/json_schema_policy/slack.json",
|
|
"tests/fixtures/json_schema_policy/google_calendar.json",
|
|
"tests/fixtures/json_schema_policy/google_drive.json",
|
|
"tests/fixtures/json_schema_policy/notion.json",
|
|
"tests/fixtures/json_schema_policy/microsoft_outlook_email.json",
|
|
];
|
|
const OVERSIZED_NOTION_CREATE_PAGE_SCHEMA_PATH: &str =
|
|
"tests/fixtures/json_schema_policy/oversized_notion_create_page_input_schema.json";
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct FixtureFile {
|
|
source: String,
|
|
tools: Vec<FixtureTool>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct FixtureTool {
|
|
name: String,
|
|
description: String,
|
|
input_schema: Value,
|
|
#[serde(default)]
|
|
expected_preserved: Vec<ExpectedValue>,
|
|
#[serde(default)]
|
|
expected_pruned: Vec<String>,
|
|
#[serde(default)]
|
|
expected_dropped_fields: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ExpectedValue {
|
|
pointer: String,
|
|
value: Value,
|
|
}
|
|
|
|
#[test]
|
|
fn json_schema_policy_fixtures_convert_to_responses_tools() {
|
|
for fixture in FIXTURE_PATHS.into_iter().map(load_fixture::<FixtureFile>) {
|
|
for fixture_tool in &fixture.tools {
|
|
let responses_tool = convert_fixture_tool(&fixture, fixture_tool);
|
|
let parameters = serde_json::to_value(&responses_tool.parameters)
|
|
.expect("responses parameters should serialize");
|
|
|
|
let expected_fields = [
|
|
(
|
|
"preserve the tool name",
|
|
json!(fixture_tool.name),
|
|
json!(responses_tool.name),
|
|
),
|
|
(
|
|
"preserve the tool description",
|
|
json!(fixture_tool.description),
|
|
json!(responses_tool.description),
|
|
),
|
|
(
|
|
"remain a strict:false tool",
|
|
json!(false),
|
|
json!(responses_tool.strict),
|
|
),
|
|
(
|
|
"produce object-shaped parameters",
|
|
json!("object"),
|
|
parameters.get("type").cloned().unwrap_or(Value::Null),
|
|
),
|
|
];
|
|
|
|
for (message, expected, actual) in expected_fields {
|
|
assert_eq!(actual, expected, "{} should {message}", fixture_tool.name);
|
|
}
|
|
assert!(
|
|
parameters.get("properties").is_some_and(Value::is_object),
|
|
"{} should produce a parameters.properties object",
|
|
fixture_tool.name
|
|
);
|
|
|
|
for expected in &fixture_tool.expected_preserved {
|
|
assert_eq!(
|
|
parameters.pointer(&expected.pointer),
|
|
Some(&expected.value),
|
|
"{} should preserve {}",
|
|
fixture_tool.name,
|
|
expected.pointer
|
|
);
|
|
}
|
|
|
|
for pointer in &fixture_tool.expected_pruned {
|
|
assert!(
|
|
parameters.pointer(pointer).is_none(),
|
|
"{} should prune unreachable definition {pointer}",
|
|
fixture_tool.name
|
|
);
|
|
}
|
|
|
|
for pointer in &fixture_tool.expected_dropped_fields {
|
|
assert!(
|
|
fixture_tool.input_schema.pointer(pointer).is_some(),
|
|
"{} fixture should contain expected dropped field {pointer}",
|
|
fixture_tool.name
|
|
);
|
|
assert!(
|
|
parameters.pointer(pointer).is_none(),
|
|
"{} should drop field {pointer} after JsonSchema conversion",
|
|
fixture_tool.name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn json_schema_policy_oversized_golden_schema_triggers_compaction() {
|
|
let fixture: FixtureFile = load_fixture(OVERSIZED_NOTION_CREATE_PAGE_SCHEMA_PATH);
|
|
let fixture_tool = fixture
|
|
.tools
|
|
.first()
|
|
.expect("oversized fixture should contain a tool");
|
|
let input_bytes = compact_json_len(&fixture_tool.input_schema);
|
|
|
|
let responses_tool = convert_fixture_tool(&fixture, fixture_tool);
|
|
let parameters =
|
|
serde_json::to_value(&responses_tool.parameters).expect("responses parameters serialize");
|
|
let output_bytes = compact_json_len(¶meters);
|
|
|
|
assert!(
|
|
output_bytes < input_bytes,
|
|
"compaction should reduce schema size from {input_bytes} bytes"
|
|
);
|
|
|
|
let absent_pointers = [
|
|
("/description", "drop root description"),
|
|
("/properties/parent/description", "drop nested descriptions"),
|
|
(
|
|
"/$defs",
|
|
"drop root definitions after stripping descriptions is insufficient",
|
|
),
|
|
];
|
|
for (pointer, message) in absent_pointers {
|
|
assert!(
|
|
parameters.pointer(pointer).is_none(),
|
|
"oversized schema should {message}"
|
|
);
|
|
}
|
|
|
|
let expected_values = [
|
|
(
|
|
"/properties/parent",
|
|
json!({}),
|
|
"rewrite local refs before dropping root definitions",
|
|
),
|
|
(
|
|
"/properties/children/items",
|
|
json!({}),
|
|
"rewrite nested local refs before dropping root definitions",
|
|
),
|
|
(
|
|
"/properties/markdown/type",
|
|
json!("string"),
|
|
"retain top-level argument shape",
|
|
),
|
|
(
|
|
"/properties/properties/type",
|
|
json!("object"),
|
|
"retain object argument shape",
|
|
),
|
|
];
|
|
for (pointer, expected, message) in expected_values {
|
|
assert_eq!(
|
|
parameters.pointer(pointer),
|
|
Some(&expected),
|
|
"oversized schema should {message}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn load_fixture<T: DeserializeOwned>(path: &str) -> T {
|
|
let path = codex_utils_cargo_bin::find_resource!(path)
|
|
.unwrap_or_else(|err| panic!("resolve fixture {path}: {err}"));
|
|
let fixture = fs::read_to_string(&path)
|
|
.unwrap_or_else(|err| panic!("read fixture {}: {err}", path.display()));
|
|
serde_json::from_str(&fixture)
|
|
.unwrap_or_else(|err| panic!("parse fixture {}: {err}", path.display()))
|
|
}
|
|
|
|
fn convert_fixture_tool(
|
|
fixture: &FixtureFile,
|
|
fixture_tool: &FixtureTool,
|
|
) -> codex_tools::ResponsesApiTool {
|
|
let name = &fixture_tool.name;
|
|
let input_schema = fixture_tool
|
|
.input_schema
|
|
.as_object()
|
|
.unwrap_or_else(|| panic!("{name} input_schema should be an object"))
|
|
.clone();
|
|
let tool = rmcp::model::Tool::new(
|
|
name.to_string(),
|
|
fixture_tool.description.clone(),
|
|
Arc::new(input_schema),
|
|
);
|
|
|
|
mcp_tool_to_responses_api_tool(&ToolName::namespaced(&fixture.source, name), &tool)
|
|
.unwrap_or_else(|err| panic!("convert {name} from {}: {err}", fixture.source))
|
|
}
|
|
|
|
fn compact_json_len(value: &Value) -> usize {
|
|
serde_json::to_vec(value)
|
|
.unwrap_or_else(|err| panic!("serialize compact JSON: {err}"))
|
|
.len()
|
|
}
|