use std::path::{Path, PathBuf};
use serde_json::Value;
use super::{
blob_entries, compress_dir, dir_hash, dir_hash_for_client, expand_client_entries, generate_marketplace, tree_hash, write_version_dir,
};
use crate::host::Plugin;
const FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/plugin");
fn test_plugin() -> Plugin {
Plugin {
name: "ez-test-plugin",
marketplace: "ez-test-mkt",
version: "0.1.0",
agents: &["claude"],
instructions: None,
statusline: None,
blob: &[],
}
}
fn fixture_blob() -> Vec<u8> {
compress_dir(Path::new(FIXTURE)).unwrap()
}
fn scratch() -> PathBuf {
let dir = crate::scratch::path("ez-mat");
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn embedded_hash_equals_materialized_hash() {
let plugin = test_plugin();
let blob = fixture_blob();
let mut entries = blob_entries(&blob).unwrap();
expand_client_entries(&mut entries, "claude");
let root = scratch();
let versions = root.join("versions");
std::fs::create_dir_all(&versions).unwrap();
let version_dir = versions.join("0.1.0@claude");
write_version_dir(&plugin, &entries, &versions, &version_dir).unwrap();
assert!(version_dir.join(".claude-plugin/marketplace.json").exists());
assert_eq!(dir_hash(&version_dir).unwrap(), tree_hash(&blob, "claude").unwrap());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn client_token_materializes_per_client_and_hashes_match_baseline() {
let src = scratch();
let cp = src.join(".claude-plugin");
std::fs::create_dir_all(&cp).unwrap();
std::fs::write(cp.join("plugin.json"), br#"{"name":"tok","version":"0.1.0","description":"d","author":{"name":"a"}}"#).unwrap();
std::fs::create_dir_all(src.join("hooks")).unwrap();
std::fs::write(
src.join("hooks").join("hooks.json"),
br#"{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"h --client ${AGENTGEAR_CLIENT}"}]}]}}"#,
)
.unwrap();
let blob = compress_dir(&src).unwrap();
let plugin = Plugin {
name: "tok",
marketplace: "tok-mkt",
version: "0.1.0",
agents: &["claude"],
instructions: None,
statusline: None,
blob: &[],
};
let root = scratch();
let versions = root.join("versions");
std::fs::create_dir_all(&versions).unwrap();
let mut entries = blob_entries(&blob).unwrap();
expand_client_entries(&mut entries, "claude");
let version_dir = versions.join("0.1.0@claude");
write_version_dir(&plugin, &entries, &versions, &version_dir).unwrap();
let h = std::fs::read_to_string(version_dir.join("hooks/hooks.json")).unwrap();
assert!(h.contains("--client claude"), "token not substituted:\n{h}");
assert!(!h.contains("${AGENTGEAR_CLIENT}"), "raw token survived on disk:\n{h}");
assert_eq!(dir_hash(&version_dir).unwrap(), tree_hash(&blob, "claude").unwrap());
assert_eq!(dir_hash(&version_dir).unwrap(), dir_hash_for_client(&src, "claude").unwrap());
assert_ne!(tree_hash(&blob, "claude").unwrap(), tree_hash(&blob, "copilot-cli").unwrap());
std::fs::remove_dir_all(&src).ok();
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn write_version_dir_is_idempotent_when_target_exists() {
let plugin = test_plugin();
let entries = blob_entries(&fixture_blob()).unwrap();
let root = scratch();
let versions = root.join("versions");
std::fs::create_dir_all(&versions).unwrap();
let version_dir = versions.join("0.1.0@claude");
write_version_dir(&plugin, &entries, &versions, &version_dir).unwrap();
let first = dir_hash(&version_dir).unwrap();
write_version_dir(&plugin, &entries, &versions, &version_dir).unwrap();
assert_eq!(dir_hash(&version_dir).unwrap(), first);
let temps: Vec<_> = std::fs::read_dir(&versions)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
.collect();
assert!(temps.is_empty(), "leftover temp dirs: {temps:?}");
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn generated_marketplace_has_no_version_and_no_bom() {
let plugin = test_plugin();
let entries = blob_entries(&fixture_blob()).unwrap();
let bytes = generate_marketplace(&plugin, &entries).unwrap();
assert_ne!(bytes.first(), Some(&0xEF), "must not start with a UTF-8 BOM");
let value: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(value["name"], "ez-test-mkt");
assert!(value.get("version").is_none(), "marketplace entry must not carry a version");
assert!(value["description"].is_string());
assert!(value["owner"]["name"].is_string());
assert_eq!(value["plugins"][0]["source"], "./");
assert_eq!(value["plugins"][0]["name"], "ez-test-plugin");
}
#[test]
fn blob_roundtrips_and_compresses_repetitive_input() {
let dir = scratch();
let cp = dir.join(".claude-plugin");
std::fs::create_dir_all(&cp).unwrap();
std::fs::write(cp.join("plugin.json"), br#"{"name":"x","version":"0.1.0","description":"d","author":{"name":"a"}}"#).unwrap();
let big = "abcdefgh".repeat(8192); std::fs::write(dir.join("big.txt"), big.as_bytes()).unwrap();
let blob = compress_dir(&dir).unwrap();
assert!(blob.len() * 4 < big.len(), "blob {} not materially smaller than raw {}", blob.len(), big.len());
let entries = blob_entries(&blob).unwrap();
let got = entries.iter().find(|(rel, _)| rel == "big.txt").map(|(_, b)| b.clone()).unwrap();
assert_eq!(got, big.as_bytes());
std::fs::remove_dir_all(&dir).ok();
}