use std::path::Path;
use std::process::Command as StdCommand;
use assert_cmd::prelude::*;
use serde_json::Value;
use tempfile::TempDir;
struct Project {
dir: TempDir,
}
impl Project {
fn new() -> Self {
Project {
dir: tempfile::tempdir().unwrap(),
}
}
fn path(&self) -> &Path {
self.dir.path()
}
fn cmd(&self, args: &[&str]) -> StdCommand {
let mut c = StdCommand::cargo_bin("wipe").unwrap();
c.current_dir(self.dir.path());
c.env("WIPE_AUTHOR", "Tester <t@example.com>");
c.env("WIPE_CONFIG_DIR", self.dir.path());
c.args(args);
c
}
fn run(&self, args: &[&str]) -> String {
let out = self.cmd(args).output().unwrap();
assert!(
out.status.success(),
"command {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
fn json(&self, args: &[&str]) -> Value {
let mut v = args.to_vec();
v.push("--json");
let stdout = self.run(&v);
serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("bad json from {args:?}: {e}\n{stdout}"))
}
}
#[test]
fn init_creates_board_and_status_shows_lists() {
let p = Project::new();
p.run(&["init", ".", "--name", "Demo"]);
assert!(p.path().join(".wipe/board.json").is_file());
let status = p.json(&["status"]);
assert_eq!(status["board"], "Demo");
let lists = status["lists"].as_array().unwrap();
assert_eq!(lists.len(), 4);
assert_eq!(lists[0]["list"], "backlog");
}
#[test]
fn full_agent_flow() {
let p = Project::new();
p.run(&["init", ".", "--name", "Flow"]);
let t1 = p.json(&[
"ticket",
"create",
"--title",
"Add login",
"--priority",
"high",
]);
assert_eq!(t1["id"], "T-1");
let t2 = p.json(&["ticket", "create", "--title", "Fix navbar"]);
assert_eq!(t2["id"], "T-2");
p.json(&["ticket", "move", "T-1", "--to", "in-progress"]);
let c = p.json(&["comment", "add", "T-1", "--body", "Use OAuth"]);
assert_eq!(c["comment"], "c-1");
assert_eq!(c["author"], "Tester <t@example.com>");
p.json(&["label", "assign", "T-1", "needs-review"]);
let show = p.json(&["ticket", "show", "T-1"]);
assert_eq!(show["list"], "in-progress");
assert_eq!(show["labels"][0], "needs-review");
assert_eq!(show["comments"][0]["body"], "Use OAuth");
let closed = p.json(&["ticket", "close", "T-1"]);
assert_eq!(closed["list"], "done");
let backlog = p.json(&["ticket", "list", "--list", "backlog"]);
let arr = backlog.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["id"], "T-2");
}
#[test]
fn json_error_object_and_nonzero_exit_on_missing_ticket() {
let p = Project::new();
p.run(&["init", ".", "--name", "Err"]);
let out = p
.cmd(&["ticket", "show", "T-99", "--json"])
.output()
.unwrap();
assert!(!out.status.success());
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["ok"], false);
assert!(v["error"].as_str().unwrap().contains("T-99"));
}
#[test]
fn delete_requires_confirmation() {
let p = Project::new();
p.run(&["init", ".", "--name", "Del"]);
p.json(&["ticket", "create", "--title", "Temp"]);
let refused = p.cmd(&["ticket", "delete", "T-1"]).output().unwrap();
assert!(!refused.status.success());
p.json(&["ticket", "delete", "T-1", "--yes"]);
let missing = p
.cmd(&["ticket", "show", "T-1", "--json"])
.output()
.unwrap();
assert!(!missing.status.success());
}
#[test]
fn discovers_board_from_subdirectory() {
let p = Project::new();
p.run(&["init", ".", "--name", "Nested"]);
let sub = p.path().join("src").join("deep");
std::fs::create_dir_all(&sub).unwrap();
let mut c = StdCommand::cargo_bin("wipe").unwrap();
c.current_dir(&sub)
.env("WIPE_AUTHOR", "T <t@e.com>")
.args(["status", "--json"]);
let out = c.output().unwrap();
assert!(out.status.success());
let v: Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["board"], "Nested");
}
#[test]
fn config_roundtrip() {
let p = Project::new();
p.run(&["init", ".", "--name", "Cfg"]);
p.json(&["config", "set", "daemon.port", "9999"]);
let got = p.json(&["config", "get", "daemon.port"]);
assert_eq!(got["value"], 9999);
p.json(&["config", "set", "daemon.expose", "tailscale"]);
let expose = p.json(&["config", "get", "daemon.expose"]);
assert_eq!(expose["value"], "tailscale");
}
#[test]
fn skill_is_embedded() {
let p = Project::new();
let out = p.run(&["skill"]);
assert!(out.contains("wipe - agent operating guide"));
assert!(p.run(&["skill", "show"]).contains("agent operating guide"));
}
#[test]
fn init_empty_starter_has_no_lists() {
let p = Project::new();
let v = p.json(&[
"init",
".",
"--name",
"Blank",
"--yes",
"--starter",
"empty",
]);
assert_eq!(v["starter"], "empty");
let status = p.json(&["status"]);
assert!(status["lists"].as_array().unwrap().is_empty());
}
#[test]
fn skill_install_writes_file_and_respects_force() {
let p = Project::new();
let base = p.path().join("dest");
let base_s = base.to_str().unwrap();
let v = p.json(&["skill", "install", "--dir", base_s, "--target", "claude"]);
assert_eq!(v["target"], "claude");
assert!(base.join("skills/wipe/SKILL.md").is_file());
let again = p
.cmd(&["skill", "install", "--dir", base_s])
.output()
.unwrap();
assert!(!again.status.success());
p.json(&["skill", "install", "--dir", base_s, "--force"]);
let path = p.json(&["skill", "path", "--dir", base_s, "--target", "agents"]);
assert!(path["path"].as_str().unwrap().contains("SKILL.md"));
}
#[test]
fn skill_install_auto_detects_agents_dir() {
let p = Project::new();
std::fs::create_dir_all(p.path().join(".agents")).unwrap();
let v = p.json(&["skill", "install"]);
assert_eq!(v["target"], "agents");
assert!(p.path().join(".agents/skills/wipe/SKILL.md").is_file());
}
#[test]
fn global_config_roundtrip() {
let p = Project::new();
p.json(&["config", "--global", "set", "autoserve", "true"]);
p.json(&["config", "--global", "set", "starter", "empty"]);
assert_eq!(
p.json(&["config", "--global", "get", "autoserve"])["value"],
true
);
assert_eq!(
p.json(&["config", "--global", "get", "starter"])["value"],
"empty"
);
let v = p.json(&["init", ".", "--name", "G", "--yes"]);
assert_eq!(v["starter"], "empty");
assert_eq!(v["autoserve"], true);
}
#[test]
fn supervision_protocol_offline() {
let p = Project::new();
p.run(&["init", ".", "--name", "Calc Service"]);
let filed = p.json(&[
"ticket",
"create",
"--title",
"Implement add",
"--list",
"todo",
"--body",
"Create calc.py defining add(a, b).",
]);
assert_eq!(filed["id"], "T-1");
let todo = p.json(&["ticket", "list", "--list", "todo"]);
let assigned = todo.as_array().unwrap();
assert_eq!(assigned.len(), 1);
let id = assigned[0]["id"].as_str().unwrap().to_string();
let spec = p.json(&["ticket", "show", &id]);
assert!(spec["body"].as_str().unwrap().contains("calc.py"));
p.json(&[
"comment",
"add",
&id,
"--body",
"Implemented add(a,b) in calc.py",
]);
p.json(&["ticket", "move", &id, "--to", "done"]);
let done = p.json(&["ticket", "show", &id]);
assert_eq!(done["list"], "done");
assert!(!done["comments"].as_array().unwrap().is_empty());
}