Run plugin and skill tests on Windows (#37836)

## Why

The plugin and skill test suites were excluded wholesale on Windows, even though
most cases do not depend on POSIX behavior.

## What changed

- Make generated TOML and JSON fixtures escape Windows paths correctly.
- Use Windows-safe path canonicalization and normalize advertised skill paths.
- Select test environments through the environment-aware builders.
- Run the suites on Windows while narrowly skipping cases that execute POSIX
  commands or require matching host and executor path conventions.

GitOrigin-RevId: aadf31013c2bf9bac728adc4dd96f572d4a3bf3f
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-08-10 14:45:55 +00:00
1 parent 680934adc4
commit 3b67b03a3f
5 files changed
+57 -65

No files matched your search

+3 -6
View File
@@ -41,16 +41,15 @@ fn stack(codex_home: &TempDir, user: &str, session: &str) -> ConfigLayerStack {
} }
fn path_toggle_config(path: &std::path::Path, enabled: bool) -> String { fn path_toggle_config(path: &std::path::Path, enabled: bool) -> String {
let path = toml::Value::String(path.display().to_string());
format!( format!(
r#"[[skills.config]] r#"[[skills.config]]
path = "{}" path = {path}
enabled = {enabled} enabled = {enabled}
"#, "#
path.display()
) )
} }
#[cfg_attr(windows, ignore)]
#[test] #[test]
fn session_flags_can_reenable_user_disabled_path() { fn session_flags_can_reenable_user_disabled_path() {
let codex_home = TempDir::new().expect("temp dir"); let codex_home = TempDir::new().expect("temp dir");
@@ -71,7 +70,6 @@ fn session_flags_can_reenable_user_disabled_path() {
); );
} }
#[cfg_attr(windows, ignore)]
#[test] #[test]
fn session_flags_can_disable_user_enabled_path() { fn session_flags_can_disable_user_enabled_path() {
let codex_home = TempDir::new().expect("temp dir"); let codex_home = TempDir::new().expect("temp dir");
@@ -115,7 +113,6 @@ enabled = false
); );
} }
#[cfg_attr(windows, ignore)]
#[test] #[test]
fn preserves_order_across_path_and_name_selectors() { fn preserves_order_across_path_and_name_selectors() {
let codex_home = TempDir::new().expect("temp dir"); let codex_home = TempDir::new().expect("temp dir");
+36 -46
View File
@@ -1,4 +1,3 @@
#![cfg(not(target_os = "windows"))]
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use std::sync::Arc; use std::sync::Arc;
@@ -41,6 +40,7 @@ use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::skip_if_remote; use core_test_support::skip_if_remote;
use core_test_support::skip_if_target_windows;
use core_test_support::stdio_server_bin; use core_test_support::stdio_server_bin;
use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
@@ -201,17 +201,16 @@ fn write_plugin_mcp_plugin(home: &TempDir, command: &str) {
let plugin_root = write_sample_plugin_manifest_and_config(home); let plugin_root = write_sample_plugin_manifest_and_config(home);
std::fs::write( std::fs::write(
plugin_root.join(".mcp.json"), plugin_root.join(".mcp.json"),
format!( serde_json::to_vec(&serde_json::json!({
r#"{{ "mcpServers": {
"mcpServers": {{ "sample": {
"sample": {{ "command": command,
"command": "{command}", "cwd": ".",
"cwd": ".", "startup_timeout_sec": 60.0,
"startup_timeout_sec": 60.0 },
}} },
}} }))
}}"# .expect("serialize plugin MCP configuration"),
),
) )
.expect("write plugin mcp config"); .expect("write plugin mcp config");
} }
@@ -271,10 +270,7 @@ async fn build_analytics_plugin_test_codex(
.with_config(move |config| { .with_config(move |config| {
config.chatgpt_base_url = chatgpt_base_url; config.chatgpt_base_url = chatgpt_base_url;
}); });
Ok(builder builder.build_with_auto_env(server).await
.build(server)
.await
.expect("create new conversation"))
} }
async fn build_apps_enabled_plugin_test_codex( async fn build_apps_enabled_plugin_test_codex(
@@ -292,10 +288,7 @@ async fn build_apps_enabled_plugin_test_codex(
.expect("test config should allow feature update"); .expect("test config should allow feature update");
config.chatgpt_base_url = chatgpt_base_url; config.chatgpt_base_url = chatgpt_base_url;
}); });
Ok(builder builder.build_with_remote_and_local_env(server).await
.build(server)
.await
.expect("create new conversation"))
} }
async fn mount_plugin_tool_search_turn(server: &MockServer) -> ResponseMock { async fn mount_plugin_tool_search_turn(server: &MockServer) -> ResponseMock {
@@ -349,6 +342,7 @@ fn searched_plugin_tools(
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn persisted_remote_plugin_command_attribution_flows_through_turn_context() -> Result<()> { async fn persisted_remote_plugin_command_attribution_flows_through_turn_context() -> Result<()> {
skip_if_target_windows!(Ok(()), "executes a POSIX shell script");
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
skip_if_remote!( skip_if_remote!(
Ok(()), Ok(()),
@@ -452,12 +446,11 @@ async fn agent_plugin_skills_use_shared_catalog_and_direct_child_discovery() ->
) )
.await; .await;
let codex_home = Arc::new(TempDir::new()?); let codex_home = Arc::new(TempDir::new()?);
let skill_path = std::fs::canonicalize(write_agent_plugin_skill_plugin(codex_home.as_ref()))?; let skill_path = dunce::canonicalize(write_agent_plugin_skill_plugin(codex_home.as_ref()))?;
let test_codex = test_codex() let mut builder = test_codex()
.with_home(Arc::clone(&codex_home)) .with_home(Arc::clone(&codex_home))
.with_extensions(skills_extensions()) .with_extensions(skills_extensions());
.build(&server) let test_codex = builder.build_with_auto_env(&server).await?;
.await?;
test_codex test_codex
.codex .codex
@@ -625,12 +618,11 @@ async fn legacy_plugin_skill_prompt_remains_complete() -> Result<()> {
"x".repeat(9_000) "x".repeat(9_000)
); );
std::fs::write(&skill_path, &skill_contents)?; std::fs::write(&skill_path, &skill_contents)?;
let skill_path = std::fs::canonicalize(skill_path)?; let skill_path = dunce::canonicalize(skill_path)?;
let test_codex = test_codex() let mut builder = test_codex()
.with_home(codex_home) .with_home(codex_home)
.with_extensions(skills_extensions()) .with_extensions(skills_extensions());
.build(&server) let test_codex = builder.build_with_auto_env(&server).await?;
.await?;
test_codex test_codex
.codex .codex
@@ -718,19 +710,18 @@ async fn agent_plugin_root_mcp_stdio_tool_round_trip_expands_reserved_paths() ->
plugin_root.join("mcp.json"), plugin_root.join("mcp.json"),
serde_json::to_vec_pretty(&mcp_config)?, serde_json::to_vec_pretty(&mcp_config)?,
)?; )?;
let test_codex = test_codex() let mut builder = test_codex().with_home(Arc::clone(&codex_home));
.with_home(Arc::clone(&codex_home)) let test_codex = builder.build_with_remote_and_local_env(&server).await?;
.build(&server)
.await?;
wait_for_mcp_server(&test_codex.codex, "agent").await?; wait_for_mcp_server(&test_codex.codex, "agent").await?;
let data_root = std::fs::read_dir(codex_home.path().join("plugins/data/agent-plugins"))? let data_root = dunce::canonicalize(
.next() std::fs::read_dir(codex_home.path().join("plugins/data/agent-plugins"))?
.expect("Agent Plugin data root")? .next()
.path() .expect("Agent Plugin data root")?
.canonicalize()?; .path(),
)?;
let expected_env = format!( let expected_env = format!(
"{}|{}", "{}|{}",
plugin_root.canonicalize()?.display(), dunce::canonicalize(&plugin_root)?.display(),
data_root.display() data_root.display()
); );
@@ -1215,7 +1206,7 @@ async fn explicitly_requested_mcp_waits_for_startup(request: ExplicitMcpRequest)
return Ok(()); return Ok(());
} }
}; };
let skill_path = std::fs::canonicalize(write_plugin_skill_plugin(codex_home.as_ref()))?; let skill_path = dunce::canonicalize(write_plugin_skill_plugin(codex_home.as_ref()))?;
write_plugin_mcp_plugin(codex_home.as_ref(), &rmcp_test_server_bin); write_plugin_mcp_plugin(codex_home.as_ref(), &rmcp_test_server_bin);
write_plugin_app_plugin(codex_home.as_ref()); write_plugin_app_plugin(codex_home.as_ref());
let initialize_barrier = block_plugin_mcp_startup(codex_home.as_ref(), &rmcp_test_server_bin); let initialize_barrier = block_plugin_mcp_startup(codex_home.as_ref(), &rmcp_test_server_bin);
@@ -1229,10 +1220,7 @@ async fn explicitly_requested_mcp_waits_for_startup(request: ExplicitMcpRequest)
.enable(Feature::Apps) .enable(Feature::Apps)
.expect("test config should allow feature update"); .expect("test config should allow feature update");
}); });
let test_codex = builder let test_codex = builder.build_with_remote_and_local_env(&server).await?;
.build(&server)
.await
.expect("create new conversation");
let codex = Arc::clone(&test_codex.codex); let codex = Arc::clone(&test_codex.codex);
let input = match request { let input = match request {
@@ -1384,7 +1372,7 @@ async fn explicit_plugin_skill_invocation_tracks_remote_plugin_id() -> Result<()
.await; .await;
let codex_home = Arc::new(TempDir::new()?); let codex_home = Arc::new(TempDir::new()?);
let skill_path = std::fs::canonicalize(write_remote_plugin_skill_plugin(codex_home.as_ref()))?; let skill_path = dunce::canonicalize(write_remote_plugin_skill_plugin(codex_home.as_ref()))?;
persist_sample_remote_plugin_id(codex_home.as_ref()); persist_sample_remote_plugin_id(codex_home.as_ref());
let test_codex = build_analytics_plugin_test_codex(&server, codex_home).await?; let test_codex = build_analytics_plugin_test_codex(&server, codex_home).await?;
let codex = Arc::clone(&test_codex.codex); let codex = Arc::clone(&test_codex.codex);
@@ -1429,6 +1417,8 @@ enum ImplicitPluginSkillInvocation {
async fn implicit_plugin_skill_invocation_tracks_remote_plugin_id( async fn implicit_plugin_skill_invocation_tracks_remote_plugin_id(
invocation: ImplicitPluginSkillInvocation, invocation: ImplicitPluginSkillInvocation,
) -> Result<()> { ) -> Result<()> {
skip_if_target_windows!(Ok(()), "executes POSIX cat and bash commands");
skip_if_remote!(Ok(()), "shell commands use host plugin-cache paths");
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
let server = start_mock_server().await; let server = start_mock_server().await;
let codex_home = Arc::new(TempDir::new()?); let codex_home = Arc::new(TempDir::new()?);
@@ -1,4 +1,3 @@
#![cfg(not(target_os = "windows"))]
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use anyhow::Context; use anyhow::Context;
@@ -159,7 +158,7 @@ async fn build_test(
configure_apps_without_search_tool(config, apps_base_url.as_str()); configure_apps_without_search_tool(config, apps_base_url.as_str());
} }
}); });
builder.build(server).await builder.build_with_auto_env(server).await
} }
async fn build_gated_step_preparation_test( async fn build_gated_step_preparation_test(
@@ -1029,7 +1028,7 @@ async fn endpoint_mode_with_no_eligible_candidates_exposes_no_suggestion_tools()
)]; )];
} }
}); });
let test = builder.build(&server).await?; let test = builder.build_with_auto_env(&server).await?;
test.submit_turn("list tools").await?; test.submit_turn("list tools").await?;
+13 -6
View File
@@ -1,4 +1,3 @@
#![cfg(not(target_os = "windows"))]
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use anyhow::Result; use anyhow::Result;
@@ -24,7 +23,7 @@ use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::skip_if_remote; use core_test_support::skip_if_remote;
use core_test_support::skip_if_target_windows; use core_test_support::skip_if_wine_exec;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::test_codex; use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields; use core_test_support::test_codex::turn_permission_fields;
@@ -55,8 +54,10 @@ async fn write_repo_skill(
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_turn_includes_skill_instructions() -> Result<()> { async fn user_turn_includes_skill_instructions() -> Result<()> {
// TODO(anp): Remove after skill-path helpers use target-native paths. skip_if_wine_exec!(
skip_if_target_windows!(Ok(()), "requires native cross-OS skill paths"); Ok(()),
"skill paths require matching host and executor path conventions"
);
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
let server = start_mock_server().await; let server = start_mock_server().await;
@@ -219,7 +220,10 @@ async fn user_turn_selects_symlinked_skill_by_advertised_discovery_path() -> Res
let request = mock.single_request(); let request = mock.single_request();
let developer_texts = request.message_input_texts("developer"); let developer_texts = request.message_input_texts("developer");
let advertised_path = format!("(file: {discovery_path_display})"); let advertised_path = format!(
"(file: {})",
discovery_path.to_string_lossy().replace('\\', "/")
);
assert!( assert!(
developer_texts developer_texts
.iter() .iter()
@@ -243,7 +247,10 @@ async fn user_turn_selects_symlinked_skill_by_advertised_discovery_path() -> Res
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn idle_user_turn_includes_skill_instructions_in_the_first_request() -> Result<()> { async fn idle_user_turn_includes_skill_instructions_in_the_first_request() -> Result<()> {
skip_if_target_windows!(Ok(()), "requires native cross-OS skill paths"); skip_if_wine_exec!(
Ok(()),
"skill paths require matching host and executor path conventions"
);
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
let server = start_mock_server().await; let server = start_mock_server().await;
@@ -143,12 +143,12 @@ fn config_stack_with_session_flags(
} }
fn path_toggle_config(path: &std::path::Path, enabled: bool) -> String { fn path_toggle_config(path: &std::path::Path, enabled: bool) -> String {
let path = toml::Value::String(path.display().to_string());
format!( format!(
r#"[[skills.config]] r#"[[skills.config]]
path = "{}" path = {path}
enabled = {enabled} enabled = {enabled}
"#, "#
path.display()
) )
} }
@@ -908,7 +908,6 @@ async fn skills_for_cwd_uses_cached_result_until_force_reload() {
); );
} }
#[cfg_attr(windows, ignore)]
#[tokio::test] #[tokio::test]
async fn skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill() { async fn skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill() {
let codex_home = tempfile::tempdir().expect("tempdir"); let codex_home = tempfile::tempdir().expect("tempdir");