mod common;
use common::TestRepo;
use std::path::Path;
use std::process::Command;
fn run_setup_claude(cwd: &Path) -> std::process::Output {
Command::new(TestRepo::cw_bin())
.arg("setup-claude")
.current_dir(cwd)
.output()
.expect("failed to spawn gw setup-claude")
}
fn read_settings(path: &Path) -> serde_json::Value {
let raw = std::fs::read_to_string(path).expect("settings.json must exist");
serde_json::from_str(&raw).expect("valid JSON")
}
#[test]
fn creates_all_files_from_scratch() {
let repo = TestRepo::new();
let cwd = repo.path();
let out = run_setup_claude(cwd);
assert!(
out.status.success(),
"gw setup-claude failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
cwd.join(".claude/skills/gw-delegate/SKILL.md").exists(),
"gw-delegate/SKILL.md must be created"
);
assert!(
cwd.join(".claude/skills/gw-manage/SKILL.md").exists(),
"gw-manage/SKILL.md must be created"
);
assert!(
cwd.join(".claude/skills/gw-manage/references/gw-commands.md")
.exists(),
"gw-commands.md must be created"
);
let settings_path = cwd.join(".claude/settings.json");
assert!(
settings_path.exists(),
".claude/settings.json must be created"
);
let v = read_settings(&settings_path);
let pre = v["hooks"]["PreToolUse"]
.as_array()
.expect("PreToolUse array");
assert_eq!(pre.len(), 1);
assert_eq!(pre[0]["matcher"], "Bash");
assert_eq!(
pre[0]["hooks"][0]["command"].as_str().expect("command"),
"gw guard --tool-input -"
);
let create = v["hooks"]["WorktreeCreate"]
.as_array()
.expect("WorktreeCreate array");
assert_eq!(create.len(), 1);
assert_eq!(
create[0]["hooks"][0]["command"].as_str().expect("command"),
"gw _claude-worktree-create"
);
let remove = v["hooks"]["WorktreeRemove"]
.as_array()
.expect("WorktreeRemove array");
assert_eq!(remove.len(), 1);
assert_eq!(
remove[0]["hooks"][0]["command"].as_str().expect("command"),
"gw _claude-worktree-remove"
);
}
#[test]
fn idempotent_two_runs() {
let repo = TestRepo::new();
let cwd = repo.path();
run_setup_claude(cwd);
let out2 = run_setup_claude(cwd);
assert!(
out2.status.success(),
"second run failed: {}",
String::from_utf8_lossy(&out2.stderr)
);
let settings_path = cwd.join(".claude/settings.json");
let v = read_settings(&settings_path);
assert_eq!(v["hooks"]["PreToolUse"].as_array().unwrap().len(), 1);
assert_eq!(v["hooks"]["WorktreeCreate"].as_array().unwrap().len(), 1);
assert_eq!(v["hooks"]["WorktreeRemove"].as_array().unwrap().len(), 1);
assert!(cwd.join(".claude/skills/gw-delegate/SKILL.md").exists());
assert!(cwd.join(".claude/skills/gw-manage/SKILL.md").exists());
}
#[test]
fn preserves_existing_user_pretooluse_write_hook() {
let repo = TestRepo::new();
let cwd = repo.path();
let claude_dir = cwd.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let settings_path = claude_dir.join("settings.json");
std::fs::write(
&settings_path,
r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "/usr/local/bin/my-linter" }
]
}
]
}
}
"#,
)
.unwrap();
let out = run_setup_claude(cwd);
assert!(out.status.success());
let v = read_settings(&settings_path);
let arr = v["hooks"]["PreToolUse"].as_array().unwrap();
assert_eq!(arr.len(), 2, "Write hook + Bash hook = 2");
let matchers: Vec<&str> = arr
.iter()
.filter_map(|e| e.get("matcher").and_then(|m| m.as_str()))
.collect();
assert!(matchers.contains(&"Write"), "Write hook must be preserved");
assert!(matchers.contains(&"Bash"), "Bash hook must be added");
}
#[test]
fn malformed_json_errors_without_overwriting() {
let repo = TestRepo::new();
let cwd = repo.path();
let claude_dir = cwd.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let settings_path = claude_dir.join("settings.json");
let bad_content = "{ this is not valid json }}}";
std::fs::write(&settings_path, bad_content).unwrap();
let out = run_setup_claude(cwd);
assert!(
!out.status.success(),
"should exit non-zero on malformed JSON"
);
let after = std::fs::read_to_string(&settings_path).unwrap();
assert_eq!(after, bad_content, "malformed file must not be overwritten");
}
#[test]
fn refreshes_drifted_skill_file() {
let repo = TestRepo::new();
let cwd = repo.path();
run_setup_claude(cwd);
let skill_path = cwd.join(".claude/skills/gw-delegate/SKILL.md");
let original = std::fs::read_to_string(&skill_path).unwrap();
let modified = format!("{original}\n# MANUALLY MODIFIED\n");
std::fs::write(&skill_path, &modified).unwrap();
let out = run_setup_claude(cwd);
assert!(out.status.success());
let after = std::fs::read_to_string(&skill_path).unwrap();
assert_eq!(
after, original,
"skill file must be restored to embedded content"
);
}
#[test]
fn migrates_legacy_absolute_path_entries() {
let repo = TestRepo::new();
let cwd = repo.path();
let claude_dir = cwd.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let settings_path = claude_dir.join("settings.json");
std::fs::write(
&settings_path,
r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "/usr/local/bin/gw guard --tool-input -" }
]
},
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "/usr/local/bin/my-linter" }
]
}
],
"WorktreeCreate": [
{
"hooks": [
{ "type": "command", "command": "/opt/homebrew/bin/gw _claude-worktree-create" }
]
}
],
"WorktreeRemove": [
{
"hooks": [
{ "type": "command", "command": "/Users/dave/proj/target/release/gw _claude-worktree-remove" }
]
}
]
}
}
"#,
)
.unwrap();
let out = run_setup_claude(cwd);
assert!(
out.status.success(),
"migration run failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = read_settings(&settings_path);
let pre = v["hooks"]["PreToolUse"].as_array().unwrap();
assert_eq!(pre.len(), 2, "Write hook + migrated Bash hook");
let pre_cmds: Vec<&str> = pre
.iter()
.map(|e| e["hooks"][0]["command"].as_str().unwrap())
.collect();
assert!(pre_cmds.contains(&"gw guard --tool-input -"));
assert!(pre_cmds.contains(&"/usr/local/bin/my-linter"));
let create = v["hooks"]["WorktreeCreate"].as_array().unwrap();
assert_eq!(create.len(), 1);
assert_eq!(
create[0]["hooks"][0]["command"],
"gw _claude-worktree-create"
);
let remove = v["hooks"]["WorktreeRemove"].as_array().unwrap();
assert_eq!(remove.len(), 1);
assert_eq!(
remove[0]["hooks"][0]["command"],
"gw _claude-worktree-remove"
);
}