mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Migrate plugin commands into skills on install (#33411)
## What changed - Convert supported plugin command Markdown into generated skills when a plugin is installed. - Read command sources from the manifest's `commands` field, falling back to the plugin's `commands/` directory. - Load generated command skills alongside native plugin skills while preferring native skills with the same name. - Skip commands with unsupported templates, missing descriptions, name collisions, or generated skills larger than 4 KB, and keep migration failures from blocking plugin installation. ## Testing - Cover default and manifest-configured command paths, stale generated skill cleanup, size limits, native-skill precedence, invalid manifest fields, and non-fatal migration errors. GitOrigin-RevId: 67d910048544b160b191066569bca69b2b951ebc
This commit is contained in:
committed by
copyberry
parent
38b064c31b
commit
2cd6ed7509
@@ -1885,6 +1885,153 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn install_plugin_materializes_default_command_skills() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let source_root = codex_home.path().join("source/sample");
|
||||
|
||||
write_file(
|
||||
&source_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "sample",
|
||||
"skills": "./custom-skills/"
|
||||
}"#,
|
||||
);
|
||||
fs::create_dir_all(source_root.join("custom-skills")).unwrap();
|
||||
write_file(
|
||||
&source_root.join("custom-skills/source-command-pr-review/SKILL.md"),
|
||||
"---\nname: source-command-pr-review\ndescription: Native review skill\n---\n",
|
||||
);
|
||||
write_file(
|
||||
&source_root.join("commands/pr/review.md"),
|
||||
"---\ndescription: Review a pull request\n---\nInspect the proposed changes.\n",
|
||||
);
|
||||
write_file(
|
||||
&source_root.join("commands/summarize.md"),
|
||||
"---\ndescription: Summarize a change\n---\nSummarize the proposed changes.\n",
|
||||
);
|
||||
write_file(
|
||||
&source_root.join("commands/oversized.md"),
|
||||
&format!("---\ndescription: Oversized\n---\n{}", "x".repeat(4_000)),
|
||||
);
|
||||
write_file(
|
||||
&source_root.join(".codex-plugin/migrated-command-skills/undeclared-command/SKILL.md"),
|
||||
"---\nname: undeclared-command\ndescription: undeclared command\n---\n",
|
||||
);
|
||||
let result = PluginStore::new(codex_home.path().to_path_buf())
|
||||
.install(
|
||||
source_root.abs(),
|
||||
PluginId::parse("sample@test").expect("plugin id should parse"),
|
||||
)
|
||||
.unwrap();
|
||||
let migrated_skill = result
|
||||
.installed_path
|
||||
.join(".codex-plugin/migrated-command-skills/source-command-pr-review/SKILL.md");
|
||||
let expected_migrated_skill = "---\nname: \"source-command-pr-review\"\ndescription: \"Review a pull request\"\n---\n\n# source-command-pr-review\n\nUse this skill when the user asks to run the migrated source command `pr-review`.\n\n## Command Template\n\nInspect the proposed changes.\n";
|
||||
assert_eq!(
|
||||
fs::read_to_string(&migrated_skill).unwrap(),
|
||||
expected_migrated_skill
|
||||
);
|
||||
assert!(
|
||||
!result
|
||||
.installed_path
|
||||
.join(".codex-plugin/migrated-command-skills/undeclared-command")
|
||||
.exists()
|
||||
);
|
||||
assert!(
|
||||
!result
|
||||
.installed_path
|
||||
.join(".codex-plugin/migrated-command-skills/source-command-oversized")
|
||||
.exists()
|
||||
);
|
||||
|
||||
let manifest = crate::manifest::load_plugin_manifest(&result.installed_path).unwrap();
|
||||
let resolved = load_plugin_skills(
|
||||
&result.installed_path,
|
||||
&result.plugin_id,
|
||||
&manifest,
|
||||
/*restriction_product*/ None,
|
||||
&SkillConfigRules::default(),
|
||||
/*plugin_skill_snapshots*/ None,
|
||||
Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
resolved
|
||||
.skills
|
||||
.iter()
|
||||
.map(|skill| skill.path_to_skills_md.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
AbsolutePathBuf::from_absolute_path_checked(
|
||||
fs::canonicalize(
|
||||
result
|
||||
.installed_path
|
||||
.join("custom-skills/source-command-pr-review/SKILL.md")
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.unwrap(),
|
||||
AbsolutePathBuf::from_absolute_path_checked(
|
||||
fs::canonicalize(result.installed_path.join(
|
||||
".codex-plugin/migrated-command-skills/source-command-summarize/SKILL.md"
|
||||
))
|
||||
.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_plugin_ignores_invalid_commands_manifest_field() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let source_root = codex_home.path().join("source/sample");
|
||||
write_file(
|
||||
&source_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample","commands":{}}"#,
|
||||
);
|
||||
write_file(
|
||||
&source_root.join("commands/review.md"),
|
||||
"---\ndescription: Review\n---\nReview the current change.\n",
|
||||
);
|
||||
|
||||
let result = PluginStore::new(codex_home.path().to_path_buf())
|
||||
.install(
|
||||
source_root.abs(),
|
||||
PluginId::parse("sample@test").expect("plugin id should parse"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!result
|
||||
.installed_path
|
||||
.join(".codex-plugin/migrated-command-skills")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_plugin_ignores_command_migration_errors() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let source_root = codex_home.path().join("source/sample");
|
||||
write_file(
|
||||
&source_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample","commands":"./commands/review.md"}"#,
|
||||
);
|
||||
fs::create_dir_all(source_root.join("commands")).unwrap();
|
||||
fs::write(source_root.join("commands/review.md"), [0xff]).unwrap();
|
||||
|
||||
let result = PluginStore::new(codex_home.path().to_path_buf())
|
||||
.install(
|
||||
source_root.abs(),
|
||||
PluginId::parse("sample@test").expect("plugin id should parse"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(result.installed_path.join("commands/review.md").is_file());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugin_skills_dedupes_overlapping_manifest_roots() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -2851,6 +2998,10 @@ async fn install_plugin_writes_marketplace_manifest_fallback_when_missing_plugin
|
||||
"review skill",
|
||||
)
|
||||
.unwrap();
|
||||
write_file(
|
||||
&plugin_root.join("commands/review.md"),
|
||||
"---\ndescription: Review code\n---\nReview the current change.\n",
|
||||
);
|
||||
fs::write(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
@@ -2866,6 +3017,7 @@ async fn install_plugin_writes_marketplace_manifest_fallback_when_missing_plugin
|
||||
"skills": [
|
||||
"./skills/thermo-nuclear-code-quality-review"
|
||||
],
|
||||
"commands": ["./commands/review.md"],
|
||||
"category": "code-review"
|
||||
}
|
||||
]
|
||||
@@ -2931,6 +3083,14 @@ async fn install_plugin_writes_marketplace_manifest_fallback_when_missing_plugin
|
||||
serde_json::json!({ "name": "Byron Grogan" })
|
||||
);
|
||||
assert_eq!(fallback_json["category"], "code-review");
|
||||
assert_eq!(
|
||||
fs::read_to_string(
|
||||
installed_path
|
||||
.join(".codex-plugin/migrated-command-skills/source-command-review/SKILL.md")
|
||||
)
|
||||
.unwrap(),
|
||||
"---\nname: \"source-command-review\"\ndescription: \"Review code\"\n---\n\n# source-command-review\n\nUse this skill when the user asks to run the migrated source command `review`.\n\n## Command Template\n\nReview the current change.\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user