use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
fn cartog_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cartog"))
}
fn fixture(pin: &str) -> (tempfile::TempDir, PathBuf, PathBuf) {
let dir = tempfile::TempDir::new().expect("tempdir");
let root = dir.path();
let project = root.join("project");
std::fs::create_dir_all(&project).expect("project dir");
std::fs::write(project.join(".cartog.toml"), "").expect("write config");
let plugin_root = root.join("plugin");
std::fs::create_dir_all(plugin_root.join(".claude-plugin")).expect("plugin dir");
std::fs::write(
plugin_root.join(".claude-plugin").join("plugin.json"),
format!(r#"{{"name":"cartog","version":"{pin}"}}"#),
)
.expect("write manifest");
(dir, project, plugin_root)
}
fn serve_instructions(project: &Path, envs: &[(&str, &str)], home: &Path) -> String {
let mut cmd = Command::new(cartog_bin());
cmd.arg("serve")
.current_dir(project)
.env("CARTOG_SINGLE_WRITER", "0")
.env("HOME", home)
.env("XDG_STATE_HOME", home.join("state"))
.env("XDG_DATA_HOME", home.join("data"))
.env("XDG_CONFIG_HOME", home.join("config"))
.env("CARTOG_REGISTRY", "")
.env_remove("CARTOG_PLUGIN_JSON")
.env_remove("CLAUDE_PLUGIN_ROOT")
.env_remove("CARTOG_NO_UPDATE_CHECK")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (k, v) in envs {
cmd.env(k, v);
}
let mut child = cmd.spawn().expect("spawn cartog serve");
let init = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"pin-test","version":"0"}}}"#;
{
let stdin = child.stdin.as_mut().expect("stdin");
writeln!(stdin, "{init}").expect("write initialize");
stdin.flush().expect("flush");
}
let mut line = String::new();
BufReader::new(child.stdout.take().expect("stdout"))
.read_line(&mut line)
.expect("read initialize response");
let mut stderr = String::new();
let _ = child.kill();
if let Some(mut e) = child.stderr.take() {
use std::io::Read;
let _ = e.read_to_string(&mut stderr);
}
let _ = child.wait();
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap_or_else(|e| {
panic!("initialize response was not JSON ({e}): {line:?}\nserver stderr: {stderr}")
});
parsed["result"]["instructions"]
.as_str()
.unwrap_or_else(|| panic!("no instructions in response: {line}"))
.to_string()
}
#[test]
fn a_pin_behind_the_binary_reaches_serve_via_claude_plugin_root() {
let (dir, project, plugin_root) = fixture("99.9.9");
let text = serve_instructions(
&project,
&[("CLAUDE_PLUGIN_ROOT", plugin_root.to_str().unwrap())],
dir.path(),
);
assert!(
text.contains("99.9.9"),
"instructions must name the pin the plugin root resolved to: {text}"
);
assert!(
text.contains(env!("CARGO_PKG_VERSION")),
"instructions must name the running version: {text}"
);
}
#[test]
fn a_pin_behind_the_binary_reaches_serve_via_cartog_plugin_json() {
let (dir, project, plugin_root) = fixture("99.9.9");
let manifest = plugin_root.join(".claude-plugin").join("plugin.json");
let text = serve_instructions(
&project,
&[("CARTOG_PLUGIN_JSON", manifest.to_str().unwrap())],
dir.path(),
);
assert!(
text.contains("99.9.9"),
"instructions must name the pin the explicit path resolved to: {text}"
);
}
#[test]
fn a_pin_the_binary_already_satisfies_says_nothing() {
let (dir, project, plugin_root) = fixture("0.0.1");
let text = serve_instructions(
&project,
&[("CLAUDE_PLUGIN_ROOT", plugin_root.to_str().unwrap())],
dir.path(),
);
assert!(
!text.contains("0.0.1") && !text.contains("NOTE:"),
"a binary ahead of the pin must not report drift: {text}"
);
}
#[test]
fn the_update_check_kill_switch_silences_the_drift_sentence() {
let (dir, project, plugin_root) = fixture("99.9.9");
let text = serve_instructions(
&project,
&[
("CLAUDE_PLUGIN_ROOT", plugin_root.to_str().unwrap()),
("CARTOG_NO_UPDATE_CHECK", "1"),
],
dir.path(),
);
assert!(
!text.contains("99.9.9") && !text.contains("NOTE:"),
"CARTOG_NO_UPDATE_CHECK must silence the drift sentence: {text}"
);
}
#[test]
fn no_manifest_leaves_the_instructions_untouched() {
let (dir, project, _plugin_root) = fixture("99.9.9");
let text = serve_instructions(&project, &[], dir.path());
assert!(
!text.contains("NOTE:"),
"a server with no discoverable pin must not report drift: {text}"
);
}