use std::path::{Path, PathBuf};
use std::process::Command;
fn checkout() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.unwrap()
}
#[test]
fn actual_standalone_archive_supports_doctor_and_handoff() {
let root = checkout();
let temp = tempfile::tempdir().unwrap();
let output = Command::new("python3")
.args([
"-c",
r#"
import json, runpy, sys, zipfile
from pathlib import Path
builder = runpy.run_path(sys.argv[1])
scratch = Path(sys.argv[2])
version = sys.argv[4]
archive = scratch / f'shepherd-{version}-plugin.zip'
receipt = builder['build_archive'](archive)
assert builder['validate_archive'](archive, version, Path(sys.argv[3])) == receipt
# A portable root remains canonical without the compatibility overlay. Its
# inline OpenAI settings must work independently of a legacy installation.
plugin = scratch / 'without-overlay'
with zipfile.ZipFile(archive) as source:
source.extractall(plugin)
(plugin / '.codex-plugin/plugin.json').unlink()
(plugin / 'hooks/scripts/seed_preflight_check.sh').chmod(0o755)
builder['check_cli'](Path(sys.argv[3]), plugin, version)
# Without inline settings, the portable manifest selects the supported overlay.
plugin = scratch / 'portable-fallback'
with zipfile.ZipFile(archive) as source:
source.extractall(plugin)
manifest = json.loads((plugin / 'plugin.json').read_text())
del manifest['extensions']
(plugin / 'plugin.json').write_text(json.dumps(manifest))
(plugin / 'hooks/scripts/seed_preflight_check.sh').chmod(0o755)
builder['check_cli'](Path(sys.argv[3]), plugin, version)
# The same root layout is also usable by older hosts through the overlay.
plugin = scratch / 'overlay-only'
with zipfile.ZipFile(archive) as source:
source.extractall(plugin)
(plugin / 'plugin.json').unlink()
(plugin / 'hooks/scripts/seed_preflight_check.sh').chmod(0o755)
builder['check_cli'](Path(sys.argv[3]), plugin, version)
print(json.dumps(receipt))
"#,
])
.arg(root.join("scripts/build-portable-plugin.py"))
.arg(temp.path())
.arg(env!("CARGO_BIN_EXE_shepherd"))
.arg(env!("CARGO_PKG_VERSION"))
.output()
.unwrap();
assert!(
output.status.success(),
"standalone portable carrier: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn portable_manifest_is_authoritative_and_escaping_hooks_are_rejected() {
let temp = tempfile::tempdir().unwrap();
let manifest = temp.path().join("plugin.json");
let value = serde_json::json!({
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "shepherd", "version": env!("CARGO_PKG_VERSION"),
"extensions": {"com.openai": {"hooks": "../outside/hooks.json"}}
});
std::fs::write(&manifest, value.to_string()).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(["doctor", "--host", "codex", "--plugin-root"])
.arg(temp.path())
.arg("--json")
.output()
.unwrap();
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stdout).contains("must stay inside plugin root"),
"{}",
String::from_utf8_lossy(&output.stdout)
);
std::fs::write(&manifest, "{").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(["doctor", "--host", "codex", "--plugin-root"])
.arg(temp.path())
.arg("--json")
.output()
.unwrap();
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("not valid JSON"));
assert_eq!(std::fs::read_to_string(&manifest).unwrap(), "{");
}