mod common;
use common::Sandbox;
const SESSION_START: &str = "vivac session start --hook";
const SESSION_END: &str = "vivac session end --hook";
const CONFIG_LABEL: &str = ".codex/config.toml";
const HOOKS_LABEL: &str = ".codex/hooks.json";
const SKILL_LABEL: &str = ".agents/skills/vivac-migrate/SKILL.md";
const EXPECTED_CONFIG: &str = "# added by vivac setup codex\n\
[mcp_servers.vivac]\n\
command = \"vivac\"\n\
args = [\"mcp\"]\n\
# end of what vivac setup codex added\n";
const EXPECTED_HOOKS: &str = "{\n \
\"hooks\": {\n \
\"SessionStart\": [\n \
{\n \
\"matcher\": \"startup|resume|clear|compact\",\n \
\"hooks\": [\n \
{ \"type\": \"command\", \"command\": \"vivac session start --hook\" }\n \
]\n \
}\n \
],\n \
\"Stop\": [\n \
{\n \
\"hooks\": [\n \
{ \"type\": \"command\", \"command\": \"vivac session end --hook\" }\n \
]\n \
}\n \
]\n \
}\n\
}\n";
fn config_path(c: &Sandbox) -> std::path::PathBuf {
c.0.join(".codex").join("config.toml")
}
fn hooks_path(c: &Sandbox) -> std::path::PathBuf {
c.0.join(".codex").join("hooks.json")
}
fn skill_path(c: &Sandbox) -> std::path::PathBuf {
c.0.join(".agents")
.join("skills")
.join("vivac-migrate")
.join("SKILL.md")
}
fn read(p: &std::path::Path) -> String {
std::fs::read_to_string(p).unwrap_or_else(|e| panic!("reading {p:?}: {e}"))
}
#[cfg(unix)]
fn printed(p: &std::path::Path) -> std::path::PathBuf {
std::fs::canonicalize(p).unwrap_or_else(|e| panic!("canonicalize {p:?}: {e}"))
}
#[cfg(not(unix))]
fn printed(p: &std::path::Path) -> std::path::PathBuf {
p.to_path_buf()
}
#[test]
fn dry_run_writes_nothing_and_shows_the_three_paths() {
let c = Sandbox::new_empty("setup-codex-dry-run");
let before: Vec<_> = std::fs::read_dir(&c.0).unwrap().collect();
let (out, code) = c.run(&["setup", "codex", "--dry-run"]);
assert_eq!(code, 0, "{out}");
assert!(out.contains("vivac setup codex, in"), "{out}");
assert!(out.contains(CONFIG_LABEL), "{out}");
assert!(out.contains(HOOKS_LABEL), "{out}");
assert!(out.contains(SKILL_LABEL), "{out}");
assert!(out.contains("Nothing written: --dry-run."), "{out}");
let after: Vec<_> = std::fs::read_dir(&c.0).unwrap().collect();
assert_eq!(before.len(), after.len(), "dry-run created something");
assert!(!c.0.join(".codex").exists());
assert!(!c.0.join(".agents").exists());
}
#[test]
fn a_clean_project_gets_the_three_files_with_the_exact_content() {
let c = Sandbox::new_empty("setup-codex-fresh");
let (out, code) = c.run(&["setup", "codex", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(out.contains("Written."), "{out}");
assert_eq!(read(&config_path(&c)), EXPECTED_CONFIG);
assert_eq!(read(&hooks_path(&c)), EXPECTED_HOOKS);
let hooks: serde_json::Value = serde_json::from_str(&read(&hooks_path(&c))).unwrap();
assert_eq!(
hooks["hooks"]["SessionStart"][0]["hooks"][0]["command"],
SESSION_START
);
assert_eq!(
hooks["hooks"]["Stop"][0]["hooks"][0]["command"],
SESSION_END
);
}
#[test]
fn the_skill_is_byte_for_byte_the_one_claude_code_writes() {
let c = Sandbox::new_empty("setup-codex-skill");
c.ok(&["setup", "codex", "--yes"]);
c.ok(&["setup", "claude-code", "--yes"]);
let codex_skill = std::fs::read(skill_path(&c)).unwrap();
let claude_code_skill = std::fs::read(
c.0.join(".claude")
.join("skills")
.join("vivac-migrate")
.join("SKILL.md"),
)
.unwrap();
assert_eq!(
codex_skill, claude_code_skill,
"the two harnesses wrote a different skill"
);
}
#[test]
fn the_summary_names_the_two_doors_with_the_real_project_path() {
let c = Sandbox::new_empty("setup-codex-doors");
let (out, code) = c.run(&["setup", "codex", "--yes"]);
assert_eq!(code, 0, "{out}");
let path = printed(&c.0);
assert!(
out.contains(&format!("[projects.'{}']", path.display())),
"{out}"
);
assert!(out.contains("trust_level = \"trusted\""), "{out}");
assert!(out.contains("~/.codex/config.toml"), "{out}");
assert!(out.contains("/hooks"), "{out}");
}
#[test]
fn none_of_the_three_files_carries_an_absolute_path() {
let c = Sandbox::new_empty("setup-codex-no-path");
c.ok(&["setup", "codex", "--yes"]);
let needle = c.0.to_string_lossy().into_owned();
for (label, text) in [
(CONFIG_LABEL, read(&config_path(&c))),
(HOOKS_LABEL, read(&hooks_path(&c))),
(SKILL_LABEL, read(&skill_path(&c))),
] {
assert!(
!text.contains(&needle),
"{label} carries the project's own path:\n{text}"
);
}
}
#[test]
fn a_file_already_there_stops_instead_of_guessing_how_to_merge() {
let c = Sandbox::new_empty("setup-codex-conflict");
std::fs::create_dir_all(c.0.join(".codex")).unwrap();
std::fs::write(config_path(&c), "# hand-written\n").unwrap();
let (out, code) = c.run(&["setup", "codex", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(out.contains(CONFIG_LABEL), "{out}");
assert_eq!(
read(&config_path(&c)),
"# hand-written\n",
"the existing file was touched"
);
assert!(!hooks_path(&c).exists(), "hooks.json was written anyway");
assert!(!skill_path(&c).exists(), "SKILL.md was written anyway");
}
#[test]
fn setup_with_no_harness_names_both_harnesses() {
let c = Sandbox::new_empty("setup-codex-no-harness");
let (out, code) = c.run(&["setup"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("claude-code"), "{out}");
assert!(out.contains("codex"), "{out}");
}
#[test]
fn setup_with_an_unknown_harness_names_both_harnesses() {
let c = Sandbox::new_empty("setup-codex-unknown-harness");
let (out, code) = c.run(&["setup", "loquesea"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("claude-code"), "{out}");
assert!(out.contains("codex"), "{out}");
}
fn assert_nothing_was_written(c: &Sandbox) {
assert!(!config_path(c).exists(), "config.toml was written anyway");
assert!(!hooks_path(c).exists(), "hooks.json was written anyway");
assert!(!skill_path(c).exists(), "SKILL.md was written anyway");
}
#[test]
fn undo_refuses_before_writing_anything() {
let c = Sandbox::new_empty("setup-codex-flag-undo");
let (out, code) = c.run(&["setup", "codex", "--yes", "--undo"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--undo"), "{out}");
assert_nothing_was_written(&c);
}
#[test]
fn join_refuses_before_writing_anything() {
let c = Sandbox::new_empty("setup-codex-flag-join");
let (out, code) = c.run(&["setup", "codex", "--yes", "--join", "somewhere"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--join"), "{out}");
assert_nothing_was_written(&c);
}
#[test]
fn new_tree_refuses_before_writing_anything() {
let c = Sandbox::new_empty("setup-codex-flag-new-tree");
let (out, code) = c.run(&["setup", "codex", "--yes", "--new-tree"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--new-tree"), "{out}");
assert_nothing_was_written(&c);
}
#[test]
fn lane_name_refuses_before_writing_anything() {
let c = Sandbox::new_empty("setup-codex-flag-lane-name");
let (out, code) = c.run(&["setup", "codex", "--yes", "--lane-name", "mine"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--lane-name"), "{out}");
assert_nothing_was_written(&c);
}
#[test]
fn name_refuses_before_writing_anything() {
let c = Sandbox::new_empty("setup-codex-flag-name");
let (out, code) = c.run(&["setup", "codex", "--yes", "--name", "IQuorum"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--name"), "{out}");
assert_nothing_was_written(&c);
}