fix: move inline codex-rs/core unit tests into sibling files (#14444)

## Why
PR #13783 moved the `codex.rs` unit tests into `codex_tests.rs`. This
applies the same extraction pattern across the rest of `codex-rs/core`
so the production modules stay focused on runtime code instead of large
inline test blocks.

Keeping the tests in sibling files also makes follow-up edits easier to
review because product changes no longer have to share a file with
hundreds or thousands of lines of test scaffolding.

## What changed
- replaced each inline `mod tests { ... }` in `codex-rs/core/src/**`
with a path-based module declaration
- moved each extracted unit test module into a sibling `*_tests.rs`
file, using `mod_tests.rs` for `mod.rs` modules
- preserved the existing `cfg(...)` guards and module-local structure so
the refactor remains structural rather than behavioral

## Testing
- `cargo test -p codex-core --lib` (`1653 passed; 0 failed; 5 ignored`)
- `just fix -p codex-core`
- `cargo fmt --check`
- `cargo shear`
This commit is contained in:
Michael Bolin
2026-03-12 08:16:36 -07:00
committed by GitHub
parent 7f2ca502f5
commit 0c8a36676a
252 changed files with 40158 additions and 40383 deletions
+2 -165
View File
@@ -352,168 +352,5 @@ fn apply_zip_permissions(
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::io::Write;
use tempfile::tempdir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
#[test]
fn curated_plugins_repo_path_uses_codex_home_tmp_dir() {
let tmp = tempdir().expect("tempdir");
assert_eq!(
curated_plugins_repo_path(tmp.path()),
tmp.path().join(".tmp/plugins")
);
}
#[test]
fn read_curated_plugins_sha_reads_trimmed_sha_file() {
let tmp = tempdir().expect("tempdir");
fs::create_dir_all(tmp.path().join(".tmp")).expect("create tmp");
fs::write(tmp.path().join(".tmp/plugins.sha"), "abc123\n").expect("write sha");
assert_eq!(
read_curated_plugins_sha(tmp.path()).as_deref(),
Some("abc123")
);
}
#[tokio::test]
async fn sync_openai_plugins_repo_downloads_zipball_and_records_sha() {
let tmp = tempdir().expect("tempdir");
let server = MockServer::start().await;
let sha = "0123456789abcdef0123456789abcdef01234567";
Mock::given(method("GET"))
.and(path("/repos/openai/plugins"))
.respond_with(
ResponseTemplate::new(200).set_body_string(r#"{"default_branch":"main"}"#),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/repos/openai/plugins/git/ref/heads/main"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(format!(r#"{{"object":{{"sha":"{sha}"}}}}"#)),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path(format!("/repos/openai/plugins/zipball/{sha}")))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "application/zip")
.set_body_bytes(curated_repo_zipball_bytes(sha)),
)
.mount(&server)
.await;
let server_uri = server.uri();
let tmp_path = tmp.path().to_path_buf();
tokio::task::spawn_blocking(move || {
sync_openai_plugins_repo_with_api_base_url(tmp_path.as_path(), &server_uri)
})
.await
.expect("sync task should join")
.expect("sync should succeed");
let repo_path = curated_plugins_repo_path(tmp.path());
assert!(repo_path.join(".agents/plugins/marketplace.json").is_file());
assert!(
repo_path
.join("plugins/gmail/.codex-plugin/plugin.json")
.is_file()
);
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
}
#[tokio::test]
async fn sync_openai_plugins_repo_skips_archive_download_when_sha_matches() {
let tmp = tempdir().expect("tempdir");
let repo_path = curated_plugins_repo_path(tmp.path());
fs::create_dir_all(repo_path.join(".agents/plugins")).expect("create repo");
fs::write(
repo_path.join(".agents/plugins/marketplace.json"),
r#"{"name":"openai-curated","plugins":[]}"#,
)
.expect("write marketplace");
fs::create_dir_all(tmp.path().join(".tmp")).expect("create tmp");
let sha = "fedcba9876543210fedcba9876543210fedcba98";
fs::write(tmp.path().join(".tmp/plugins.sha"), format!("{sha}\n")).expect("write sha");
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/repos/openai/plugins"))
.respond_with(
ResponseTemplate::new(200).set_body_string(r#"{"default_branch":"main"}"#),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/repos/openai/plugins/git/ref/heads/main"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(format!(r#"{{"object":{{"sha":"{sha}"}}}}"#)),
)
.mount(&server)
.await;
let server_uri = server.uri();
let tmp_path = tmp.path().to_path_buf();
tokio::task::spawn_blocking(move || {
sync_openai_plugins_repo_with_api_base_url(tmp_path.as_path(), &server_uri)
})
.await
.expect("sync task should join")
.expect("sync should succeed");
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
assert!(repo_path.join(".agents/plugins/marketplace.json").is_file());
}
fn curated_repo_zipball_bytes(sha: &str) -> Vec<u8> {
let cursor = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(cursor);
let options = SimpleFileOptions::default();
let root = format!("openai-plugins-{sha}");
writer
.start_file(format!("{root}/.agents/plugins/marketplace.json"), options)
.expect("start marketplace entry");
writer
.write_all(
br#"{
"name": "openai-curated",
"plugins": [
{
"name": "gmail",
"source": {
"source": "local",
"path": "./plugins/gmail"
}
}
]
}"#,
)
.expect("write marketplace");
writer
.start_file(
format!("{root}/plugins/gmail/.codex-plugin/plugin.json"),
options,
)
.expect("start plugin manifest entry");
writer
.write_all(br#"{"name":"gmail"}"#)
.expect("write plugin manifest");
writer.finish().expect("finish zip writer").into_inner()
}
}
#[path = "curated_repo_tests.rs"]
mod tests;
@@ -0,0 +1,159 @@
use super::*;
use pretty_assertions::assert_eq;
use std::io::Write;
use tempfile::tempdir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
#[test]
fn curated_plugins_repo_path_uses_codex_home_tmp_dir() {
let tmp = tempdir().expect("tempdir");
assert_eq!(
curated_plugins_repo_path(tmp.path()),
tmp.path().join(".tmp/plugins")
);
}
#[test]
fn read_curated_plugins_sha_reads_trimmed_sha_file() {
let tmp = tempdir().expect("tempdir");
fs::create_dir_all(tmp.path().join(".tmp")).expect("create tmp");
fs::write(tmp.path().join(".tmp/plugins.sha"), "abc123\n").expect("write sha");
assert_eq!(
read_curated_plugins_sha(tmp.path()).as_deref(),
Some("abc123")
);
}
#[tokio::test]
async fn sync_openai_plugins_repo_downloads_zipball_and_records_sha() {
let tmp = tempdir().expect("tempdir");
let server = MockServer::start().await;
let sha = "0123456789abcdef0123456789abcdef01234567";
Mock::given(method("GET"))
.and(path("/repos/openai/plugins"))
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"default_branch":"main"}"#))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/repos/openai/plugins/git/ref/heads/main"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(format!(r#"{{"object":{{"sha":"{sha}"}}}}"#)),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path(format!("/repos/openai/plugins/zipball/{sha}")))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "application/zip")
.set_body_bytes(curated_repo_zipball_bytes(sha)),
)
.mount(&server)
.await;
let server_uri = server.uri();
let tmp_path = tmp.path().to_path_buf();
tokio::task::spawn_blocking(move || {
sync_openai_plugins_repo_with_api_base_url(tmp_path.as_path(), &server_uri)
})
.await
.expect("sync task should join")
.expect("sync should succeed");
let repo_path = curated_plugins_repo_path(tmp.path());
assert!(repo_path.join(".agents/plugins/marketplace.json").is_file());
assert!(
repo_path
.join("plugins/gmail/.codex-plugin/plugin.json")
.is_file()
);
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
}
#[tokio::test]
async fn sync_openai_plugins_repo_skips_archive_download_when_sha_matches() {
let tmp = tempdir().expect("tempdir");
let repo_path = curated_plugins_repo_path(tmp.path());
fs::create_dir_all(repo_path.join(".agents/plugins")).expect("create repo");
fs::write(
repo_path.join(".agents/plugins/marketplace.json"),
r#"{"name":"openai-curated","plugins":[]}"#,
)
.expect("write marketplace");
fs::create_dir_all(tmp.path().join(".tmp")).expect("create tmp");
let sha = "fedcba9876543210fedcba9876543210fedcba98";
fs::write(tmp.path().join(".tmp/plugins.sha"), format!("{sha}\n")).expect("write sha");
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/repos/openai/plugins"))
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"default_branch":"main"}"#))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/repos/openai/plugins/git/ref/heads/main"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(format!(r#"{{"object":{{"sha":"{sha}"}}}}"#)),
)
.mount(&server)
.await;
let server_uri = server.uri();
let tmp_path = tmp.path().to_path_buf();
tokio::task::spawn_blocking(move || {
sync_openai_plugins_repo_with_api_base_url(tmp_path.as_path(), &server_uri)
})
.await
.expect("sync task should join")
.expect("sync should succeed");
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
assert!(repo_path.join(".agents/plugins/marketplace.json").is_file());
}
fn curated_repo_zipball_bytes(sha: &str) -> Vec<u8> {
let cursor = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(cursor);
let options = SimpleFileOptions::default();
let root = format!("openai-plugins-{sha}");
writer
.start_file(format!("{root}/.agents/plugins/marketplace.json"), options)
.expect("start marketplace entry");
writer
.write_all(
br#"{
"name": "openai-curated",
"plugins": [
{
"name": "gmail",
"source": {
"source": "local",
"path": "./plugins/gmail"
}
}
]
}"#,
)
.expect("write marketplace");
writer
.start_file(
format!("{root}/plugins/gmail/.codex-plugin/plugin.json"),
options,
)
.expect("start plugin manifest entry");
writer
.write_all(br#"{"name":"gmail"}"#)
.expect("write plugin manifest");
writer.finish().expect("finish zip writer").into_inner()
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -579
View File
@@ -390,582 +390,5 @@ enum MarketplacePluginSource {
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
#[test]
fn resolve_marketplace_plugin_finds_repo_marketplace_plugin() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join("nested")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./plugin-1"
}
}
]
}"#,
)
.unwrap();
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved,
ResolvedMarketplacePlugin {
plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string())
.unwrap(),
source_path: AbsolutePathBuf::try_from(repo_root.join("plugin-1")).unwrap(),
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
}
);
}
#[test]
fn resolve_marketplace_plugin_reports_missing_plugin() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{"name":"codex-curated","plugins":[]}"#,
)
.unwrap();
let err = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"missing",
)
.unwrap_err();
assert_eq!(
err.to_string(),
"plugin `missing` was not found in marketplace `codex-curated`"
);
}
#[test]
fn list_marketplaces_returns_home_and_repo_marketplaces() {
let tmp = tempdir().unwrap();
let home_root = tmp.path().join("home");
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(home_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
home_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "shared-plugin",
"source": {
"source": "local",
"path": "./home-shared"
}
},
{
"name": "home-only",
"source": {
"source": "local",
"path": "./home-only"
}
}
]
}"#,
)
.unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "shared-plugin",
"source": {
"source": "local",
"path": "./repo-shared"
}
},
{
"name": "repo-only",
"source": {
"source": "local",
"path": "./repo-only"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
Some(&home_root),
)
.unwrap();
assert_eq!(
marketplaces,
vec![
MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(
home_root.join(".agents/plugins/marketplace.json"),
)
.unwrap(),
plugins: vec![
MarketplacePluginSummary {
name: "shared-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(home_root.join("home-shared"))
.unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
MarketplacePluginSummary {
name: "home-only".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(home_root.join("home-only"))
.unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
],
},
MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(
repo_root.join(".agents/plugins/marketplace.json"),
)
.unwrap(),
plugins: vec![
MarketplacePluginSummary {
name: "shared-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("repo-shared"))
.unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
MarketplacePluginSummary {
name: "repo-only".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("repo-only"))
.unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
],
},
]
);
}
#[test]
fn list_marketplaces_keeps_distinct_entries_for_same_name() {
let tmp = tempdir().unwrap();
let home_root = tmp.path().join("home");
let repo_root = tmp.path().join("repo");
let home_marketplace = home_root.join(".agents/plugins/marketplace.json");
let repo_marketplace = repo_root.join(".agents/plugins/marketplace.json");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(home_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
home_marketplace.clone(),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./home-plugin"
}
}
]
}"#,
)
.unwrap();
fs::write(
repo_marketplace.clone(),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./repo-plugin"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
Some(&home_root),
)
.unwrap();
assert_eq!(
marketplaces,
vec![
MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(home_marketplace).unwrap(),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(home_root.join("home-plugin")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
}],
},
MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(repo_marketplace.clone()).unwrap(),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
}],
},
]
);
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_marketplace).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved.source_path,
AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap()
);
}
#[test]
fn list_marketplaces_dedupes_multiple_roots_in_same_repo() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let nested_root = repo_root.join("nested/project");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(&nested_root).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./plugin"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[
AbsolutePathBuf::try_from(repo_root.clone()).unwrap(),
AbsolutePathBuf::try_from(nested_root).unwrap(),
],
None,
)
.unwrap();
assert_eq!(
marketplaces,
vec![MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json"))
.unwrap(),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("plugin")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
}],
}]
);
}
#[test]
fn list_marketplaces_resolves_plugin_interface_paths_to_absolute() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let plugin_root = repo_root.join("plugins/demo-plugin");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "demo-plugin",
"source": {
"source": "local",
"path": "./plugins/demo-plugin"
},
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
"category": "Design"
}
]
}"#,
)
.unwrap();
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{
"name": "demo-plugin",
"interface": {
"displayName": "Demo",
"category": "Productivity",
"capabilities": ["Interactive", "Write"],
"composerIcon": "./assets/icon.png",
"logo": "./assets/logo.png",
"screenshots": ["./assets/shot1.png"]
}
}"#,
)
.unwrap();
let marketplaces =
list_marketplaces_with_home(&[AbsolutePathBuf::try_from(repo_root).unwrap()], None)
.unwrap();
assert_eq!(
marketplaces[0].plugins[0].install_policy,
MarketplacePluginInstallPolicy::Available
);
assert_eq!(
marketplaces[0].plugins[0].auth_policy,
MarketplacePluginAuthPolicy::OnInstall
);
assert_eq!(
marketplaces[0].plugins[0].interface,
Some(PluginManifestInterfaceSummary {
display_name: Some("Demo".to_string()),
short_description: None,
long_description: None,
developer_name: None,
category: Some("Design".to_string()),
capabilities: vec!["Interactive".to_string(), "Write".to_string()],
website_url: None,
privacy_policy_url: None,
terms_of_service_url: None,
default_prompt: None,
brand_color: None,
composer_icon: Some(
AbsolutePathBuf::try_from(plugin_root.join("assets/icon.png")).unwrap(),
),
logo: Some(AbsolutePathBuf::try_from(plugin_root.join("assets/logo.png")).unwrap()),
screenshots: vec![
AbsolutePathBuf::try_from(plugin_root.join("assets/shot1.png")).unwrap(),
],
})
);
}
#[test]
fn list_marketplaces_ignores_plugin_interface_assets_without_dot_slash() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let plugin_root = repo_root.join("plugins/demo-plugin");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "demo-plugin",
"source": {
"source": "local",
"path": "./plugins/demo-plugin"
}
}
]
}"#,
)
.unwrap();
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{
"name": "demo-plugin",
"interface": {
"displayName": "Demo",
"capabilities": ["Interactive"],
"composerIcon": "assets/icon.png",
"logo": "/tmp/logo.png",
"screenshots": ["assets/shot1.png"]
}
}"#,
)
.unwrap();
let marketplaces =
list_marketplaces_with_home(&[AbsolutePathBuf::try_from(repo_root).unwrap()], None)
.unwrap();
assert_eq!(
marketplaces[0].plugins[0].interface,
Some(PluginManifestInterfaceSummary {
display_name: Some("Demo".to_string()),
short_description: None,
long_description: None,
developer_name: None,
category: None,
capabilities: vec!["Interactive".to_string()],
website_url: None,
privacy_policy_url: None,
terms_of_service_url: None,
default_prompt: None,
brand_color: None,
composer_icon: None,
logo: None,
screenshots: Vec::new(),
})
);
assert_eq!(
marketplaces[0].plugins[0].install_policy,
MarketplacePluginInstallPolicy::Available
);
assert_eq!(
marketplaces[0].plugins[0].auth_policy,
MarketplacePluginAuthPolicy::OnInstall
);
}
#[test]
fn resolve_marketplace_plugin_rejects_non_relative_local_paths() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "../plugin-1"
}
}
]
}"#,
)
.unwrap();
let err = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap_err();
assert_eq!(
err.to_string(),
format!(
"invalid marketplace file `{}`: local plugin source path must start with `./`",
repo_root.join(".agents/plugins/marketplace.json").display()
)
);
}
#[test]
fn resolve_marketplace_plugin_uses_first_duplicate_entry() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./first"
}
},
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./second"
}
}
]
}"#,
)
.unwrap();
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved.source_path,
AbsolutePathBuf::try_from(repo_root.join("first")).unwrap()
);
}
}
#[path = "marketplace_tests.rs"]
mod tests;
@@ -0,0 +1,571 @@
use super::*;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
#[test]
fn resolve_marketplace_plugin_finds_repo_marketplace_plugin() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join("nested")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./plugin-1"
}
}
]
}"#,
)
.unwrap();
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved,
ResolvedMarketplacePlugin {
plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string())
.unwrap(),
source_path: AbsolutePathBuf::try_from(repo_root.join("plugin-1")).unwrap(),
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
}
);
}
#[test]
fn resolve_marketplace_plugin_reports_missing_plugin() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{"name":"codex-curated","plugins":[]}"#,
)
.unwrap();
let err = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"missing",
)
.unwrap_err();
assert_eq!(
err.to_string(),
"plugin `missing` was not found in marketplace `codex-curated`"
);
}
#[test]
fn list_marketplaces_returns_home_and_repo_marketplaces() {
let tmp = tempdir().unwrap();
let home_root = tmp.path().join("home");
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(home_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
home_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "shared-plugin",
"source": {
"source": "local",
"path": "./home-shared"
}
},
{
"name": "home-only",
"source": {
"source": "local",
"path": "./home-only"
}
}
]
}"#,
)
.unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "shared-plugin",
"source": {
"source": "local",
"path": "./repo-shared"
}
},
{
"name": "repo-only",
"source": {
"source": "local",
"path": "./repo-only"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
Some(&home_root),
)
.unwrap();
assert_eq!(
marketplaces,
vec![
MarketplaceSummary {
name: "codex-curated".to_string(),
path:
AbsolutePathBuf::try_from(home_root.join(".agents/plugins/marketplace.json"),)
.unwrap(),
plugins: vec![
MarketplacePluginSummary {
name: "shared-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(home_root.join("home-shared")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
MarketplacePluginSummary {
name: "home-only".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(home_root.join("home-only")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
],
},
MarketplaceSummary {
name: "codex-curated".to_string(),
path:
AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json"),)
.unwrap(),
plugins: vec![
MarketplacePluginSummary {
name: "shared-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("repo-shared")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
MarketplacePluginSummary {
name: "repo-only".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("repo-only")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
},
],
},
]
);
}
#[test]
fn list_marketplaces_keeps_distinct_entries_for_same_name() {
let tmp = tempdir().unwrap();
let home_root = tmp.path().join("home");
let repo_root = tmp.path().join("repo");
let home_marketplace = home_root.join(".agents/plugins/marketplace.json");
let repo_marketplace = repo_root.join(".agents/plugins/marketplace.json");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(home_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
home_marketplace.clone(),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./home-plugin"
}
}
]
}"#,
)
.unwrap();
fs::write(
repo_marketplace.clone(),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./repo-plugin"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
Some(&home_root),
)
.unwrap();
assert_eq!(
marketplaces,
vec![
MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(home_marketplace).unwrap(),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(home_root.join("home-plugin")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
}],
},
MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(repo_marketplace.clone()).unwrap(),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
}],
},
]
);
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_marketplace).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved.source_path,
AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap()
);
}
#[test]
fn list_marketplaces_dedupes_multiple_roots_in_same_repo() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let nested_root = repo_root.join("nested/project");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(&nested_root).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./plugin"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[
AbsolutePathBuf::try_from(repo_root.clone()).unwrap(),
AbsolutePathBuf::try_from(nested_root).unwrap(),
],
None,
)
.unwrap();
assert_eq!(
marketplaces,
vec![MarketplaceSummary {
name: "codex-curated".to_string(),
path: AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json"))
.unwrap(),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("plugin")).unwrap(),
},
install_policy: MarketplacePluginInstallPolicy::Available,
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
interface: None,
}],
}]
);
}
#[test]
fn list_marketplaces_resolves_plugin_interface_paths_to_absolute() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let plugin_root = repo_root.join("plugins/demo-plugin");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "demo-plugin",
"source": {
"source": "local",
"path": "./plugins/demo-plugin"
},
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
"category": "Design"
}
]
}"#,
)
.unwrap();
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{
"name": "demo-plugin",
"interface": {
"displayName": "Demo",
"category": "Productivity",
"capabilities": ["Interactive", "Write"],
"composerIcon": "./assets/icon.png",
"logo": "./assets/logo.png",
"screenshots": ["./assets/shot1.png"]
}
}"#,
)
.unwrap();
let marketplaces =
list_marketplaces_with_home(&[AbsolutePathBuf::try_from(repo_root).unwrap()], None)
.unwrap();
assert_eq!(
marketplaces[0].plugins[0].install_policy,
MarketplacePluginInstallPolicy::Available
);
assert_eq!(
marketplaces[0].plugins[0].auth_policy,
MarketplacePluginAuthPolicy::OnInstall
);
assert_eq!(
marketplaces[0].plugins[0].interface,
Some(PluginManifestInterfaceSummary {
display_name: Some("Demo".to_string()),
short_description: None,
long_description: None,
developer_name: None,
category: Some("Design".to_string()),
capabilities: vec!["Interactive".to_string(), "Write".to_string()],
website_url: None,
privacy_policy_url: None,
terms_of_service_url: None,
default_prompt: None,
brand_color: None,
composer_icon: Some(
AbsolutePathBuf::try_from(plugin_root.join("assets/icon.png")).unwrap(),
),
logo: Some(AbsolutePathBuf::try_from(plugin_root.join("assets/logo.png")).unwrap()),
screenshots: vec![
AbsolutePathBuf::try_from(plugin_root.join("assets/shot1.png")).unwrap(),
],
})
);
}
#[test]
fn list_marketplaces_ignores_plugin_interface_assets_without_dot_slash() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let plugin_root = repo_root.join("plugins/demo-plugin");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "demo-plugin",
"source": {
"source": "local",
"path": "./plugins/demo-plugin"
}
}
]
}"#,
)
.unwrap();
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{
"name": "demo-plugin",
"interface": {
"displayName": "Demo",
"capabilities": ["Interactive"],
"composerIcon": "assets/icon.png",
"logo": "/tmp/logo.png",
"screenshots": ["assets/shot1.png"]
}
}"#,
)
.unwrap();
let marketplaces =
list_marketplaces_with_home(&[AbsolutePathBuf::try_from(repo_root).unwrap()], None)
.unwrap();
assert_eq!(
marketplaces[0].plugins[0].interface,
Some(PluginManifestInterfaceSummary {
display_name: Some("Demo".to_string()),
short_description: None,
long_description: None,
developer_name: None,
category: None,
capabilities: vec!["Interactive".to_string()],
website_url: None,
privacy_policy_url: None,
terms_of_service_url: None,
default_prompt: None,
brand_color: None,
composer_icon: None,
logo: None,
screenshots: Vec::new(),
})
);
assert_eq!(
marketplaces[0].plugins[0].install_policy,
MarketplacePluginInstallPolicy::Available
);
assert_eq!(
marketplaces[0].plugins[0].auth_policy,
MarketplacePluginAuthPolicy::OnInstall
);
}
#[test]
fn resolve_marketplace_plugin_rejects_non_relative_local_paths() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "../plugin-1"
}
}
]
}"#,
)
.unwrap();
let err = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap_err();
assert_eq!(
err.to_string(),
format!(
"invalid marketplace file `{}`: local plugin source path must start with `./`",
repo_root.join(".agents/plugins/marketplace.json").display()
)
);
}
#[test]
fn resolve_marketplace_plugin_uses_first_duplicate_entry() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./first"
}
},
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./second"
}
}
]
}"#,
)
.unwrap();
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved.source_path,
AbsolutePathBuf::try_from(repo_root.join("first")).unwrap()
);
}
+2 -9
View File
@@ -79,12 +79,5 @@ pub(crate) fn render_explicit_plugin_instructions(
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn render_plugins_section_returns_none_for_empty_plugins() {
assert_eq!(render_plugins_section(&[]), None);
}
}
#[path = "render_tests.rs"]
mod tests;
@@ -0,0 +1,7 @@
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn render_plugins_section_returns_none_for_empty_plugins() {
assert_eq!(render_plugins_section(&[]), None);
}
+2 -194
View File
@@ -342,197 +342,5 @@ fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), PluginStoreErr
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
fn write_plugin(root: &Path, dir_name: &str, manifest_name: &str) {
let plugin_root = root.join(dir_name);
fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap();
fs::create_dir_all(plugin_root.join("skills")).unwrap();
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
format!(r#"{{"name":"{manifest_name}"}}"#),
)
.unwrap();
fs::write(plugin_root.join("skills/SKILL.md"), "skill").unwrap();
fs::write(plugin_root.join(".mcp.json"), r#"{"mcpServers":{}}"#).unwrap();
}
#[test]
fn install_copies_plugin_into_default_marketplace() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "sample-plugin", "sample-plugin");
let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap();
let result = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(),
plugin_id.clone(),
)
.unwrap();
let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local");
assert_eq!(
result,
PluginInstallResult {
plugin_id,
plugin_version: "local".to_string(),
installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(),
}
);
assert!(installed_path.join(".codex-plugin/plugin.json").is_file());
assert!(installed_path.join("skills/SKILL.md").is_file());
}
#[test]
fn install_uses_manifest_name_for_destination_and_key() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "source-dir", "manifest-name");
let plugin_id = PluginId::new("manifest-name".to_string(), "market".to_string()).unwrap();
let result = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(),
plugin_id.clone(),
)
.unwrap();
assert_eq!(
result,
PluginInstallResult {
plugin_id,
plugin_version: "local".to_string(),
installed_path: AbsolutePathBuf::try_from(
tmp.path().join("plugins/cache/market/manifest-name/local"),
)
.unwrap(),
}
);
}
#[test]
fn plugin_root_derives_path_from_key_and_version() {
let tmp = tempdir().unwrap();
let store = PluginStore::new(tmp.path().to_path_buf());
let plugin_id = PluginId::new("sample".to_string(), "debug".to_string()).unwrap();
assert_eq!(
store.plugin_root(&plugin_id, "local").as_path(),
tmp.path().join("plugins/cache/debug/sample/local")
);
}
#[test]
fn install_with_version_uses_requested_cache_version() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "sample-plugin", "sample-plugin");
let plugin_id =
PluginId::new("sample-plugin".to_string(), "openai-curated".to_string()).unwrap();
let plugin_version = "0123456789abcdef".to_string();
let result = PluginStore::new(tmp.path().to_path_buf())
.install_with_version(
AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(),
plugin_id.clone(),
plugin_version.clone(),
)
.unwrap();
let installed_path = tmp.path().join(format!(
"plugins/cache/openai-curated/sample-plugin/{plugin_version}"
));
assert_eq!(
result,
PluginInstallResult {
plugin_id,
plugin_version,
installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(),
}
);
assert!(installed_path.join(".codex-plugin/plugin.json").is_file());
}
#[test]
fn active_plugin_version_reads_version_directory_name() {
let tmp = tempdir().unwrap();
write_plugin(
&tmp.path().join("plugins/cache/debug"),
"sample-plugin/local",
"sample-plugin",
);
let store = PluginStore::new(tmp.path().to_path_buf());
let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap();
assert_eq!(
store.active_plugin_version(&plugin_id),
Some("local".to_string())
);
assert_eq!(
store.active_plugin_root(&plugin_id).unwrap().as_path(),
tmp.path().join("plugins/cache/debug/sample-plugin/local")
);
}
#[test]
fn plugin_root_rejects_path_separators_in_key_segments() {
let err = PluginId::parse("../../etc@debug").unwrap_err();
assert_eq!(
err.to_string(),
"invalid plugin name: only ASCII letters, digits, `_`, and `-` are allowed in `../../etc@debug`"
);
let err = PluginId::parse("sample@../../etc").unwrap_err();
assert_eq!(
err.to_string(),
"invalid marketplace name: only ASCII letters, digits, `_`, and `-` are allowed in `sample@../../etc`"
);
}
#[test]
fn install_rejects_manifest_names_with_path_separators() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "source-dir", "../../etc");
let err = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(),
PluginId::new("source-dir".to_string(), "debug".to_string()).unwrap(),
)
.unwrap_err();
assert_eq!(
err.to_string(),
"invalid plugin name: only ASCII letters, digits, `_`, and `-` are allowed"
);
}
#[test]
fn install_rejects_marketplace_names_with_path_separators() {
let err = PluginId::new("sample-plugin".to_string(), "../../etc".to_string()).unwrap_err();
assert_eq!(
err.to_string(),
"invalid marketplace name: only ASCII letters, digits, `_`, and `-` are allowed"
);
}
#[test]
fn install_rejects_manifest_names_that_do_not_match_marketplace_plugin_name() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "source-dir", "manifest-name");
let err = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(),
PluginId::new("different-name".to_string(), "debug".to_string()).unwrap(),
)
.unwrap_err();
assert_eq!(
err.to_string(),
"plugin manifest name `manifest-name` does not match marketplace plugin name `different-name`"
);
}
}
#[path = "store_tests.rs"]
mod tests;
+192
View File
@@ -0,0 +1,192 @@
use super::*;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
fn write_plugin(root: &Path, dir_name: &str, manifest_name: &str) {
let plugin_root = root.join(dir_name);
fs::create_dir_all(plugin_root.join(".codex-plugin")).unwrap();
fs::create_dir_all(plugin_root.join("skills")).unwrap();
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
format!(r#"{{"name":"{manifest_name}"}}"#),
)
.unwrap();
fs::write(plugin_root.join("skills/SKILL.md"), "skill").unwrap();
fs::write(plugin_root.join(".mcp.json"), r#"{"mcpServers":{}}"#).unwrap();
}
#[test]
fn install_copies_plugin_into_default_marketplace() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "sample-plugin", "sample-plugin");
let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap();
let result = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(),
plugin_id.clone(),
)
.unwrap();
let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local");
assert_eq!(
result,
PluginInstallResult {
plugin_id,
plugin_version: "local".to_string(),
installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(),
}
);
assert!(installed_path.join(".codex-plugin/plugin.json").is_file());
assert!(installed_path.join("skills/SKILL.md").is_file());
}
#[test]
fn install_uses_manifest_name_for_destination_and_key() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "source-dir", "manifest-name");
let plugin_id = PluginId::new("manifest-name".to_string(), "market".to_string()).unwrap();
let result = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(),
plugin_id.clone(),
)
.unwrap();
assert_eq!(
result,
PluginInstallResult {
plugin_id,
plugin_version: "local".to_string(),
installed_path: AbsolutePathBuf::try_from(
tmp.path().join("plugins/cache/market/manifest-name/local"),
)
.unwrap(),
}
);
}
#[test]
fn plugin_root_derives_path_from_key_and_version() {
let tmp = tempdir().unwrap();
let store = PluginStore::new(tmp.path().to_path_buf());
let plugin_id = PluginId::new("sample".to_string(), "debug".to_string()).unwrap();
assert_eq!(
store.plugin_root(&plugin_id, "local").as_path(),
tmp.path().join("plugins/cache/debug/sample/local")
);
}
#[test]
fn install_with_version_uses_requested_cache_version() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "sample-plugin", "sample-plugin");
let plugin_id =
PluginId::new("sample-plugin".to_string(), "openai-curated".to_string()).unwrap();
let plugin_version = "0123456789abcdef".to_string();
let result = PluginStore::new(tmp.path().to_path_buf())
.install_with_version(
AbsolutePathBuf::try_from(tmp.path().join("sample-plugin")).unwrap(),
plugin_id.clone(),
plugin_version.clone(),
)
.unwrap();
let installed_path = tmp.path().join(format!(
"plugins/cache/openai-curated/sample-plugin/{plugin_version}"
));
assert_eq!(
result,
PluginInstallResult {
plugin_id,
plugin_version,
installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(),
}
);
assert!(installed_path.join(".codex-plugin/plugin.json").is_file());
}
#[test]
fn active_plugin_version_reads_version_directory_name() {
let tmp = tempdir().unwrap();
write_plugin(
&tmp.path().join("plugins/cache/debug"),
"sample-plugin/local",
"sample-plugin",
);
let store = PluginStore::new(tmp.path().to_path_buf());
let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap();
assert_eq!(
store.active_plugin_version(&plugin_id),
Some("local".to_string())
);
assert_eq!(
store.active_plugin_root(&plugin_id).unwrap().as_path(),
tmp.path().join("plugins/cache/debug/sample-plugin/local")
);
}
#[test]
fn plugin_root_rejects_path_separators_in_key_segments() {
let err = PluginId::parse("../../etc@debug").unwrap_err();
assert_eq!(
err.to_string(),
"invalid plugin name: only ASCII letters, digits, `_`, and `-` are allowed in `../../etc@debug`"
);
let err = PluginId::parse("sample@../../etc").unwrap_err();
assert_eq!(
err.to_string(),
"invalid marketplace name: only ASCII letters, digits, `_`, and `-` are allowed in `sample@../../etc`"
);
}
#[test]
fn install_rejects_manifest_names_with_path_separators() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "source-dir", "../../etc");
let err = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(),
PluginId::new("source-dir".to_string(), "debug".to_string()).unwrap(),
)
.unwrap_err();
assert_eq!(
err.to_string(),
"invalid plugin name: only ASCII letters, digits, `_`, and `-` are allowed"
);
}
#[test]
fn install_rejects_marketplace_names_with_path_separators() {
let err = PluginId::new("sample-plugin".to_string(), "../../etc".to_string()).unwrap_err();
assert_eq!(
err.to_string(),
"invalid marketplace name: only ASCII letters, digits, `_`, and `-` are allowed"
);
}
#[test]
fn install_rejects_manifest_names_that_do_not_match_marketplace_plugin_name() {
let tmp = tempdir().unwrap();
write_plugin(tmp.path(), "source-dir", "manifest-name");
let err = PluginStore::new(tmp.path().to_path_buf())
.install(
AbsolutePathBuf::try_from(tmp.path().join("source-dir")).unwrap(),
PluginId::new("different-name".to_string(), "debug".to_string()).unwrap(),
)
.unwrap_err();
assert_eq!(
err.to_string(),
"plugin manifest name `manifest-name` does not match marketplace plugin name `different-name`"
);
}