use std::fs;
use std::os::unix::fs::{PermissionsExt, symlink};
use serde_json::Value;
use super::agent::{Agent, NO_PASS};
use super::bundle::install_current;
use super::tests::{Fixture, TestResult};
fn agent(
issuer: &str,
id: &str,
key: &std::path::Path,
) -> Result<Agent, Box<dyn std::error::Error>> {
Agent::parse(Some(issuer.into()), Some(id.into()), Some(key.into()))?
.ok_or_else(|| "agent proof missing".into())
}
fn read(fixture: &Fixture, name: &str) -> Result<Value, Box<dyn std::error::Error>> {
Ok(serde_json::from_slice(&fs::read(
fixture.bundle.join(name),
)?)?)
}
#[test]
fn install_binds_both_plugin_environments_without_copying_the_key() -> TestResult {
let fixture = Fixture::new()?;
let key = fixture.directory.path().join("agent-key");
fs::write(&key, "fixture-private-key-not-a-bundle-payload")?;
let key = fs::canonicalize(key)?;
let alias = fixture.directory.path().join("key-alias");
symlink(&key, &alias)?;
let proof = agent("https://lys.example", "agent-1", &alias)?;
let report = install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
Ok(fixture.binary.clone())
})?;
assert_eq!(report.files, 10);
let claude = read(&fixture, ".claude-plugin/plugin.json")?;
let codex = read(&fixture, ".mcp.json")?;
let claude = &claude["mcpServers"]["haematite"];
let codex = &codex["mcpServers"]["haematite"];
assert_eq!(claude["command"], "${CLAUDE_PLUGIN_ROOT}/scripts/mcp.sh");
assert_eq!(
codex["command"],
fixture
.bundle
.join("scripts/mcp.sh")
.to_str()
.ok_or("path")?
);
assert_eq!(claude["env"], codex["env"]);
assert_eq!(codex["env"]["HAEMATITE_LYS_ISSUER"], "https://lys.example");
assert_eq!(codex["env"]["HAEMATITE_LYS_AGENT"], "agent-1");
assert_eq!(
codex["env"]["HAEMATITE_KEY_FILE"],
key.to_str().ok_or("path")?
);
for name in ["HAEMATITE_BIN", "HAEMATITE_KEY_FILE"] {
let path = std::path::Path::new(codex["env"][name].as_str().ok_or("env path")?);
assert!(path.is_absolute());
assert!(path.is_file());
}
assert!(
read(&fixture, ".claude-plugin/plugin.json")?
.get("userConfig")
.is_none()
);
assert_eq!(
fs::metadata(fixture.bundle.join("scripts/mcp.sh"))?
.permissions()
.mode()
& 0o777,
0o700
);
let manifest = read(&fixture, "bundle.json")?;
for name in manifest["files"].as_object().ok_or("inventory")?.keys() {
let bytes = fs::read(fixture.bundle.join(name))?;
assert!(
!bytes
.windows(b"fixture-private-key-not-a-bundle-payload".len())
.any(|window| window == b"fixture-private-key-not-a-bundle-payload")
);
}
let repeat = install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
Ok(fixture.binary.clone())
})?;
assert!(repeat.already_installed);
let changed = agent("https://lys.example", "agent-2", &key)?;
let error = install_current(&fixture.config, &fixture.bundle, Some(&changed), || {
Ok(fixture.binary.clone())
})
.err()
.ok_or("changed identity overwrote bundle")?;
assert_eq!(error.code, "bundle_conflict");
Ok(())
}
#[test]
fn install_launcher_passes_the_agent_proof_to_the_operator() -> TestResult {
let fixture = Fixture::new()?;
fs::write(&fixture.binary, "#!/bin/sh\nprintf '%s\\n' \"$@\"\n")?;
let key = fixture.directory.path().join("key");
fs::write(&key, "fixture-key")?;
let key = fs::canonicalize(key)?;
let proof = agent("https://lys.example", "agent-1", &key)?;
install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
Ok(fixture.binary.clone())
})?;
let mcp = read(&fixture, ".mcp.json")?;
let server = &mcp["mcpServers"]["haematite"];
let mut command = std::process::Command::new(server["command"].as_str().ok_or("command")?);
command.env_clear();
for (name, value) in server["env"].as_object().ok_or("env")? {
command.env(name, value.as_str().ok_or("value")?);
}
let output = command.output()?;
assert!(output.status.success());
let args = String::from_utf8(output.stdout)?;
assert_eq!(
args.lines().collect::<Vec<_>>(),
vec![
"mcp",
"--remote",
"https://haematite.ablative.com.au/mcp",
"--lys-agent",
"https://lys.example",
"agent-1",
"--key-file",
key.to_str().ok_or("path")?
]
);
assert!(output.stderr.is_empty());
Ok(())
}
#[test]
fn install_without_an_agent_documents_the_named_refusal() -> TestResult {
let fixture = Fixture::new()?;
super::install(&fixture.binary, &fixture.config, &fixture.bundle)?;
let page = fs::read_to_string(fixture.bundle.join("skills/haematite/SKILL.md"))?;
assert!(page.contains(NO_PASS));
for name in [".mcp.json", ".claude-plugin/plugin.json"] {
assert!(
read(&fixture, name)?["mcpServers"]["haematite"]["env"]
.get("HAEMATITE_CONFIG")
.is_none()
);
}
Ok(())
}
#[test]
fn install_validates_identity_and_key_paths_before_publication() -> TestResult {
let fixture = Fixture::new()?;
let key = fixture.directory.path().join("key");
fs::write(&key, "fixture-key")?;
let key = fs::canonicalize(key)?;
for (issuer, id) in [
("https://lys.example/path", "agent-1"),
("http://lys.example", "agent-1"),
("https://user@lys.example", "agent-1"),
("https://lys.example#fragment", "agent-1"),
("https://lys.example", "agent/1"),
("https://lys.example", ""),
] {
let error = agent(issuer, id, &key)
.err()
.ok_or("invalid identity accepted")?;
assert!(error.to_string().starts_with("bundle_lys_agent_invalid"));
}
let missing = agent(
"https://lys.example",
"agent-1",
&fixture.directory.path().join("missing"),
)
.err()
.ok_or("missing key accepted")?;
assert!(missing.to_string().starts_with("bundle_lys_key_invalid"));
assert!(!fixture.bundle.exists());
Ok(())
}
#[test]
fn enrolment_defaults_to_central_services_without_a_local_config() -> TestResult {
let fixture = Fixture::new()?;
let key = fixture.directory.path().join("key");
fs::write(&key, "fixture-key")?;
let proof =
Agent::parse(None, Some("agent-1".into()), Some(key))?.ok_or("missing enrolment")?;
install_current(&fixture.config, &fixture.bundle, Some(&proof), || {
Ok(fixture.binary.clone())
})?;
for name in [".mcp.json", ".claude-plugin/plugin.json"] {
let config = read(&fixture, name)?;
let env = &config["mcpServers"]["haematite"]["env"];
assert_eq!(
env["HAEMATITE_URL"],
"https://haematite.ablative.com.au/mcp"
);
assert_eq!(env["HAEMATITE_LYS_ISSUER"], "https://lys.ablative.com.au");
assert!(env.get("HAEMATITE_CONFIG").is_none());
}
Ok(())
}