use crate::common::Drip;
use std::fs;
use std::path::Path;
use std::process::Command;
fn run_init_local(drip: &Drip, project: &Path, home: &Path) -> std::process::Output {
Command::new(&drip.bin)
.args(["init", "--agent", "claude"])
.current_dir(project)
.env("HOME", home)
.env("DRIP_DATA_DIR", drip.data_dir.path())
.env("DRIP_SESSION_ID", &drip.session_id)
.output()
.expect("drip init claude")
}
fn run_init_global(drip: &Drip, project: &Path, home: &Path) -> std::process::Output {
Command::new(&drip.bin)
.args(["init", "--global", "--agent", "claude"])
.current_dir(project)
.env("HOME", home)
.env("DRIP_DATA_DIR", drip.data_dir.path())
.env("DRIP_SESSION_ID", &drip.session_id)
.output()
.expect("drip init --global claude")
}
fn run_uninstall_local(drip: &Drip, project: &Path, home: &Path) -> std::process::Output {
Command::new(&drip.bin)
.args(["uninstall", "--agent", "claude"])
.current_dir(project)
.env("HOME", home)
.env("DRIP_DATA_DIR", drip.data_dir.path())
.env("DRIP_SESSION_ID", &drip.session_id)
.output()
.expect("drip uninstall claude")
}
fn run_uninstall_global(drip: &Drip, project: &Path, home: &Path) -> std::process::Output {
Command::new(&drip.bin)
.args(["uninstall", "--global", "--agent", "claude"])
.current_dir(project)
.env("HOME", home)
.env("DRIP_DATA_DIR", drip.data_dir.path())
.env("DRIP_SESSION_ID", &drip.session_id)
.output()
.expect("drip uninstall --global claude")
}
#[test]
fn drip_md_created_on_init() {
let drip = Drip::new();
let project = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let o = run_init_local(&drip, project.path(), home.path());
assert!(
o.status.success(),
"init failed: {}",
String::from_utf8_lossy(&o.stderr)
);
let drip_md = project.path().join("drip.md");
assert!(drip_md.exists(), "drip.md not created at {drip_md:?}");
let body = fs::read_to_string(&drip_md).unwrap();
assert!(body.contains("drip:memory"), "marker missing: {body}");
assert!(
body.contains("drip refresh"),
"drip.md missing refresh hint: {body}"
);
assert!(
body.to_lowercase().contains("diff"),
"drip.md should explain the diff format: {body}"
);
}
#[test]
fn claude_md_append_preserves_existing_content() {
let drip = Drip::new();
let project = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let pre = "# Project rules\n\
- prefer integration tests\n\
- never use `.unwrap()` in src/\n";
fs::write(project.path().join("CLAUDE.md"), pre).unwrap();
let o = run_init_local(&drip, project.path(), home.path());
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
let after = fs::read_to_string(project.path().join("CLAUDE.md")).unwrap();
assert!(after.contains("# Project rules"), "title dropped: {after}");
assert!(after.contains("prefer integration tests"));
assert!(after.contains("never use `.unwrap()`"));
assert!(after.contains("@drip.md"), "@drip.md not appended: {after}");
}
#[test]
fn claude_md_append_idempotent() {
let drip = Drip::new();
let project = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let pre = "# Existing\nfoo\n";
fs::write(project.path().join("CLAUDE.md"), pre).unwrap();
run_init_local(&drip, project.path(), home.path());
let first = fs::read_to_string(project.path().join("CLAUDE.md")).unwrap();
run_init_local(&drip, project.path(), home.path());
let second = fs::read_to_string(project.path().join("CLAUDE.md")).unwrap();
assert_eq!(first, second, "CLAUDE.md drifted on second init");
assert_eq!(
second.matches("@drip.md").count(),
1,
"@drip.md duplicated: {second}"
);
}
#[test]
fn claude_md_created_if_missing() {
let drip = Drip::new();
let project = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
assert!(!project.path().join("CLAUDE.md").exists());
let o = run_init_local(&drip, project.path(), home.path());
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
let body = fs::read_to_string(project.path().join("CLAUDE.md")).unwrap();
assert!(
body.contains("@drip.md"),
"freshly-created CLAUDE.md missing reference: {body}"
);
}
#[test]
fn uninstall_removes_reference_only() {
let drip = Drip::new();
let project = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let pre = "# Project rules\n\
keep me intact\n";
fs::write(project.path().join("CLAUDE.md"), pre).unwrap();
run_init_local(&drip, project.path(), home.path());
let after_init = fs::read_to_string(project.path().join("CLAUDE.md")).unwrap();
assert!(after_init.contains("@drip.md"));
assert!(project.path().join("drip.md").exists());
let o = run_uninstall_local(&drip, project.path(), home.path());
assert!(
o.status.success(),
"uninstall failed: {}",
String::from_utf8_lossy(&o.stderr)
);
let final_md = fs::read_to_string(project.path().join("CLAUDE.md")).unwrap();
assert!(
!final_md.contains("@drip.md"),
"@drip.md still present after uninstall: {final_md}"
);
assert!(
final_md.contains("# Project rules"),
"user content lost: {final_md}"
);
assert!(final_md.contains("keep me intact"));
assert!(
!project.path().join("drip.md").exists(),
"drip.md should be deleted on uninstall"
);
let cfg_path = project.path().join(".claude/settings.json");
if cfg_path.exists() {
let cfg = fs::read_to_string(&cfg_path).unwrap();
assert!(
!cfg.contains("hook claude"),
"drip hook entries still in settings.json: {cfg}"
);
}
}
#[test]
fn init_registers_session_start_compact_clear_hook() {
let drip = Drip::new();
let project = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let o = run_init_global(&drip, project.path(), home.path());
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
let settings_path = home.path().join(".claude/settings.json");
let parsed: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&settings_path).unwrap()).unwrap();
let arr = parsed["hooks"]["SessionStart"]
.as_array()
.expect("SessionStart array missing — install path didn't write the recovery hook");
assert!(
arr.iter().any(|m| {
m["matcher"].as_str() == Some("compact|clear")
&& m["hooks"][0]["command"]
.as_str()
.map(|c| c.ends_with("hook claude-session-start"))
.unwrap_or(false)
}),
"SessionStart entry must use the `compact|clear` matcher and our claude-session-start \
subcommand: {arr:?}"
);
let report_uninstall = run_uninstall_global(&drip, project.path(), home.path());
assert!(
report_uninstall.status.success(),
"{}",
String::from_utf8_lossy(&report_uninstall.stderr)
);
let after: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&settings_path).unwrap()).unwrap();
let post_arr = after["hooks"]["SessionStart"]
.as_array()
.cloned()
.unwrap_or_default();
assert!(
!post_arr.iter().any(|m| {
m["hooks"][0]["command"]
.as_str()
.map(|c| c.contains("hook claude-session-start"))
.unwrap_or(false)
}),
"uninstall must remove our SessionStart entry, kept: {post_arr:?}"
);
}
#[test]
fn global_install_targets_home_claude_dir() {
let drip = Drip::new();
let project = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let o = run_init_global(&drip, project.path(), home.path());
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
assert!(
home.path().join(".claude/drip.md").exists(),
"global drip.md missing"
);
let claude_md = fs::read_to_string(home.path().join(".claude/CLAUDE.md")).unwrap();
assert!(
claude_md.contains("@drip.md"),
"global CLAUDE.md: {claude_md}"
);
assert!(!project.path().join("drip.md").exists());
assert!(!project.path().join("CLAUDE.md").exists());
}