shepherd-cli 6.8.2

Small installed-plugin diagnostics for the Shepherd workflow
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(), "{");
}