use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_galdr")
}
struct Sandbox {
home: tempfile::TempDir,
}
impl Sandbox {
fn new() -> Self {
Self {
home: tempfile::tempdir().unwrap(),
}
}
fn home(&self) -> &Path {
self.home.path()
}
fn cmd(&self) -> Command {
let mut command = Command::new(bin());
command.env("HOME", self.home.path());
command
}
fn run(&self, args: &[&str]) -> Output {
self.cmd().args(args).output().unwrap()
}
fn hook(&self, json: &str, fail: bool) -> Output {
let mut command = self.cmd();
command.arg("hook");
if fail {
command.env("GALDR_HOOK_FAIL", "1");
}
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(json.as_bytes())
.unwrap();
child.wait_with_output().unwrap()
}
fn span_lines(&self, rec_id: &str) -> usize {
let path = self
.home()
.join(".galdr/spans")
.join(format!("{rec_id}.jsonl"));
std::fs::read_to_string(path)
.map(|s| s.lines().filter(|l| !l.trim().is_empty()).count())
.unwrap_or(0)
}
fn active_rec_id(&self) -> String {
let dir = self.home().join(".galdr/active.d");
let mut ids: Vec<String> = std::fs::read_dir(&dir)
.unwrap()
.flatten()
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("json"))
.map(|e| {
let raw = std::fs::read_to_string(e.path()).unwrap();
let value: serde_json::Value = serde_json::from_str(&raw).unwrap();
value["rec_id"].as_str().unwrap().to_string()
})
.collect();
assert_eq!(ids.len(), 1, "expected exactly one active recording");
ids.pop().unwrap()
}
fn active_rec_id_by_name(&self, name: &str) -> String {
let dir = self.home().join(".galdr/active.d");
for entry in std::fs::read_dir(&dir).unwrap().flatten() {
if entry.path().extension().and_then(|x| x.to_str()) != Some("json") {
continue;
}
let raw = std::fs::read_to_string(entry.path()).unwrap();
let value: serde_json::Value = serde_json::from_str(&raw).unwrap();
if value["name"].as_str() == Some(name) {
return value["rec_id"].as_str().unwrap().to_string();
}
}
panic!("no active recording named {name}");
}
fn recording_ids(&self) -> Vec<String> {
let dir = self.home().join(".galdr/recordings");
let mut ids: Vec<String> = std::fs::read_dir(dir)
.map(|entries| {
entries
.flatten()
.filter_map(|entry| {
let path = entry.path();
if path.extension()?.to_str()? == "json" {
Some(path.file_stem()?.to_str()?.to_string())
} else {
None
}
})
.collect()
})
.unwrap_or_default();
ids.sort();
ids
}
fn record(&self, name: &str, events: &[&str]) -> String {
let before = self.recording_ids();
assert!(self.run(&["rec", "start", name]).status.success());
for event in events {
assert!(self.hook(event, false).status.success());
}
assert!(self.run(&["rec", "stop"]).status.success());
self.recording_ids()
.into_iter()
.find(|id| !before.contains(id))
.expect("a new recording id")
}
fn skill_md(&self, skill_name: &str) -> String {
let path = self
.home()
.join(".agents/skills")
.join(skill_name)
.join("SKILL.md");
std::fs::read_to_string(path).unwrap()
}
}
fn write_index_fixture(sb: &Sandbox, entries: &[(&str, bool)]) -> PathBuf {
let path = sb.home().join("index.json");
let mut body = String::new();
for (vers, yanked) in entries {
body.push_str(&format!(
r#"{{"name":"galdr","vers":"{vers}","deps":[],"cksum":"x","features":{{}},"yanked":{yanked}}}"#
));
body.push('\n');
}
std::fs::write(&path, body).unwrap();
path
}
#[cfg(target_os = "macos")]
fn fake_launchctl(sb: &Sandbox) -> (PathBuf, PathBuf) {
use std::os::unix::fs::PermissionsExt;
let log = sb.home().join("launchctl.log");
let script = sb.home().join("fake-launchctl.sh");
std::fs::write(
&script,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {}\nexit 0\n",
log.display()
),
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
(script, log)
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
const BASH_STATUS: &str =
r#"{"tool_name":"Bash","tool_input":{"command":"git status"},"tool_response":{}}"#;
fn write_human_recording(sb: &Sandbox, rec_id: &str, name: &str, value: serde_json::Value) {
let root = sb.home().join(".galdr");
std::fs::create_dir_all(root.join("spans")).unwrap();
std::fs::create_dir_all(root.join("recordings")).unwrap();
let event = serde_json::json!({
"ts": "2026-06-30T00:00:00Z",
"seq": 0,
"tool_name": "human.browser.input",
"tool_input": {},
"tool_response": {},
"event_kind": "human",
"human": {
"source": {
"kind": "browser",
"url": "https://example.test/issues/new",
"title": "New issue"
},
"action": "human.browser.input",
"target": {
"primary": {
"kind": "label",
"value": "Issue title"
},
"label": "Issue title"
},
"value": value,
"verification_hint": "Confirm the issue was saved."
}
});
std::fs::write(
root.join("spans").join(format!("{rec_id}.jsonl")),
format!("{}\n", serde_json::to_string(&event).unwrap()),
)
.unwrap();
let recording = serde_json::json!({
"rec_id": rec_id,
"name": name,
"started_at": "2026-06-30T00:00:00Z",
"ended_at": "2026-06-30T00:01:00Z",
"steps": 1,
"cwd": null
});
std::fs::write(
root.join("recordings").join(format!("{rec_id}.json")),
serde_json::to_string_pretty(&recording).unwrap(),
)
.unwrap();
}
fn active_browser_port(sb: &Sandbox) -> u16 {
let raw =
std::fs::read_to_string(sb.home().join(".galdr/observe/browser-active.json")).unwrap();
let value: serde_json::Value = serde_json::from_str(&raw).unwrap();
value["port"].as_u64().unwrap() as u16
}
fn post_browser_event(port: u16, event: serde_json::Value) {
let body = serde_json::to_string(&event).unwrap();
let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap();
let request = format!(
"POST /event HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
stream.write_all(request.as_bytes()).unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
assert!(
response.starts_with("HTTP/1.1 204"),
"unexpected response: {response}"
);
}
#[test]
fn json_output_is_machine_readable() {
let sb = Sandbox::new();
let id = sb.record("json task", &[BASH_STATUS]);
let refined = sb.home().join("r.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-json-task\ndescription: \"json\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
assert!(
sb.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap()
.status
.success()
);
let parse = |args: &[&str]| -> serde_json::Value {
let out = sb.run(args);
assert!(out.status.success(), "{args:?} failed");
serde_json::from_str(&stdout(&out))
.unwrap_or_else(|e| panic!("{args:?} did not emit valid JSON: {e}"))
};
let list = parse(&["list", "--json"]);
assert!(
list.as_array()
.unwrap()
.iter()
.any(|r| r["rec_id"] == id.as_str())
);
let show = parse(&["show", &id, "--json"]);
assert_eq!(show["recording"]["name"], "json task");
assert_eq!(show["steps"].as_array().unwrap().len(), 1);
let skills = parse(&["skills", "--json"]);
let skill = skills
.as_array()
.unwrap()
.iter()
.find(|s| s["skill_name"] == "galdr-json-task")
.expect("the distilled skill is listed");
assert_eq!(skill["origin"], "galdr");
let harnesses = parse(&["harnesses", "--json"]);
assert!(!harnesses.as_array().unwrap().is_empty());
assert!(
harnesses
.as_array()
.unwrap()
.iter()
.any(|h| h["key"] == "claude")
);
assert!(
sb.run(&[
"outcome",
"usage",
"--skill",
"galdr-json-task",
"--rec",
&id,
"--outcome",
"success",
])
.status
.success()
);
let outcomes = parse(&["outcome", "list", "--json"]);
assert!(outcomes["usage"].is_array());
assert!(outcomes["labels"].is_array());
}
#[test]
fn default_distill_writes_an_unauthored_draft() {
let sb = Sandbox::new();
let id = sb.record(
"deploy preview",
&[
BASH_STATUS,
r#"{"tool_name":"Write","tool_input":{"file_path":"/repo/out.txt"},"tool_response":{}}"#,
],
);
let out = sb.run(&["distill", &id]);
assert!(out.status.success());
let said = stdout(&out);
assert!(said.contains("author the real skill"), "{said}");
assert!(said.contains("galdr distill --from"), "{said}");
let skill = sb.skill_md("galdr-deploy-preview");
assert!(skill.contains("galdr:unauthored"), "draft marker:\n{skill}");
for section in ["## When to use", "## Steps", "## Verification"] {
assert!(skill.contains(section), "missing {section}:\n{skill}");
}
let listing = stdout(&sb.run(&["skills"]));
assert!(
listing.contains("draft"),
"should list as draft:\n{listing}"
);
}
#[test]
fn fast_distill_produces_a_complete_usable_skill() {
let sb = Sandbox::new();
let id = sb.record(
"deploy preview",
&[
BASH_STATUS,
r#"{"tool_name":"Write","tool_input":{"file_path":"/repo/out.txt"},"tool_response":{}}"#,
],
);
assert!(sb.run(&["distill", &id, "--fast"]).status.success());
let skill = sb.skill_md("galdr-deploy-preview");
for section in ["## When to use", "## Inputs", "## Steps", "## Verification"] {
assert!(skill.contains(section), "missing {section}:\n{skill}");
}
assert!(!skill.contains("galdr:unauthored"));
assert!(!skill.contains("[galdr DRAFT]"));
assert!(!skill.contains("TODO(agent)"));
let listing = stdout(&sb.run(&["skills"]));
assert!(listing.contains("final"));
assert!(listing.contains("ready"));
}
#[test]
fn distill_name_chooses_the_skill_name() {
let sb = Sandbox::new();
let id = sb.record("whatever the recording was called", &[BASH_STATUS]);
assert!(
sb.run(&["distill", &id, "--fast", "--name", "rust-greenlight"])
.status
.success()
);
let md = sb.skill_md("rust-greenlight");
assert!(md.contains("name: rust-greenlight"), "{md}");
assert!(
!sb.home()
.join(".agents/skills/galdr-whatever-the-recording-was-called")
.exists(),
"the mechanical name must not also be created"
);
assert!(sb.run(&["validate", "rust-greenlight"]).status.success());
}
#[test]
fn distill_from_honors_the_frontmatter_name() {
let sb = Sandbox::new();
let id = sb.record("visual branding upgrade", &[BASH_STATUS]);
let refined = sb.home().join("refined.md");
std::fs::write(
&refined,
format!(
"---\nname: skill-family-upgrade\ndescription: \"upgrade a family of related skills\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let out = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap();
assert!(out.status.success(), "{}", stderr(&out));
assert!(
stdout(&out).contains(
"installed as skill-family-upgrade (renamed from recording slug galdr-visual-branding-upgrade)"
),
"{}",
stdout(&out)
);
assert!(
sb.home()
.join(".agents/skills/skill-family-upgrade/SKILL.md")
.exists()
);
assert!(
!sb.home()
.join(".agents/skills/galdr-visual-branding-upgrade")
.exists(),
"the recording-slug directory must not be created"
);
assert!(stdout(&sb.run(&["skills"])).contains("skill-family-upgrade"));
assert!(
sb.run(&["validate", "skill-family-upgrade"])
.status
.success(),
"the renamed skill must validate by its frontmatter name"
);
}
#[test]
fn distill_from_name_flag_overrides_the_frontmatter() {
let sb = Sandbox::new();
let id = sb.record("branding", &[BASH_STATUS]);
let refined = sb.home().join("refined.md");
std::fs::write(
&refined,
format!(
"---\nname: frontmatter-name\ndescription: \"x\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let out = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.args(["--name", "explicit-name"])
.output()
.unwrap();
assert!(out.status.success(), "{}", stderr(&out));
assert!(
sb.home()
.join(".agents/skills/explicit-name/SKILL.md")
.exists()
);
assert!(
!sb.home().join(".agents/skills/frontmatter-name").exists(),
"the frontmatter name must not win over an explicit --name"
);
}
#[test]
fn distill_from_refuses_a_frontmatter_name_held_by_another_skill() {
let sb = Sandbox::new();
let other = sb.record("other", &[BASH_STATUS]);
let other_file = sb.home().join("other.md");
std::fs::write(
&other_file,
format!(
"---\nname: taken-name\ndescription: \"the incumbent\"\n---\n\n## Provenance\n- rec_id: `{other}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
assert!(
sb.cmd()
.args(["distill", &other, "--from"])
.arg(&other_file)
.output()
.unwrap()
.status
.success()
);
let mine = sb.record("mine", &[BASH_STATUS]);
let mine_file = sb.home().join("mine.md");
std::fs::write(
&mine_file,
format!(
"---\nname: taken-name\ndescription: \"the challenger\"\n---\n\n## Provenance\n- rec_id: `{mine}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let refused = sb
.cmd()
.args(["distill", &mine, "--from"])
.arg(&mine_file)
.output()
.unwrap();
assert!(
!refused.status.success(),
"a colliding frontmatter name must be refused"
);
assert!(
stderr(&refused).contains("already exists"),
"{}",
stderr(&refused)
);
let incumbent =
std::fs::read_to_string(sb.home().join(".agents/skills/taken-name/SKILL.md")).unwrap();
assert!(incumbent.contains("the incumbent"), "{incumbent}");
assert!(!sb.home().join(".agents/skills/galdr-mine").exists());
}
#[test]
fn distill_from_migrates_a_stale_draft_under_the_slug() {
let sb = Sandbox::new();
let id = sb.record("family upgrade", &[BASH_STATUS]);
assert!(sb.run(&["distill", &id]).status.success());
assert!(
sb.home()
.join(".agents/skills/galdr-family-upgrade/SKILL.md")
.exists(),
"the draft is written under the recording slug"
);
let refined = sb.home().join("refined.md");
std::fs::write(
&refined,
format!(
"---\nname: skill-family-upgrade\ndescription: \"upgrade a family of related skills\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let out = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap();
assert!(out.status.success(), "{}", stderr(&out));
assert!(
sb.home()
.join(".agents/skills/skill-family-upgrade/SKILL.md")
.exists()
);
assert!(
!sb.home()
.join(".agents/skills/galdr-family-upgrade")
.exists(),
"the stale slug directory must be moved aside"
);
assert!(
sb.home()
.join(".agents/skills/.retired/galdr-family-upgrade/SKILL.md")
.exists(),
"the stale draft should be retired to .retired"
);
}
#[test]
fn validate_passes_clean_skills_and_refuses_bad_content() {
let sb = Sandbox::new();
let id = sb.record("validate demo", &[BASH_STATUS]);
assert!(sb.run(&["distill", &id]).status.success());
let ok = sb.run(&["validate", "galdr-validate-demo"]);
assert!(
ok.status.success(),
"a clean distilled skill must pass: {}",
String::from_utf8_lossy(&ok.stderr)
);
let all = sb.run(&["validate", "--all", "--json"]);
assert!(all.status.success());
let parsed: serde_json::Value = serde_json::from_str(&stdout(&all)).unwrap();
assert!(parsed.is_array(), "--json emits an array: {}", stdout(&all));
let bad = sb.home().join("bad.md");
std::fs::write(
&bad,
"---\nname: galdr-bad\ndescription: \"x\"\n---\n\n## When to use\n\nx\n\n## Steps\n\n1. **Read** — /Users/alice/secret.txt\n\n## Verification\n\ny\n",
)
.unwrap();
let refused = sb
.cmd()
.args(["validate", "--file"])
.arg(&bad)
.output()
.unwrap();
assert!(
!refused.status.success(),
"a personal path must fail the gate"
);
}
#[test]
fn setup_codex_check_and_print_work() {
let sb = Sandbox::new();
let missing = stdout(&sb.run(&["setup", "codex", "--check"]));
assert!(missing.contains("not found"));
let snippet = stdout(&sb.run(&["setup", "codex", "--print"]));
assert!(snippet.contains("PostToolUse"));
assert!(snippet.contains("galdr hook"));
assert!(
snippet.contains("/hooks"),
"print must explain trusting the hook"
);
let hooks = sb.home().join(".codex/hooks.json");
std::fs::create_dir_all(hooks.parent().unwrap()).unwrap();
std::fs::write(
&hooks,
r#"{"hooks":{"PostToolUse":[{"matcher":".*","hooks":[{"type":"command","command":"galdr hook"}]}]}}"#,
)
.unwrap();
let configured = stdout(&sb.run(&["setup", "codex", "--check"]));
assert!(configured.contains("is present"));
}
#[test]
fn setup_cursor_check_and_print_work() {
let sb = Sandbox::new();
let missing = stdout(&sb.run(&["setup", "cursor", "--check"]));
assert!(missing.contains("not found"));
let snippet = stdout(&sb.run(&["setup", "cursor", "--print"]));
assert!(snippet.contains("postToolUse"));
assert!(snippet.contains("galdr hook"));
let hooks = sb.home().join(".cursor/hooks.json");
std::fs::create_dir_all(hooks.parent().unwrap()).unwrap();
std::fs::write(&hooks, snippet).unwrap();
let configured = stdout(&sb.run(&["setup", "cursor", "--check"]));
assert!(configured.contains("is present"));
}
#[test]
fn a_cursor_event_records_with_mapped_fields() {
let sb = Sandbox::new();
let cursor_event = r#"{"tool_name":"Bash","tool_input":{"command":"ls"},"tool_output":"{\"exit_code\":0}","conversation_id":"conv-abc","cwd":"/x"}"#;
let id = sb.record("cursor task", &[cursor_event]);
assert_eq!(sb.span_lines(&id), 1);
let span = std::fs::read_to_string(sb.home().join(".galdr/spans").join(format!("{id}.jsonl")))
.unwrap();
assert!(
span.contains(r#""exit_code":0"#),
"tool_output is parsed into tool_response: {span}"
);
assert!(
span.contains("conv-abc"),
"conversation_id maps to session_id: {span}"
);
}
#[test]
fn distilled_skill_is_linked_into_installed_harnesses() {
let sb = Sandbox::new();
std::fs::create_dir_all(sb.home().join(".claude/skills")).unwrap();
let id = sb.record("link task", &[BASH_STATUS]);
let refined = sb.home().join("r.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-link-task\ndescription: \"link\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
assert!(
sb.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap()
.status
.success()
);
let linked = sb.home().join(".claude/skills/galdr-link-task/SKILL.md");
assert!(
linked.exists(),
"the distilled skill must be discoverable in ~/.claude/skills"
);
let canonical = sb.home().join(".agents/skills/galdr-link-task/SKILL.md");
assert!(canonical.exists());
}
#[test]
fn link_never_clobbers_a_real_skill_already_in_the_harness() {
let sb = Sandbox::new();
let existing = sb.home().join(".claude/skills/galdr-keepme");
std::fs::create_dir_all(&existing).unwrap();
std::fs::write(existing.join("SKILL.md"), "real user content").unwrap();
let id = sb.record("keepme", &[BASH_STATUS]);
let refined = sb.home().join("r.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-keepme\ndescription: \"x\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
assert!(
sb.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap()
.status
.success()
);
let content =
std::fs::read_to_string(sb.home().join(".claude/skills/galdr-keepme/SKILL.md")).unwrap();
assert_eq!(content, "real user content");
assert!(
!sb.home()
.join(".claude/skills/galdr-keepme")
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink()
);
let out = sb.run(&["link", "--skill", "galdr-keepme", "--json"]);
let results: serde_json::Value = serde_json::from_str(&stdout(&out)).unwrap();
assert!(
results
.as_array()
.unwrap()
.iter()
.any(|r| r["harness"] == "Claude Code" && r["status"] == "conflict")
);
}
#[test]
fn link_rejects_path_traversal_in_skill_name() {
let sb = Sandbox::new();
let out = sb.run(&["link", "--skill", "../evil"]);
assert!(
!out.status.success(),
"path-traversal skill name must be rejected"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("path separator") || err.contains("invalid skill name"),
"{err}"
);
assert!(!sb.home().join(".claude/evil").exists());
}
#[test]
fn rm_retires_a_skill_unlinking_and_moving_it() {
let sb = Sandbox::new();
std::fs::create_dir_all(sb.home().join(".claude/skills")).unwrap();
let id = sb.record("retire me", &[BASH_STATUS]);
assert!(sb.run(&["distill", &id, "--fast"]).status.success());
let link = sb.home().join(".claude/skills/galdr-retire-me");
assert!(link.exists(), "the skill should be linked into the harness");
assert!(stdout(&sb.run(&["skills"])).contains("galdr-retire-me"));
let rm = sb.run(&["rm", "galdr-retire-me", "--force"]);
assert!(rm.status.success(), "{}", stderr(&rm));
let said = stdout(&rm);
assert!(
said.contains("Unlinked"),
"reports what it unlinked: {said}"
);
assert!(said.contains("retired 'galdr-retire-me'"), "{said}");
assert!(
sb.home()
.join(".claude/skills/galdr-retire-me")
.symlink_metadata()
.is_err(),
"the harness symlink must be removed"
);
assert!(!sb.home().join(".agents/skills/galdr-retire-me").exists());
assert!(
sb.home()
.join(".agents/skills/.retired/galdr-retire-me/SKILL.md")
.exists(),
"the skill should be moved to .retired, not deleted"
);
let listing = stdout(&sb.run(&["skills"]));
assert!(
!listing.contains("galdr-retire-me"),
"a retired skill must drop out of the catalog: {listing}"
);
}
#[test]
fn rm_of_a_missing_skill_fails_clearly() {
let sb = Sandbox::new();
let out = sb.run(&["rm", "does-not-exist", "--force"]);
assert!(!out.status.success(), "removing a missing skill must fail");
assert!(stderr(&out).contains("not installed"), "{}", stderr(&out));
}
#[test]
fn rm_without_force_refuses_in_a_non_interactive_context() {
let sb = Sandbox::new();
let id = sb.record("keep unless forced", &[BASH_STATUS]);
assert!(sb.run(&["distill", &id, "--fast"]).status.success());
let out = sb.run(&["rm", "galdr-keep-unless-forced"]);
assert!(!out.status.success());
assert!(stderr(&out).contains("--force"), "{}", stderr(&out));
assert!(
sb.home()
.join(".agents/skills/galdr-keep-unless-forced/SKILL.md")
.exists(),
"a refused retire must leave the skill in place"
);
}
#[test]
fn rm_suffixes_a_retired_name_that_already_exists() {
let sb = Sandbox::new();
let first = sb.record("twice", &[BASH_STATUS]);
assert!(sb.run(&["distill", &first, "--fast"]).status.success());
assert!(sb.run(&["rm", "galdr-twice", "--force"]).status.success());
let second = sb.record("twice", &[BASH_STATUS]);
assert!(sb.run(&["distill", &second, "--fast"]).status.success());
assert!(sb.run(&["rm", "galdr-twice", "--force"]).status.success());
assert!(
sb.home()
.join(".agents/skills/.retired/galdr-twice/SKILL.md")
.exists()
);
assert!(
sb.home()
.join(".agents/skills/.retired/galdr-twice.1/SKILL.md")
.exists(),
"the second retirement of the same name must be suffixed"
);
}
#[test]
fn export_redact_scrubs_secrets_from_every_file_not_just_raw() {
let sb = Sandbox::new();
let id = sb.record(
"leaky",
&[r#"{"tool_name":"Bash","tool_input":{"command":"curl -H 'Authorization: Bearer ghp_SECRETtoken123' https://api"},"tool_response":{}}"#],
);
let out = sb.home().join("exp");
assert!(
sb.cmd()
.args(["export", &id, "--out"])
.arg(&out)
.arg("--redact")
.output()
.unwrap()
.status
.success()
);
for file in ["steps.md", "raw.redacted.jsonl"] {
let content = std::fs::read_to_string(out.join(file)).unwrap();
assert!(
!content.contains("ghp_SECRETtoken123"),
"{file} still leaks the secret:\n{content}"
);
}
assert!(
std::fs::read_to_string(out.join("steps.md"))
.unwrap()
.contains("[REDACTED]")
);
}
#[test]
fn galdr_root_is_locked_to_the_owner() {
let sb = Sandbox::new();
sb.record("private", &[BASH_STATUS]);
let meta = std::fs::metadata(sb.home().join(".galdr")).unwrap();
use std::os::unix::fs::PermissionsExt;
assert_eq!(
meta.permissions().mode() & 0o077,
0,
"~/.galdr must be 0700 (no group/other access)"
);
}
#[test]
fn hook_survives_an_oversized_payload() {
let sb = Sandbox::new();
assert!(sb.run(&["rec", "start", "big"]).status.success());
let id = sb.active_rec_id();
let huge = format!(
r#"{{"tool_name":"Bash","tool_input":{{"command":"{}"}},"tool_response":{{}}}}"#,
"A".repeat(2_000_000)
);
let out = sb.hook(&huge, false);
assert!(out.status.success(), "the sensor must always exit 0");
assert!(sb.span_lines(&id) <= 1);
}
#[test]
fn computer_use_screenshots_are_dropped_but_actions_recorded() {
let sb = Sandbox::new();
assert!(sb.run(&["rec", "start", "gui task"]).status.success());
let id = sb.active_rec_id();
let blob = "iVBORw0KGgoAAAANSUhEUg".repeat(80);
let shot = format!(
r#"{{"tool_name":"mcp__computer-use__computer","tool_input":{{"action":"screenshot"}},"tool_response":{{"type":"image","source":{{"type":"base64","media_type":"image/png","data":"{blob}"}}}},"session_id":"s1"}}"#
);
assert!(sb.hook(&shot, false).status.success());
assert!(
sb.hook(
r#"{"tool_name":"mcp__computer-use__computer","tool_input":{"action":"type","text":"42.50"},"tool_response":{},"session_id":"s1"}"#,
false,
)
.status
.success()
);
assert!(sb.run(&["rec", "stop"]).status.success());
let span = std::fs::read_to_string(sb.home().join(".galdr/spans").join(format!("{id}.jsonl")))
.unwrap();
assert!(
!span.contains("iVBORw0KGgo"),
"the screenshot base64 must be dropped"
);
assert!(span.contains("stripped screenshot"));
let show = stdout(&sb.run(&["show", &id]));
assert!(show.contains("screenshot"));
assert!(show.contains("type \"42.50\""), "got: {show}");
}
#[test]
fn a_typed_secret_is_redacted_from_the_distilled_skill() {
let sb = Sandbox::new();
let id = sb.record(
"login flow",
&[
r#"{"tool_name":"mcp__computer-use__computer","tool_input":{"action":"type","text":"ghp_SUPERSECRETtoken123"},"tool_response":{}}"#,
],
);
assert!(sb.run(&["distill", &id]).status.success());
let skill = sb.skill_md("galdr-login-flow");
assert!(
!skill.contains("ghp_SUPERSECRETtoken123"),
"secret leaked into skill:\n{skill}"
);
assert!(skill.contains("[REDACTED]"));
}
#[test]
fn sensor_never_breaks_the_session() {
let sb = Sandbox::new();
assert!(sb.hook(BASH_STATUS, false).status.success());
assert!(sb.run(&["rec", "start", "demo"]).status.success());
let id = sb.active_rec_id();
assert!(sb.hook(BASH_STATUS, false).status.success());
assert_eq!(sb.span_lines(&id), 1);
let failed = sb.hook(BASH_STATUS, true);
assert!(failed.status.success(), "the sensor must always exit 0");
assert_eq!(sb.span_lines(&id), 1, "a failed hook must not append");
}
#[test]
fn recording_scopes_to_the_session_that_started_it() {
let sb = Sandbox::new();
assert!(sb.run(&["rec", "start", "scoped"]).status.success());
let id = sb.active_rec_id();
assert!(
sb.hook(
r#"{"tool_name":"Bash","tool_input":{"command":"mine-1"},"tool_response":{},"session_id":"mine"}"#,
false,
)
.status
.success()
);
assert!(
sb.hook(
r#"{"tool_name":"Bash","tool_input":{"command":"leak"},"tool_response":{},"session_id":"other","cwd":"/elsewhere"}"#,
false,
)
.status
.success()
);
assert!(
sb.hook(
r#"{"tool_name":"Read","tool_input":{"file_path":"/x"},"tool_response":{},"session_id":"mine"}"#,
false,
)
.status
.success()
);
assert_eq!(
sb.span_lines(&id),
2,
"only the bound session's events record"
);
let span = std::fs::read_to_string(sb.home().join(".galdr/spans").join(format!("{id}.jsonl")))
.unwrap();
assert!(
!span.contains("leak"),
"the foreign session's command must not leak in: {span}"
);
assert!(!span.contains("\"other\""));
}
#[test]
fn record_list_show_work_without_a_daemon() {
let sb = Sandbox::new();
let id = sb.record(
"demo task",
&[
BASH_STATUS,
r#"{"tool_name":"Write","tool_input":{"file_path":"/tmp/out.md"},"tool_response":{}}"#,
],
);
let list = sb.run(&["list"]);
assert!(list.status.success());
let listing = stdout(&list);
assert!(
listing.contains("demo task"),
"list shows the name: {listing}"
);
assert!(listing.contains("2 steps"), "list shows the step count");
let show = sb.run(&["show", &id]);
assert!(show.status.success());
let detail = stdout(&show);
assert!(detail.contains("Bash"));
assert!(detail.contains("Write"));
assert!(detail.contains("git status"));
}
#[test]
fn distill_from_installs_and_skills_lists_provenance() {
let sb = Sandbox::new();
let id = sb.record("demo", &[BASH_STATUS]);
let refined = sb.home().join("refined.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-demo\ndescription: \"does a thing\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let install = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap();
assert!(install.status.success());
let skills = sb.run(&["skills"]);
assert!(skills.status.success());
let listing = stdout(&skills);
assert!(
listing.contains("galdr-demo"),
"skills lists the skill: {listing}"
);
assert!(listing.contains(&id));
assert!(!listing.contains("orphan"));
}
#[test]
fn reindex_rebuilds_the_catalog_from_disk() {
let sb = Sandbox::new();
sb.record("demo", &[BASH_STATUS]);
let reindex = sb.run(&["reindex"]);
assert!(reindex.status.success());
assert!(stdout(&reindex).contains("catalog rebuilt"));
assert!(sb.home().join(".galdr/catalog.sqlite").exists());
let list = sb.run(&["list"]);
assert!(list.status.success());
assert!(stdout(&list).contains("demo"));
}
#[test]
fn human_events_reindex_show_distill_and_export() {
let sb = Sandbox::new();
write_human_recording(
&sb,
"01HUMAN",
"human issue form",
serde_json::json!({
"policy": "redacted",
"kind": "text",
"chars": 24
}),
);
let reindex = sb.run(&["reindex"]);
assert!(reindex.status.success(), "{}", stderr(&reindex));
let show = sb.run(&["show", "01HUMAN", "--json"]);
assert!(show.status.success(), "{}", stderr(&show));
let detail: serde_json::Value = serde_json::from_str(&stdout(&show)).unwrap();
assert_eq!(detail["steps"][0]["event_kind"], "human");
assert_eq!(
detail["steps"][0]["summary"],
"type into \"Issue title\" (text, 24 chars)"
);
let distill = sb.run(&["distill", "01HUMAN", "--fast"]);
assert!(distill.status.success(), "{}", stderr(&distill));
let skill = sb.skill_md("galdr-human-issue-form");
assert!(skill.contains("browser workflow"), "{skill}");
assert!(skill.contains("type into \"Issue title\""), "{skill}");
assert!(skill.contains("Confirm the issue was saved."), "{skill}");
write_human_recording(
&sb,
"01HUMANLIT",
"human literal",
serde_json::json!({
"policy": "literal",
"value": "Visible customer escalation"
}),
);
let literal_distill = sb.run(&["distill", "01HUMANLIT", "--fast"]);
assert!(
literal_distill.status.success(),
"{}",
stderr(&literal_distill)
);
let literal_skill = sb.skill_md("galdr-human-literal");
assert!(
!literal_skill.contains("Visible customer escalation"),
"literal typed text leaked into skill:\n{literal_skill}"
);
assert!(
literal_skill
.contains("- `<Issue title>` — text for Issue title at step 1 (27 chars recorded)"),
"{literal_skill}"
);
let out = sb.home().join("human-export");
let export = sb
.cmd()
.args(["export", "01HUMANLIT", "--out"])
.arg(&out)
.arg("--redact")
.output()
.unwrap();
assert!(export.status.success(), "{}", stderr(&export));
let raw = std::fs::read_to_string(out.join("raw.redacted.jsonl")).unwrap();
assert!(!raw.contains("Visible customer escalation"), "{raw}");
assert!(raw.contains(r#""policy":"redacted""#), "{raw}");
assert!(raw.contains(r#""kind":"literal""#), "{raw}");
}
#[test]
fn observe_synthetic_records_a_human_trace() {
let sb = Sandbox::new();
let observed = sb.run(&[
"observe",
"synthetic",
"synthetic human form",
"--fixture",
"browser-form",
]);
assert!(observed.status.success(), "{}", stderr(&observed));
let said = stdout(&observed);
assert!(said.contains("observed \"synthetic human form\""), "{said}");
let list = stdout(&sb.run(&["list"]));
assert!(list.contains("synthetic human form"), "{list}");
assert!(list.contains("4 steps"), "{list}");
let show = sb.run(&["show", "synthetic human form", "--json"]);
assert!(show.status.success(), "{}", stderr(&show));
let detail: serde_json::Value = serde_json::from_str(&stdout(&show)).unwrap();
assert_eq!(detail["recording"]["name"], "synthetic human form");
assert_eq!(detail["steps"].as_array().unwrap().len(), 4);
assert!(
detail["steps"]
.as_array()
.unwrap()
.iter()
.all(|step| step["event_kind"] == "human")
);
assert_eq!(
detail["steps"][0]["summary"],
"navigate https://example.test/issues/new"
);
assert_eq!(
detail["steps"][1]["summary"],
"type into \"Issue title\" (text, 24 chars)"
);
assert_eq!(
detail["steps"][2]["summary"],
"select \"Priority\" = \"High\""
);
assert_eq!(
detail["steps"][3]["summary"],
"click button \"Create issue\""
);
let distill = sb.run(&["distill", "synthetic human form", "--fast"]);
assert!(distill.status.success(), "{}", stderr(&distill));
let skill = sb.skill_md("galdr-synthetic-human-form");
assert!(skill.contains("browser workflow"), "{skill}");
assert!(
skill.contains("navigate https://example.test/issues/new"),
"{skill}"
);
assert!(skill.contains("select \"Priority\" = \"High\""), "{skill}");
assert!(
skill.contains("Confirm the created issue page is open or a success message appears."),
"{skill}"
);
}
#[test]
fn observe_browser_collector_records_loopback_events() {
let sb = Sandbox::new();
let start = sb.run(&[
"observe",
"browser",
"start",
"browser collector",
"--url",
"https://example.test/form",
"--no-open",
]);
assert!(start.status.success(), "{}", stderr(&start));
let port = active_browser_port(&sb);
post_browser_event(
port,
serde_json::json!({
"ts": "2026-06-30T00:00:00Z",
"action": "human.browser.navigate",
"source": {
"kind": "browser",
"url": "https://example.test/form",
"title": "Demo form"
}
}),
);
post_browser_event(
port,
serde_json::json!({
"ts": "2026-06-30T00:00:01Z",
"action": "human.browser.input",
"source": {
"kind": "browser",
"url": "https://example.test/form",
"title": "Demo form"
},
"target": {
"primary": {
"kind": "label",
"value": "Email"
},
"label": "Email"
},
"value": {
"policy": "redacted",
"kind": "text",
"chars": 16
}
}),
);
let status = stdout(&sb.run(&["observe", "browser", "status"]));
assert!(status.contains("events: 2"), "{status}");
assert!(status.contains("server: up"), "{status}");
let stop = sb.run(&["observe", "browser", "stop"]);
assert!(stop.status.success(), "{}", stderr(&stop));
let said = stdout(&stop);
assert!(said.contains("stopped browser observation"), "{said}");
assert!(said.contains("2 human steps"), "{said}");
let show = sb.run(&["show", "browser collector", "--json"]);
assert!(show.status.success(), "{}", stderr(&show));
let detail: serde_json::Value = serde_json::from_str(&stdout(&show)).unwrap();
assert_eq!(detail["steps"].as_array().unwrap().len(), 2);
assert_eq!(detail["steps"][0]["event_kind"], "human");
assert_eq!(
detail["steps"][0]["summary"],
"navigate https://example.test/form"
);
assert_eq!(
detail["steps"][1]["summary"],
"type into \"Email\" (text, 16 chars)"
);
}
#[test]
fn recording_writes_keep_an_existing_catalog_current_without_a_daemon() {
let sb = Sandbox::new();
sb.record("first", &[BASH_STATUS]);
assert!(sb.run(&["reindex"]).status.success());
let second = sb.record(
"second",
&[r#"{"tool_name":"Read","tool_input":{"file_path":"/tmp/input.md"},"tool_response":{}}"#],
);
let list = sb.run(&["list"]);
assert!(list.status.success());
let listing = stdout(&list);
assert!(
listing.contains("second"),
"list should not read a stale catalog: {listing}"
);
let show = sb.run(&["show", &second]);
assert!(show.status.success());
let detail = stdout(&show);
assert!(
detail.contains("/tmp/input.md"),
"show should include the newly indexed step: {detail}"
);
}
#[test]
fn skill_writes_keep_an_existing_catalog_current_without_a_daemon() {
let sb = Sandbox::new();
let id = sb.record("stale catalog", &[BASH_STATUS]);
assert!(sb.run(&["reindex"]).status.success());
let refined = sb.home().join("refined.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-stale-catalog\ndescription: \"stale catalog check\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let install = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap();
assert!(install.status.success());
let skills = sb.run(&["skills"]);
assert!(skills.status.success());
let listing = stdout(&skills);
assert!(
listing.contains("galdr-stale-catalog"),
"skills should not read a stale catalog: {listing}"
);
assert!(listing.contains(&id));
}
#[test]
fn draft_distill_keeps_an_existing_catalog_current_without_a_daemon() {
let sb = Sandbox::new();
let id = sb.record("draft catalog", &[BASH_STATUS]);
assert!(sb.run(&["reindex"]).status.success());
let draft = sb.run(&["distill", &id]);
assert!(draft.status.success());
let skills = sb.run(&["skills"]);
assert!(skills.status.success());
let listing = stdout(&skills);
assert!(
listing.contains("galdr-draft-catalog"),
"draft distillation should update an existing catalog: {listing}"
);
assert!(listing.contains(&id));
}
#[test]
fn parametrize_emit_keeps_an_existing_catalog_current_without_a_daemon() {
let sb = Sandbox::new();
let write = |path: &str| {
format!(
r#"{{"tool_name":"Write","tool_input":{{"file_path":"{path}"}},"tool_response":{{}}}}"#
)
};
let a = sb.record("ship", &[BASH_STATUS, &write("/repo-a/out.md")]);
let b = sb.record("ship", &[BASH_STATUS, &write("/repo-b/out.md")]);
assert!(sb.run(&["reindex"]).status.success());
let emit = sb.run(&["parametrize", &a, &b, "--emit"]);
assert!(emit.status.success());
let skills = sb.run(&["skills"]);
assert!(skills.status.success());
let listing = stdout(&skills);
assert!(
listing.contains("galdr-ship-param"),
"parametrize should update an existing catalog: {listing}"
);
assert!(listing.contains(&a));
}
#[test]
fn parametrize_emits_a_templated_skill() {
let sb = Sandbox::new();
let write = |path: &str| {
format!(
r#"{{"tool_name":"Write","tool_input":{{"file_path":"{path}"}},"tool_response":{{}}}}"#
)
};
let a = sb.record("ship", &[BASH_STATUS, &write("/repo-a/out.md")]);
let b = sb.record("ship", &[BASH_STATUS, &write("/repo-b/out.md")]);
let emit = sb.run(&["parametrize", &a, &b, "--emit"]);
assert!(
emit.status.success(),
"{}",
String::from_utf8_lossy(&emit.stderr)
);
let skill = sb.skill_md("galdr-ship-param");
assert!(skill.contains("## Parameters"));
assert!(skill.contains("## Procedure (parametrized)"));
assert!(skill.contains("{{OUT}}"), "the output path is templated");
assert!(
!skill.contains("LOW-CONFIDENCE"),
"a clean alignment is high confidence"
);
}
#[test]
fn parametrize_marks_divergent_recordings_low_confidence() {
let sb = Sandbox::new();
let a = sb.record(
"task",
&[
BASH_STATUS,
r#"{"tool_name":"Read","tool_input":{"file_path":"/a.rs"},"tool_response":{}}"#,
],
);
let b = sb.record(
"task",
&[r#"{"tool_name":"Glob","tool_input":{"pattern":"*.rs"},"tool_response":{}}"#],
);
assert!(sb.run(&["parametrize", &a, &b, "--emit"]).status.success());
let skill = sb.skill_md("galdr-task-param");
assert!(skill.contains("LOW-CONFIDENCE"));
assert!(skill.contains("## Alignment notes"));
}
#[test]
fn distill_auto_falls_back_to_a_complete_skill_without_an_engine() {
let sb = Sandbox::new();
let id = sb.record("auto demo", &[BASH_STATUS]);
let auto = sb.run(&["distill", &id, "--auto"]);
assert!(
auto.status.success(),
"--auto must exit 0 even with no engine"
);
let skill = sb.skill_md("galdr-auto-demo");
assert!(skill.contains("galdr-auto-demo"));
assert!(skill.contains("## When to use"));
assert!(skill.contains("## Verification"));
assert!(!skill.contains("[galdr DRAFT]"));
}
#[test]
fn diff_reports_constants_and_parameters() {
let sb = Sandbox::new();
let write = |path: &str| {
format!(
r#"{{"tool_name":"Write","tool_input":{{"file_path":"{path}"}},"tool_response":{{}}}}"#
)
};
let a = sb.record("ship", &[BASH_STATUS, &write("/repo-a/out.md")]);
let b = sb.record("ship", &[BASH_STATUS, &write("/repo-b/out.md")]);
let diff = sb.run(&["diff", &a, &b]);
assert!(diff.status.success());
let report = stdout(&diff);
assert!(report.contains("confidence: HIGH"));
assert!(report.contains("OUT"), "the output path is a parameter");
assert!(report.contains("Constants:"));
}
#[test]
fn suggest_surfaces_repeated_tasks_and_dedupes_distilled_ones() {
let sb = Sandbox::new();
let write = |path: &str| {
format!(
r#"{{"tool_name":"Write","tool_input":{{"file_path":"{path}"}},"tool_response":{{}}}}"#
)
};
let a = sb.record("ship", &[BASH_STATUS, &write("/repo-a/out.md")]);
let _b = sb.record("ship", &[BASH_STATUS, &write("/repo-b/out.md")]);
let _c = sb.record("oneoff", &[BASH_STATUS]);
let parse = |args: &[&str]| -> serde_json::Value {
let out = sb.run(args);
assert!(out.status.success(), "{args:?} failed");
serde_json::from_str(&stdout(&out))
.unwrap_or_else(|e| panic!("{args:?} did not emit valid JSON: {e}"))
};
let opps = parse(&["suggest", "--json"]);
let arr = opps.as_array().expect("an array of opportunities");
assert_eq!(arr.len(), 1, "only the repeated shape is an opportunity");
assert_eq!(arr[0]["count"], 2);
let recs = arr[0]["recordings"].as_array().unwrap();
assert_eq!(recs.len(), 2);
assert!(sb.run(&["distill", &a]).status.success());
let after = parse(&["suggest", "--json"]);
assert!(
after.as_array().unwrap().is_empty(),
"an installed skill dedupes its shape out of the opportunities"
);
}
#[test]
fn bare_galdr_shows_a_friendly_overview() {
let sb = Sandbox::new();
sb.record("demo-task", &[BASH_STATUS]);
let out = sb.run(&[]);
assert!(out.status.success(), "bare `galdr` must exit 0");
let text = stdout(&out);
assert!(text.contains("galdr"), "{text}");
assert!(text.contains("next"), "shows next steps: {text}");
assert!(text.contains("galdr rec start"), "{text}");
assert!(
text.contains("1 recordings"),
"reflects the catalog: {text}"
);
assert!(
!text.contains('\u{1b}'),
"no ANSI escapes when piped: {text:?}"
);
}
#[test]
fn recording_references_resolve_without_a_ulid() {
let sb = Sandbox::new();
let older = sb.record("weekly-report", &[BASH_STATUS]);
let newest = sb.record("ship-preview", &[BASH_STATUS]);
assert!(sb.run(&["distill"]).status.success());
let skill = sb.skill_md("galdr-ship-preview");
assert!(
skill.contains(&newest),
"the newest recording was distilled"
);
let shown = sb.run(&["show", "weekly-report", "--json"]);
assert!(shown.status.success());
let detail: serde_json::Value = serde_json::from_str(&stdout(&shown)).unwrap();
assert_eq!(detail["recording"]["rec_id"], older.as_str());
assert!(sb.run(&["show", &newest]).status.success());
let miss = sb.run(&["show", "does-not-exist"]);
assert!(!miss.status.success());
assert!(stderr(&miss).contains("galdr list"));
}
#[test]
fn suggest_and_bench_render_human_reports() {
let sb = Sandbox::new();
assert!(sb.run(&["suggest"]).status.success());
assert!(stdout(&sb.run(&["suggest"])).contains("No repeated"));
assert!(stdout(&sb.run(&["bench"])).contains("No replay outcomes"));
let write = |path: &str| {
format!(
r#"{{"tool_name":"Write","tool_input":{{"file_path":"{path}"}},"tool_response":{{}}}}"#
)
};
let id = sb.record("ship", &[BASH_STATUS, &write("/a/out.md")]);
sb.record("ship", &[BASH_STATUS, &write("/b/out.md")]);
let suggest = stdout(&sb.run(&["suggest"]));
assert!(suggest.contains("Skill opportunities"), "{suggest}");
assert!(suggest.contains("galdr distill"), "{suggest}");
let refined = sb.home().join("s.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-ship\ndescription: \"ship\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
assert!(
sb.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap()
.status
.success()
);
assert!(
sb.run(&[
"outcome",
"usage",
"--skill",
"galdr-ship",
"--rec",
&id,
"--outcome",
"success",
])
.status
.success()
);
let bench = stdout(&sb.run(&["bench"]));
assert!(bench.contains("Replay reliability"), "{bench}");
assert!(bench.contains("galdr-ship"), "{bench}");
}
#[test]
fn rec_control_rejects_double_start_and_orphan_stop() {
let sb = Sandbox::new();
let stop = sb.run(&["rec", "stop"]);
assert!(!stop.status.success());
assert!(stderr(&stop).contains("no active recording"));
let status = sb.run(&["rec", "status"]);
assert!(status.status.success());
assert!(stdout(&status).contains("no active recording"));
assert!(sb.run(&["rec", "start", "one"]).status.success());
assert!(sb.run(&["rec", "start", "two"]).status.success());
let both = stdout(&sb.run(&["rec", "status"]));
assert!(both.contains("2 active recordings"), "{both}");
assert!(both.contains("one") && both.contains("two"), "{both}");
let ambiguous = sb.run(&["rec", "stop"]);
assert!(!ambiguous.status.success());
assert!(
stderr(&ambiguous).contains("specify which"),
"{}",
stderr(&ambiguous)
);
assert!(sb.run(&["rec", "stop", "one"]).status.success());
let after = stdout(&sb.run(&["rec", "status"]));
assert!(after.contains("1 active recording"), "{after}");
assert!(after.contains("two"), "{after}");
assert!(sb.run(&["rec", "stop"]).status.success());
assert!(!sb.run(&["rec", "stop"]).status.success());
}
#[test]
fn a_corrupt_active_flag_is_treated_as_not_recording() {
let sb = Sandbox::new();
assert!(sb.run(&["rec", "start", "seed"]).status.success());
assert!(sb.run(&["rec", "stop"]).status.success());
std::fs::write(sb.home().join(".galdr/active"), "}{ not json").unwrap();
let status = sb.run(&["rec", "status"]);
assert!(status.status.success());
assert!(stdout(&status).contains("no active recording"));
assert!(sb.run(&["rec", "start", "fresh"]).status.success());
}
#[test]
fn list_is_empty_on_a_fresh_install() {
let sb = Sandbox::new();
let out = sb.run(&["list"]);
assert!(out.status.success());
assert!(stdout(&out).contains("no recordings yet"));
}
#[test]
fn bench_reports_replay_hit_rate_from_recorded_outcomes() {
let sb = Sandbox::new();
let id = sb.record("bench", &[BASH_STATUS]);
let refined = sb.home().join("bench.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-bench\ndescription: \"bench task\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
assert!(
sb.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap()
.status
.success()
);
let record_outcome = |outcome: &str, retries: &str| {
sb.run(&[
"outcome",
"usage",
"--skill",
"galdr-bench",
"--rec",
&id,
"--outcome",
outcome,
"--retries",
retries,
])
};
assert!(record_outcome("success", "0").status.success());
assert!(record_outcome("failed", "1").status.success());
let out = sb.run(&["bench", "--json"]);
assert!(out.status.success());
let report: serde_json::Value = serde_json::from_str(&stdout(&out)).unwrap();
assert_eq!(report["total_replays"], 2);
assert_eq!(report["overall_success_rate"], 0.5);
let skill = report["skills"]
.as_array()
.unwrap()
.iter()
.find(|s| s["skill_name"] == "galdr-bench")
.expect("galdr-bench in the report");
assert_eq!(skill["uses"], 2);
assert_eq!(skill["success"], 1);
assert_eq!(skill["failed"], 1);
assert_eq!(skill["success_rate"], 0.5);
assert_eq!(skill["avg_retries"], 0.5);
}
#[test]
fn rec_status_and_capture_policy_work() {
let sb = Sandbox::new();
assert!(stdout(&sb.run(&["rec", "status"])).contains("no active recording"));
std::fs::create_dir_all(sb.home().join(".galdr")).unwrap();
std::fs::write(
sb.home().join(".galdr/config.json"),
r#"{"capture":{"deny_tools":["Secret"],"deny_cwd_prefixes":["/private"],"max_response_chars":12}}"#,
)
.unwrap();
assert!(sb.run(&["rec", "start", "capture"]).status.success());
let id = sb.active_rec_id();
assert!(
sb.hook(
r#"{"tool_name":"Secret","tool_input":{"value":"x"},"tool_response":{"token":"abc"}}"#,
false,
)
.status
.success()
);
assert_eq!(sb.span_lines(&id), 0, "denied tools are not recorded");
assert!(
sb.hook(
r#"{"tool_name":"Bash","tool_input":{"command":"echo hi"},"tool_response":{"stdout":"abcdefghijklmnopqrstuvwxyz"},"cwd":"/tmp"}"#,
false,
)
.status
.success()
);
assert_eq!(sb.span_lines(&id), 1);
let span = std::fs::read_to_string(sb.home().join(".galdr/spans").join(format!("{id}.jsonl")))
.unwrap();
assert!(span.contains("galdr_truncated"));
let status = stdout(&sb.run(&["rec", "status"]));
assert!(status.contains("1 active recording"), "{status}");
assert!(status.contains("capture"), "{status}");
assert!(status.contains("steps: 1"), "{status}");
}
#[test]
fn skills_catalog_reports_status_readiness_and_delta() {
let sb = Sandbox::new();
let id = sb.record("readiness", &[BASH_STATUS]);
assert!(sb.run(&["distill", &id]).status.success());
let draft_listing = stdout(&sb.run(&["skills"]));
assert!(draft_listing.contains("galdr-readiness"));
assert!(draft_listing.contains("draft"));
assert!(draft_listing.contains("readiness"));
let refined = sb.home().join("refined.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-readiness\ndescription: \"readiness check\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let install = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap();
assert!(install.status.success());
let final_listing = stdout(&sb.run(&["skills"]));
assert!(final_listing.contains("final"));
assert!(
final_listing.contains("(+"),
"readiness delta should show the final skill improved: {final_listing}"
);
let evaluations = stdout(&sb.run(&["evaluations", "--skill", "galdr-readiness"]));
assert!(evaluations.contains("readiness_lint"));
assert!(evaluations.contains("galdr-readiness"));
}
#[test]
fn outcome_usage_and_labels_survive_reindex() {
let sb = Sandbox::new();
let id = sb.record("outcome", &[BASH_STATUS]);
let refined = sb.home().join("outcome.md");
std::fs::write(
&refined,
format!(
"---\nname: galdr-outcome\ndescription: \"outcome capture\"\n---\n\n## Provenance\n- rec_id: `{id}`\n\n## Goal\nx\n## Procedure\ny\n## Success criteria\nz\n"
),
)
.unwrap();
let install = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&refined)
.output()
.unwrap();
assert!(install.status.success());
let usage = sb.run(&[
"outcome",
"usage",
"--skill",
"galdr-outcome",
"--rec",
&id,
"--task-kind",
"smoke",
"--outcome",
"success",
"--retries",
"1",
"--manual-interventions",
"2",
"--notes",
"worked after one retry",
]);
assert!(usage.status.success());
assert!(stdout(&usage).contains("usage recorded"));
let label = sb.run(&[
"outcome",
"label",
"--skill",
"galdr-outcome",
"--rec",
&id,
"--evaluator",
"human",
"--label",
"accepted",
"--confidence",
"0.9",
"--notes",
"reviewed",
]);
assert!(label.status.success());
assert!(stdout(&label).contains("outcome recorded"));
let usage_log = sb.home().join(".galdr/outcomes/skill_usage.jsonl");
let outcome_log = sb.home().join(".galdr/outcomes/skill_outcomes.jsonl");
assert!(
std::fs::read_to_string(usage_log)
.unwrap()
.contains("success")
);
assert!(
std::fs::read_to_string(outcome_log)
.unwrap()
.contains("accepted")
);
assert!(sb.run(&["reindex"]).status.success());
let listing = stdout(&sb.run(&["outcome", "list", "--skill", "galdr-outcome"]));
assert!(listing.contains("success"));
assert!(listing.contains("accepted"));
assert!(listing.contains("interventions 2"));
}
#[test]
fn distill_from_rejects_unfinished_skills() {
let sb = Sandbox::new();
let id = sb.record("unfinished", &[BASH_STATUS]);
let bad = sb.home().join("bad.md");
std::fs::write(
&bad,
"---\nname: galdr-unfinished\ndescription: \"bad\"\n---\n\n## Goal\nx\n## Procedure\ny\n",
)
.unwrap();
let install = sb
.cmd()
.args(["distill", &id, "--from"])
.arg(&bad)
.output()
.unwrap();
assert!(!install.status.success());
assert!(String::from_utf8_lossy(&install.stderr).contains("Success criteria"));
}
#[test]
fn setup_claude_check_and_print_work_without_mutating_settings() {
let sb = Sandbox::new();
let missing = stdout(&sb.run(&["setup", "claude", "--check"]));
assert!(missing.contains("settings not found"));
let snippet = stdout(&sb.run(&["setup", "claude", "--print"]));
assert!(snippet.contains("PostToolUse"));
assert!(snippet.contains("galdr hook"));
let settings = sb.home().join(".claude/settings.json");
std::fs::create_dir_all(settings.parent().unwrap()).unwrap();
std::fs::write(&settings, snippet).unwrap();
let configured = stdout(&sb.run(&["setup", "claude", "--check"]));
assert!(configured.contains("is configured"));
}
#[test]
fn export_omits_raw_by_default_and_can_write_redacted_raw() {
let sb = Sandbox::new();
let id = sb.record(
"export",
&[r#"{"tool_name":"Bash","tool_input":{"command":"deploy","api_key":"secret-key"},"tool_response":{"token":"secret-token","ok":true}}"#],
);
let out = sb.home().join("export-default");
let export = sb
.cmd()
.args(["export", &id, "--out"])
.arg(&out)
.output()
.unwrap();
assert!(export.status.success());
assert!(out.join("recording.json").exists());
assert!(out.join("steps.md").exists());
assert!(out.join("skills.json").exists());
assert!(out.join("usage.json").exists());
assert!(out.join("outcomes.json").exists());
assert!(!out.join("raw.jsonl").exists());
let redacted = sb.home().join("export-redacted");
let export = sb
.cmd()
.args(["export", &id, "--out"])
.arg(&redacted)
.arg("--redact")
.output()
.unwrap();
assert!(export.status.success());
let raw = std::fs::read_to_string(redacted.join("raw.redacted.jsonl")).unwrap();
assert!(raw.contains("[REDACTED]"));
assert!(!raw.contains("secret-token"));
assert!(!raw.contains("secret-key"));
}
#[test]
fn doctor_passes_when_claude_hook_is_configured() {
let sb = Sandbox::new();
let settings = sb.home().join(".claude/settings.json");
std::fs::create_dir_all(settings.parent().unwrap()).unwrap();
std::fs::write(
&settings,
r#"{"hooks":{"PostToolUse":[{"hooks":[{"type":"command","command":"galdr hook"}]}]}}"#,
)
.unwrap();
let index = write_index_fixture(&sb, &[(env!("CARGO_PKG_VERSION"), false)]);
let doctor = sb
.cmd()
.env("GALDR_INDEX_FILE", &index)
.arg("doctor")
.output()
.unwrap();
assert!(
doctor.status.success(),
"{}\n{}",
stdout(&doctor),
String::from_utf8_lossy(&doctor.stderr)
);
let said = stdout(&doctor);
assert!(said.contains("doctor: ok"));
assert!(
said.contains(&format!("up to date (v{})", env!("CARGO_PKG_VERSION"))),
"doctor should surface the update check: {said}"
);
}
#[test]
fn upgrade_check_reports_up_to_date() {
let sb = Sandbox::new();
let index = write_index_fixture(
&sb,
&[("0.14.0", false), (env!("CARGO_PKG_VERSION"), false)],
);
let out = sb
.cmd()
.env("GALDR_INDEX_FILE", &index)
.args(["upgrade", "--check"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
assert!(stdout(&out).contains("is up to date"), "{}", stdout(&out));
}
#[test]
fn upgrade_check_flags_a_newer_version_with_exit_10() {
let sb = Sandbox::new();
let index = write_index_fixture(&sb, &[(env!("CARGO_PKG_VERSION"), false), ("9.9.9", false)]);
let out = sb
.cmd()
.env("GALDR_INDEX_FILE", &index)
.args(["upgrade", "--check"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(10), "{}", stderr(&out));
let said = stdout(&out);
assert!(said.contains("9.9.9"), "{said}");
assert!(said.contains("galdr upgrade"), "{said}");
}
#[test]
fn upgrade_check_ignores_a_yanked_newer_version() {
let sb = Sandbox::new();
let index = write_index_fixture(&sb, &[(env!("CARGO_PKG_VERSION"), false), ("9.9.9", true)]);
let out = sb
.cmd()
.env("GALDR_INDEX_FILE", &index)
.args(["upgrade", "--check"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
assert!(stdout(&out).contains("is up to date"), "{}", stdout(&out));
}
#[test]
fn upgrade_check_reports_local_ahead() {
let sb = Sandbox::new();
let index = write_index_fixture(&sb, &[("0.0.1", false)]);
let out = sb
.cmd()
.env("GALDR_INDEX_FILE", &index)
.args(["upgrade", "--check"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
let said = stdout(&out);
assert!(said.contains("ahead of crates.io"), "{said}");
assert!(said.contains("v0.0.1"), "{said}");
}
#[test]
fn upgrade_check_is_soft_when_offline() {
let sb = Sandbox::new();
let missing = sb.home().join("no-such-index.json");
let out = sb
.cmd()
.env("GALDR_INDEX_FILE", &missing)
.args(["upgrade", "--check"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0), "{}", stderr(&out));
assert!(stdout(&out).contains("offline"), "{}", stdout(&out));
}
#[test]
fn upgrade_rejects_an_invalid_from() {
let sb = Sandbox::new();
let out = sb
.cmd()
.env("GALDR_INDEX_FILE", sb.home().join("unused.json"))
.args(["upgrade", "--check", "--from", "bogus"])
.output()
.unwrap();
assert!(!out.status.success());
assert!(stderr(&out).contains("--from"), "{}", stderr(&out));
}
#[test]
fn daemon_indexes_and_answers_queries() {
let sb = Sandbox::new();
let pidfile = sb.home().join(".galdr/galdrd.pid");
let socket = sb.home().join(".galdr/galdrd.sock");
assert!(stdout(&sb.run(&["daemon", "status"])).contains("daemon stopped"));
assert!(sb.run(&["daemon", "--detach"]).status.success());
struct Guard(PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
if let Ok(pid) = std::fs::read_to_string(&self.0)
&& let Ok(pid) = pid.trim().parse::<i32>()
{
let _ = Command::new("kill")
.arg(pid.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
}
let _guard = Guard(pidfile.clone());
let mut ready = false;
for _ in 0..100 {
if socket.exists() {
ready = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert!(ready, "daemon socket never appeared");
let status = stdout(&sb.run(&["daemon", "status"]));
assert!(status.contains("daemon running"));
assert!(
status.contains(&format!("version: {}", env!("CARGO_PKG_VERSION"))),
"daemon status should print the version: {status}"
);
sb.record("daemon demo", &[BASH_STATUS]);
std::thread::sleep(std::time::Duration::from_millis(200));
let list = sb.run(&["list"]);
assert!(list.status.success());
assert!(
stdout(&list).contains("daemon demo"),
"the daemon-backed catalog should list the recording"
);
let stop = sb.run(&["daemon", "stop"]);
assert!(stop.status.success());
assert!(stdout(&stop).contains("daemon stopped"));
}
#[cfg(target_os = "macos")]
#[test]
fn daemon_install_writes_loads_and_uninstalls_the_launchagent() {
let sb = Sandbox::new();
let (launchctl, log) = fake_launchctl(&sb);
let install = sb
.cmd()
.env("GALDR_LAUNCHCTL", &launchctl)
.args(["daemon", "install"])
.output()
.unwrap();
assert!(install.status.success(), "{}", stderr(&install));
let plist = sb
.home()
.join("Library/LaunchAgents/dev.galdr.daemon.plist");
assert!(plist.exists(), "the LaunchAgent plist must be written");
let body = std::fs::read_to_string(&plist).unwrap();
assert!(body.contains("<string>dev.galdr.daemon</string>"), "{body}");
assert!(
body.contains(bin()),
"plist must point at this binary: {body}"
);
assert!(body.contains("<string>daemon</string>"), "{body}");
assert!(body.contains("<key>KeepAlive</key>"), "{body}");
assert!(body.contains("<key>RunAtLoad</key>"), "{body}");
assert!(sb.home().join(".galdr/logs").is_dir(), "log dir must exist");
let logged = std::fs::read_to_string(&log).unwrap();
assert!(logged.contains("bootstrap"), "expected bootstrap: {logged}");
let status = sb
.cmd()
.env("GALDR_LAUNCHCTL", &launchctl)
.args(["daemon", "status"])
.output()
.unwrap();
assert!(
stdout(&status).contains("launchd: managed"),
"{}",
stdout(&status)
);
let uninstall = sb
.cmd()
.env("GALDR_LAUNCHCTL", &launchctl)
.args(["daemon", "uninstall"])
.output()
.unwrap();
assert!(uninstall.status.success(), "{}", stderr(&uninstall));
assert!(!plist.exists(), "the plist must be removed on uninstall");
assert!(
sb.home().join(".galdr/logs").is_dir(),
"uninstall must not delete logs/state"
);
let logged = std::fs::read_to_string(&log).unwrap();
assert!(logged.contains("bootout"), "expected bootout: {logged}");
}
#[cfg(target_os = "macos")]
#[test]
fn daemon_status_reports_unmanaged_without_a_launchagent() {
let sb = Sandbox::new();
let status = sb.run(&["daemon", "status"]);
assert!(status.status.success());
assert!(
stdout(&status).contains("launchd: unmanaged"),
"{}",
stdout(&status)
);
}
#[cfg(not(target_os = "macos"))]
#[test]
fn daemon_install_is_macos_only() {
let sb = Sandbox::new();
let out = sb.run(&["daemon", "install"]);
assert!(!out.status.success());
assert!(stderr(&out).contains("macOS-only"), "{}", stderr(&out));
}
#[test]
fn concurrent_recordings_bind_and_isolate_by_session() {
let sb = Sandbox::new();
for name in ["projA", "projB", "projC"] {
std::fs::create_dir_all(sb.home().join(name)).unwrap();
}
let proj_a = std::fs::canonicalize(sb.home().join("projA")).unwrap();
let proj_b = std::fs::canonicalize(sb.home().join("projB")).unwrap();
let proj_c = std::fs::canonicalize(sb.home().join("projC")).unwrap();
assert!(
sb.cmd()
.current_dir(&proj_a)
.args(["rec", "start", "alpha"])
.status()
.unwrap()
.success()
);
assert!(
sb.cmd()
.current_dir(&proj_b)
.args(["rec", "start", "beta"])
.status()
.unwrap()
.success()
);
let event = |session: &str, cwd: &std::path::Path, cmd: &str| {
format!(
r#"{{"tool_name":"Bash","tool_input":{{"command":"{cmd}"}},"tool_response":{{}},"session_id":"{session}","cwd":"{}"}}"#,
cwd.display()
)
};
assert!(
sb.hook(&event("sessA", &proj_a, "a-one"), false)
.status
.success()
);
assert!(
sb.hook(&event("sessB", &proj_b, "b-one"), false)
.status
.success()
);
assert!(
sb.hook(&event("sessB", &proj_b, "b-two"), false)
.status
.success()
);
assert!(
sb.hook(&event("sessA", &proj_a, "a-two"), false)
.status
.success()
);
assert!(
sb.hook(&event("sessC", &proj_c, "c-one"), false)
.status
.success()
);
let alpha = sb.active_rec_id_by_name("alpha");
let beta = sb.active_rec_id_by_name("beta");
assert_ne!(alpha, beta);
let span = |id: &str| {
std::fs::read_to_string(sb.home().join(".galdr/spans").join(format!("{id}.jsonl"))).unwrap()
};
let a = span(&alpha);
assert!(
a.contains("a-one") && a.contains("a-two"),
"alpha span: {a}"
);
assert!(
!a.contains("b-one") && !a.contains("b-two") && !a.contains("c-one"),
"alpha span leaked another session: {a}"
);
let b = span(&beta);
assert!(b.contains("b-one") && b.contains("b-two"), "beta span: {b}");
assert!(
!b.contains("a-one") && !b.contains("c-one"),
"beta span leaked another session: {b}"
);
assert_eq!(
sb.span_lines(&alpha),
2,
"alpha recorded exactly its 2 steps"
);
assert_eq!(sb.span_lines(&beta), 2, "beta recorded exactly its 2 steps");
assert!(sb.run(&["rec", "stop", "alpha"]).status.success());
let status = stdout(&sb.run(&["rec", "status"]));
assert!(status.contains("1 active recording"), "{status}");
assert!(status.contains("beta"), "{status}");
assert!(
sb.hook(&event("sessB", &proj_b, "b-three"), false)
.status
.success()
);
assert_eq!(
sb.span_lines(&beta),
3,
"beta kept recording after alpha stopped"
);
let list = stdout(&sb.run(&["list"]));
assert!(
list.contains("alpha"),
"alpha should be a closed recording: {list}"
);
}
#[test]
fn legacy_active_flag_is_migrated_without_losing_the_recording() {
let sb = Sandbox::new();
std::fs::create_dir_all(sb.home().join(".galdr")).unwrap();
std::fs::write(
sb.home().join(".galdr/active"),
r#"{"rec_id":"01LEGACY0000000000000000AA","name":"legacy-run","started_at":"2026-07-02T00:00:00Z","origin_cwd":null,"bound_session":null}"#,
)
.unwrap();
let status = stdout(&sb.run(&["rec", "status"]));
assert!(status.contains("legacy-run"), "{status}");
assert!(status.contains("01LEGACY0000000000000000AA"), "{status}");
assert!(
!sb.home().join(".galdr/active").exists(),
"legacy flag must be removed"
);
assert!(
sb.home()
.join(".galdr/active.d/01LEGACY0000000000000000AA.json")
.exists(),
"recording must be folded into active.d/"
);
assert!(sb.hook(BASH_STATUS, false).status.success());
assert_eq!(sb.span_lines("01LEGACY0000000000000000AA"), 1);
assert!(sb.run(&["rec", "stop", "legacy-run"]).status.success());
}