use serde_json::{Map, Value};
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
struct PackagedRuntimeAsset {
name: &'static str,
package_include_path: &'static str,
runtime_relative_path: &'static str,
}
const PACKAGED_RUNTIME_ASSETS: &[PackagedRuntimeAsset] = &[
PackagedRuntimeAsset {
name: "plugin_manifest",
package_include_path: "assets/plugin/.codex-plugin/plugin.json",
runtime_relative_path: ".codex-plugin/plugin.json",
},
PackagedRuntimeAsset {
name: "mcp_manifest",
package_include_path: "assets/plugin/.mcp.json",
runtime_relative_path: ".mcp.json",
},
PackagedRuntimeAsset {
name: "hooks_source",
package_include_path: "assets/plugin/hooks/hooks.json",
runtime_relative_path: "hooks/hooks.json",
},
PackagedRuntimeAsset {
name: "cap_skill",
package_include_path: "assets/skills/cap/SKILL.md",
runtime_relative_path: "skills/cap/SKILL.md",
},
PackagedRuntimeAsset {
name: "ccc_plugin_skill",
package_include_path: "assets/plugin/skills/ccc/SKILL.md",
runtime_relative_path: "skills/ccc/SKILL.md",
},
PackagedRuntimeAsset {
name: "coding_comment_discipline_skill",
package_include_path: "assets/plugin/skills/coding-comment-discipline/SKILL.md",
runtime_relative_path: "skills/coding-comment-discipline/SKILL.md",
},
];
const SSL_PACKAGE_INCLUDE_PATH: &str = "assets/skills/ssl/**";
const SSL_RUNTIME_RELATIVE_DIR: &str = "skills/ssl";
fn repo_root() -> PathBuf {
let source_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("ccc crate should live under rust/ccc-mcp")
.to_path_buf();
if source_root.join(".codex-plugin/plugin.json").exists() {
return source_root;
}
Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/plugin")
}
fn source_repo_root() -> Option<PathBuf> {
let source_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("ccc crate should live under rust/ccc-mcp")
.to_path_buf();
source_root
.join("skills/cap/SKILL.md")
.exists()
.then_some(source_root)
}
fn read_json(path: &Path) -> Value {
let text = fs::read_to_string(path).unwrap_or_else(|error| {
panic!("read {}: {error}", path.display());
});
serde_json::from_str(&text).unwrap_or_else(|error| {
panic!("parse {}: {error}", path.display());
})
}
fn semver_tokens(value: &str) -> Vec<&str> {
value
.split(|character: char| !character.is_ascii_digit() && character != '.')
.map(|token| token.trim_matches('.'))
.filter(|token| {
let mut parts = token.split('.');
let Some(major) = parts.next() else {
return false;
};
let Some(minor) = parts.next() else {
return false;
};
let Some(patch) = parts.next() else {
return false;
};
parts.next().is_none()
&& !major.is_empty()
&& !minor.is_empty()
&& !patch.is_empty()
&& major.chars().all(|character| character.is_ascii_digit())
&& minor.chars().all(|character| character.is_ascii_digit())
&& patch.chars().all(|character| character.is_ascii_digit())
})
.collect()
}
fn collect_stale_version_strings(value: &Value, json_path: &str, stale_versions: &mut Vec<String>) {
match value {
Value::String(string) => {
for token in semver_tokens(string) {
if token != env!("CARGO_PKG_VERSION") {
stale_versions.push(format!("{json_path}: {token}"));
}
}
}
Value::Array(items) => {
for (index, item) in items.iter().enumerate() {
collect_stale_version_strings(
item,
&format!("{json_path}[{index}]"),
stale_versions,
);
}
}
Value::Object(map) => {
for (key, child) in map {
collect_stale_version_strings(child, &format!("{json_path}.{key}"), stale_versions);
}
}
Value::Bool(_) | Value::Number(_) | Value::Null => {}
}
}
fn cargo_package_include_entries() -> BTreeSet<String> {
let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
let manifest_text = fs::read_to_string(&manifest_path).unwrap_or_else(|error| {
panic!("read {}: {error}", manifest_path.display());
});
let manifest = manifest_text
.parse::<toml::Value>()
.unwrap_or_else(|error| {
panic!("parse {}: {error}", manifest_path.display());
});
manifest
.get("package")
.and_then(|package| package.get("include"))
.and_then(toml::Value::as_array)
.expect("Cargo.toml package.include should be an array")
.iter()
.map(|entry| {
entry
.as_str()
.expect("Cargo.toml package.include entries should be strings")
.to_string()
})
.collect()
}
fn assert_plugin_relative_path(value: &Value, field: &str) {
let path = value
.get(field)
.and_then(Value::as_str)
.unwrap_or_else(|| panic!("manifest should include {field}"));
assert!(
path.starts_with("./"),
"manifest {field} should be plugin-root relative and ./-prefixed"
);
}
#[test]
fn ccc_plugin_manifest_version_matches_crate_version() {
let crate_manifest_path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/plugin/.codex-plugin/plugin.json");
let crate_manifest = read_json(&crate_manifest_path);
assert_eq!(crate_manifest["version"], env!("CARGO_PKG_VERSION"));
if let Some(root) = source_repo_root() {
let public_manifest = read_json(&root.join(".codex-plugin/plugin.json"));
assert_eq!(public_manifest["version"], env!("CARGO_PKG_VERSION"));
}
}
#[test]
fn ccc_plugin_metadata_contains_no_stale_version_strings() {
let crate_manifest_path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/plugin/.codex-plugin/plugin.json");
let mut metadata_paths = vec![crate_manifest_path];
if let Some(root) = source_repo_root() {
metadata_paths.push(root.join(".codex-plugin/plugin.json"));
}
let mut stale_versions = Vec::new();
for metadata_path in metadata_paths {
let metadata = read_json(&metadata_path);
collect_stale_version_strings(
&metadata,
&metadata_path.display().to_string(),
&mut stale_versions,
);
}
assert!(
stale_versions.is_empty(),
"plugin metadata should only mention crate version {}; stale strings: {:?}",
env!("CARGO_PKG_VERSION"),
stale_versions
);
}
#[test]
fn cargo_packaged_asset_list_matches_runtime_lookup_contract() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let include_entries = cargo_package_include_entries();
let mut runtime_relative_paths = BTreeSet::new();
for asset in PACKAGED_RUNTIME_ASSETS {
assert!(
include_entries.contains(asset.package_include_path),
"Cargo package should include {} at {} for runtime lookup {}",
asset.name,
asset.package_include_path,
asset.runtime_relative_path
);
assert!(
manifest_dir.join(asset.package_include_path).is_file(),
"packaged source asset should exist for {} at {}",
asset.name,
asset.package_include_path
);
assert!(
runtime_relative_paths.insert(asset.runtime_relative_path.to_string()),
"runtime lookup path should be unique: {}",
asset.runtime_relative_path
);
}
assert!(
include_entries.contains(SSL_PACKAGE_INCLUDE_PATH),
"Cargo package should include SSL manifests via {SSL_PACKAGE_INCLUDE_PATH}"
);
let ssl_asset_dir = manifest_dir.join("assets/skills/ssl");
let mut ssl_manifest_names = fs::read_dir(&ssl_asset_dir)
.unwrap_or_else(|error| panic!("read {}: {error}", ssl_asset_dir.display()))
.map(|entry| {
entry
.expect("read SSL manifest entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.filter(|name| name.ends_with(".skill.ssl.json"))
.collect::<Vec<_>>();
ssl_manifest_names.sort();
assert_eq!(ssl_manifest_names.len(), 10);
for manifest_name in ssl_manifest_names {
let runtime_path = format!("{SSL_RUNTIME_RELATIVE_DIR}/{manifest_name}");
assert!(
runtime_relative_paths.insert(runtime_path),
"runtime SSL lookup path should be unique"
);
}
}
fn is_server_config(value: &Value) -> bool {
value
.get("command")
.and_then(Value::as_str)
.is_some_and(|command| !command.is_empty())
}
fn mcp_server_map(value: &Value) -> &Map<String, Value> {
if let Some(map) = value.get("mcpServers").and_then(Value::as_object) {
return map;
}
if let Some(map) = value.get("mcp_servers").and_then(Value::as_object) {
return map;
}
value
.as_object()
.filter(|map| !map.is_empty() && map.values().all(is_server_config))
.expect(".mcp.json should be a direct server map or a wrapped mcpServers/mcp_servers map")
}
fn read_plugin_skill(root: &Path) -> String {
let skill_path = root.join("skills/ccc/SKILL.md");
fs::read_to_string(&skill_path).unwrap_or_else(|error| {
panic!("read {}: {error}", skill_path.display());
})
}
fn assert_skill_contains(skill: &str, expected: &str) {
assert!(
skill.contains(expected),
"plugin skill should include workflow contract text: {expected}"
);
}
#[test]
fn ccc_plugin_manifest_points_to_root_relative_package_assets() {
let root = repo_root();
let manifest_path = root.join(".codex-plugin/plugin.json");
let manifest = read_json(&manifest_path);
assert_eq!(manifest["name"], "ccc");
assert_eq!(manifest["mcpServers"], "./.mcp.json");
assert_eq!(manifest["skills"], "./skills/");
assert_plugin_relative_path(&manifest, "mcpServers");
assert_plugin_relative_path(&manifest, "skills");
let codex_plugin_entries = fs::read_dir(root.join(".codex-plugin"))
.expect("read .codex-plugin")
.map(|entry| {
entry
.expect("read .codex-plugin entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.collect::<Vec<_>>();
assert_eq!(codex_plugin_entries, vec!["plugin.json"]);
}
#[test]
fn ccc_plugin_mcp_launcher_starts_ccc_stdio_server() {
let root = repo_root();
let mcp = read_json(&root.join(".mcp.json"));
let servers = mcp_server_map(&mcp);
let ccc = servers
.get("ccc")
.expect("ccc MCP server should be declared");
assert_eq!(ccc["command"], "ccc");
assert_eq!(ccc["args"], serde_json::json!(["mcp"]));
}
#[test]
fn ccc_plugin_bundled_skill_preserves_cap_entrypoint() {
let root = repo_root();
let skill = read_plugin_skill(&root);
assert!(skill.contains("name: ccc"));
assert!(skill.contains("$cap"));
assert!(skill.contains("public CCC entry point"));
assert!(!skill.contains("name: cap"));
}
#[test]
fn ccc_plugin_bundled_skill_stays_install_discovery_metadata() {
let root = repo_root();
let skill = read_plugin_skill(&root);
assert_skill_contains(&skill, "Install/discovery metadata");
assert_skill_contains(&skill, "not a user command surface");
assert_skill_contains(&skill, "not as a runbook");
assert_skill_contains(
&skill,
"Any execution guidance belongs in persisted CCC memory",
);
assert!(!skill.contains("CCC role agents"));
assert!(!skill.contains("start a CCC run"));
assert!(!skill.contains("fan-in"));
}
#[test]
fn ccc_plugin_bundled_skill_keeps_plugin_invocation_secondary() {
let root = repo_root();
let skill = read_plugin_skill(&root);
assert_skill_contains(&skill, "host may use the packaged asset for discovery");
assert_skill_contains(&skill, "as replacements for");
assert_skill_contains(&skill, "public CCC entry point");
}
#[test]
fn ccc_packaged_hook_source_lists_expected_event_responsibilities() {
let hook_source = read_json(
&Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets")
.join("plugin")
.join("hooks")
.join("hooks.json"),
);
let hooks = hook_source["hooks"].as_object().expect("hooks event map");
for event in [
"UserPromptSubmit",
"SessionStart",
"PermissionRequest",
"PostToolUse",
"Stop",
] {
let groups = hooks
.get(event)
.and_then(Value::as_array)
.unwrap_or_else(|| panic!("{event} groups"));
assert_eq!(groups.len(), 1);
let handlers = groups[0]["hooks"]
.as_array()
.unwrap_or_else(|| panic!("{event} handlers"));
assert_eq!(handlers.len(), 1);
assert_eq!(handlers[0]["type"], "command");
assert_eq!(handlers[0]["command"], "ccc hook run");
assert_eq!(handlers[0]["timeout"], 30);
}
}
#[test]
fn crates_io_packaged_cap_skill_asset_matches_public_skill() {
let crate_asset = fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/skills/cap/SKILL.md"),
)
.expect("read crate cap skill asset");
if let Some(root) = source_repo_root() {
let public_skill =
fs::read_to_string(root.join("skills/cap/SKILL.md")).expect("read public cap skill");
assert_eq!(crate_asset, public_skill);
} else {
assert!(crate_asset.contains("name: cap"));
assert!(crate_asset.contains("public CCC entry point"));
}
}
#[test]
fn crates_io_packaged_ssl_manifest_assets_match_public_manifests() {
let crate_asset_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/skills/ssl");
let manifest_source_dir = source_repo_root()
.map(|root| root.join("skills/ssl"))
.unwrap_or_else(|| crate_asset_dir.clone());
let mut manifest_names = fs::read_dir(&manifest_source_dir)
.unwrap_or_else(|error| panic!("read {}: {error}", manifest_source_dir.display()))
.map(|entry| {
entry
.expect("read public manifest entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.filter(|name| name.ends_with(".skill.ssl.json"))
.collect::<Vec<_>>();
manifest_names.sort();
assert_eq!(manifest_names.len(), 10);
for manifest_name in manifest_names {
let public_manifest = fs::read_to_string(manifest_source_dir.join(&manifest_name))
.expect("read public manifest");
let crate_asset = fs::read_to_string(crate_asset_dir.join(&manifest_name))
.expect("read crate manifest asset");
assert_eq!(crate_asset, public_manifest, "{manifest_name}");
}
}