mod common;
use common::Sandbox;
const LOCK_SENTENCE: &str =
"this tree holds pillars and rules, and this vivac is too old to read them: update vivac";
fn config_bytes(c: &Sandbox) -> Vec<u8> {
std::fs::read(c.0.join(".vivac").join("config")).unwrap()
}
fn log_bytes(c: &Sandbox) -> Vec<u8> {
std::fs::read(c.0.join(".vivac").join("events")).unwrap()
}
fn is_locked(bytes: &[u8]) -> bool {
String::from_utf8_lossy(bytes).contains(LOCK_SENTENCE)
}
#[test]
fn a_second_init_leaves_a_locked_config_and_log_untouched() {
let c = Sandbox::new_seeded("init-twice");
c.ok(&["add", "A rule", "--type", "rule", "--why", "guard"]);
let config_before = config_bytes(&c);
let log_before = log_bytes(&c);
assert!(
c.is_locked_any(),
"setup: the tree should already be locked"
);
let out = c.ok(&["init", "--yes"]);
assert!(
out.contains("Nothing to write: this project is already set up."),
"{out}"
);
assert_eq!(config_before, config_bytes(&c), "the config moved");
assert_eq!(log_before, log_bytes(&c), "the log moved");
}
#[test]
fn init_over_an_empty_vivac_directory_plants_a_tree() {
let c = Sandbox::new_empty("init-empty-dir");
std::fs::create_dir_all(c.0.join(".vivac")).unwrap();
let out = c.ok(&["init", "--yes"]);
assert!(out.contains("Written."), "{out}");
assert!(
out.contains("First node: vivac push \"<title>\" --why \"<reason>\""),
"{out}"
);
assert!(c.0.join(".vivac").join("config").is_file());
assert!(c.0.join(".vivac").join("events").is_file());
}
#[test]
fn init_keeps_the_tree_out_of_version_control() {
let c = Sandbox::new_seeded("gitignore");
let g = std::fs::read_to_string(c.0.join(".vivac").join(".gitignore")).unwrap();
assert_eq!(g, "*\n");
}
#[test]
fn init_writes_the_gitignore_a_tree_from_before_lacks() {
let c = Sandbox::new_seeded("init-gitignore");
std::fs::remove_file(c.0.join(".vivac").join(".gitignore")).unwrap();
let plan = c.ok(&["init", "--dry-run"]);
assert!(
lane_line_containing(
&plan,
".vivac/.gitignore",
"create: keeps .vivac/ out of version control",
),
"{plan}"
);
c.ok(&["init", "--yes"]);
let g = std::fs::read_to_string(c.0.join(".vivac").join(".gitignore")).unwrap();
assert_eq!(g, "*\n");
}
#[test]
fn init_says_it_created_the_trees_gitignore_instead_of_claiming_nothing_changed() {
let c = Sandbox::new_empty("init-gitignore-message");
c.ok(&["init", "--yes"]);
std::fs::remove_file(c.0.join(".vivac").join(".gitignore")).unwrap();
let out = c.ok(&["init", "--yes"]);
assert!(
out.contains("init wrote in it: its own .gitignore."),
"{out}"
);
assert!(!out.contains("init changed nothing in it"), "{out}");
}
#[test]
fn a_log_with_no_config_regenerates_it_locked() {
let c = Sandbox::seeded_with_no_lane("init-no-config");
c.ok(&["add", "A rule", "--type", "rule", "--why", "guard"]);
std::fs::remove_file(c.0.join(".vivac").join("config")).unwrap();
c.ok(&["stack"]);
assert!(
is_locked(&config_bytes(&c)),
"a config regenerated over a governed log came back unlocked"
);
}
use std::path::Path;
fn run_in(dir: &Path, home: &Path, args: &[&str]) -> (String, i32) {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_vivac"))
.current_dir(dir)
.env("VIVAC_HOME", home)
.args(args)
.output()
.unwrap();
(
String::from_utf8_lossy(&out.stdout).into_owned() + &String::from_utf8_lossy(&out.stderr),
out.status.code().unwrap_or(-1),
)
}
fn read(p: &Path) -> String {
std::fs::read_to_string(p).unwrap_or_else(|e| panic!("reading {p:?}: {e}"))
}
fn lane_id_of(lane_dir: &Path) -> String {
let text =
std::fs::read_to_string(lane_dir.join(".vivac").join("lane")).expect("the lane file reads");
let v: serde_json::Value = serde_json::from_str(&text).expect("the lane file parses");
v["id"]
.as_str()
.expect("a lane file names an id")
.to_string()
}
#[cfg(unix)]
fn printed(p: &Path) -> std::path::PathBuf {
std::fs::canonicalize(p).unwrap_or_else(|e| panic!("canonicalize {p:?}: {e}"))
}
#[cfg(not(unix))]
fn printed(p: &Path) -> std::path::PathBuf {
p.to_path_buf()
}
fn run_with_home(dir: &Path, home: &Path, vivac_home: &Path, args: &[&str]) -> (String, i32) {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_vivac"))
.current_dir(dir)
.env("HOME", home)
.env("USERPROFILE", home)
.env("VIVAC_HOME", vivac_home)
.args(args)
.output()
.unwrap();
(
String::from_utf8_lossy(&out.stdout).into_owned() + &String::from_utf8_lossy(&out.stderr),
out.status.code().unwrap_or(-1),
)
}
fn home_folder_text(here: &Path) -> String {
format!(
" {} is your home folder. Claude Code's settings and skills here are\n \
yours for every project, not this one's, and setup never writes there.\n \
Run setup in the folder you open Claude Code in, inside a project.",
here.display()
)
}
const PLAN_STATUS_COLUMN: usize = 45;
fn lane_line_containing(out: &str, label: &str, rest: &str) -> bool {
let lines: Vec<&str> = out.lines().collect();
lines.iter().enumerate().any(|(i, l)| {
if !l.trim_start().starts_with(label) {
return false;
}
let mut joined = l.trim_start().to_string();
for cont in &lines[i + 1..] {
if !cont.starts_with(&" ".repeat(PLAN_STATUS_COLUMN)) {
break;
}
joined.push(' ');
joined.push_str(cont.trim());
}
joined
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.contains(rest)
})
}
fn plan_words(out: &str) -> String {
out.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn list(dir: &Path) -> Vec<String> {
std::fs::read_dir(dir)
.map(|rd| {
rd.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect()
})
.unwrap_or_default()
}
fn already_planted(dir: &std::path::Path) -> bool {
dir.join(".vivac").join("config").is_file() || dir.join(".vivac").join("events").is_file()
}
#[test]
fn init_dry_run_plans_a_plant_and_writes_nothing() {
let c = Sandbox::new_empty("init-dry-run");
let out = c.ok(&["init", "--dry-run"]);
assert!(out.contains("plant the tree"), "{out}");
assert!(out.contains("Nothing written: --dry-run."), "{out}");
assert!(!already_planted(&c.0), "a dry run must not plant");
}
#[test]
fn init_yes_plants_and_declares_the_founding_lane() {
let c = Sandbox::new_empty("init-yes");
let out = c.ok(&["init", "--yes"]);
assert!(out.contains("Written."), "{out}");
assert!(c.0.join(".vivac").join("config").is_file());
let log = std::fs::read_to_string(c.0.join(".vivac").join("events")).unwrap();
assert!(log.contains("\"type\":\"lane.declared\""), "{log}");
}
#[test]
fn init_name_saves_the_product_to_the_registry() {
let c = Sandbox::new_empty("init-name");
c.ok(&["init", "--yes", "--name", "IQuorum"]);
let registry = std::fs::read_to_string(c.global_home().join("projects")).unwrap();
assert!(registry.contains("\"name\": \"IQuorum\""), "{registry}");
}
#[test]
fn init_name_collision_wraps_a_long_name_rather_than_running_past_the_width() {
let c = Sandbox::new_empty("init-name-collision-width");
let first = c.0.join("first");
std::fs::create_dir_all(&first).unwrap();
let long = "A Name Chosen On Purpose To Run Longer Than One Line Of The Plan Could Hold";
run_in(&first, c.global_home(), &["init", "--yes", "--name", long]);
let second = c.0.join("second");
std::fs::create_dir_all(&second).unwrap();
let (out, code) = run_in(
&second,
c.global_home(),
&["init", "--dry-run", "--name", long],
);
assert_eq!(code, 0, "{out}");
assert!(out.contains("already names another project"), "{out}");
const SUB_LINE_INDENT: usize = 8;
for line in out.lines() {
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
if indent == SUB_LINE_INDENT || trimmed.starts_with("vivac init") {
continue;
}
assert!(
line.chars().count() <= 76,
"a plan line ran past 76 columns: {line:?}\nfull output:\n{out}"
);
}
}
#[test]
fn init_lane_name_names_the_founding_lane() {
let c = Sandbox::new_empty("init-lane-name");
c.ok(&["init", "--yes", "--lane-name", "custom-name"]);
let log = std::fs::read_to_string(c.0.join(".vivac").join("events")).unwrap();
assert!(log.contains("\"name\":\"custom-name\""), "{log}");
}
#[test]
fn init_join_wraps_a_long_lane_name_rather_than_running_past_the_width() {
let c = Sandbox::new_empty("init-join-lane-name-width");
let target =
c.0.join("A Name Chosen On Purpose To Run Longer Than One Line, target");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let joiner =
c.0.join("A Name Chosen On Purpose To Run Longer Than One Line, joiner");
std::fs::create_dir_all(&joiner).unwrap();
let (out, code) = run_in(
&joiner,
c.global_home(),
&["init", "--dry-run", "--join", target.to_str().unwrap()],
);
assert_eq!(code, 0, "{out}");
assert!(out.contains("becomes"), "{out}");
const SUB_LINE_INDENT: usize = 8;
for line in out.lines() {
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
if indent == SUB_LINE_INDENT || trimmed.starts_with("vivac init") {
continue;
}
assert!(
line.chars().count() <= 76,
"a plan line ran past 76 columns: {line:?}\nfull output:\n{out}"
);
}
}
#[test]
fn init_join_dry_run_writes_nothing() {
let target = Sandbox::new_empty("init-join-dry-run-target");
target.ok(&["init", "--yes"]);
let here = Sandbox::new_empty("init-join-dry-run-here");
let out = here.ok(&["init", "--join", target.0.to_str().unwrap(), "--dry-run"]);
assert!(out.contains("this folder becomes"), "{out}");
assert!(out.contains("Nothing written: --dry-run."), "{out}");
assert!(
!here.0.join(".vivac").exists(),
"a join dry-run must not write the lane file or anything else"
);
}
#[test]
fn init_join_declares_a_lane_of_the_target_rather_than_planting() {
let target = Sandbox::new_empty("init-join-target");
target.ok(&["init", "--yes"]);
let joiner = Sandbox::new_empty("init-join-joiner");
let out = joiner.ok(&["init", "--join", target.0.to_str().unwrap(), "--yes"]);
assert!(out.contains("becomes"), "{out}");
assert!(joiner.0.join(".vivac").join("lane").is_file());
assert!(
!already_planted(&joiner.0),
"a join must not plant a second tree"
);
}
#[test]
fn a_join_that_fails_mid_write_leaves_the_folder_as_it_was() {
let target = Sandbox::new_empty("init-join-rollback-target");
target.ok(&["init", "--yes"]);
let events = target.0.join(".vivac").join("events");
let mut perms = std::fs::metadata(&events).unwrap().permissions();
perms.set_readonly(true);
std::fs::set_permissions(&events, perms).unwrap();
let here = Sandbox::new_empty("init-join-rollback-here");
let (out, code) = here.run(&["init", "--join", target.0.to_str().unwrap(), "--yes"]);
let mut perms = std::fs::metadata(&events).unwrap().permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
std::fs::set_permissions(&events, perms).unwrap();
assert_eq!(code, 5, "{out}");
assert!(
!here.0.join(".vivac").exists(),
"a failed join left a half-written .vivac/ behind:\n{out}"
);
}
#[test]
fn init_undo_removes_a_joined_lane_and_leaves_the_target_log_growing_only() {
let target = Sandbox::new_empty("init-undo-target");
target.ok(&["init", "--yes", "--name", "Upper Product"]);
let target_log_before =
std::fs::read_to_string(target.0.join(".vivac").join("events")).unwrap();
let joiner = target.0.join("nested");
std::fs::create_dir_all(&joiner).unwrap();
let target_str = target.0.to_str().unwrap();
let (join_out, join_code) = run_in(
&joiner,
target.global_home(),
&["init", "--join", target_str, "--yes"],
);
assert_eq!(join_code, 0, "{join_out}");
assert!(joiner.join(".vivac").join("lane").is_file());
let (dry, dry_code) = run_in(
&joiner,
target.global_home(),
&["init", "--undo", "--dry-run"],
);
assert_eq!(dry_code, 0, "{dry}");
assert!(dry.contains("remove"), "{dry}");
assert!(
joiner.join(".vivac").join("lane").is_file(),
"dry-run undid something"
);
let (out, out_code) = run_in(&joiner, target.global_home(), &["init", "--undo", "--yes"]);
assert_eq!(out_code, 0, "{out}");
assert!(out.contains("Undone."), "{out}");
assert!(
!joiner.join(".vivac").exists(),
"a joined .vivac/ holding only the lane should go"
);
let target_log_after = std::fs::read_to_string(target.0.join(".vivac").join("events")).unwrap();
assert!(
target_log_after.starts_with(&target_log_before),
"the target's own pre-join history must survive the joiner's --undo untouched"
);
assert!(
target_log_after.contains("\"lane.declared\""),
"the join's own event should stay in the target's log: {target_log_after}"
);
let (joiner_brief, joiner_brief_code) = run_in(&joiner, target.global_home(), &["brief"]);
assert_eq!(joiner_brief_code, 0, "{joiner_brief}");
assert!(
joiner_brief.contains("project: Upper Product"),
"brief from the joined folder must name the product above, not \
itself, once its own .vivac/ is gone:\n{joiner_brief}"
);
assert!(
already_planted(&target.0),
"the joined tree must stay intact"
);
let (target_brief, target_brief_code) = run_in(&target.0, target.global_home(), &["brief"]);
assert_eq!(target_brief_code, 0, "{target_brief}");
assert!(
target_brief.contains("project: Upper Product"),
"the tree's own folder must still resolve to itself:\n{target_brief}"
);
}
#[test]
fn init_refuses_a_second_map() {
let c = Sandbox::new_empty("init-second-map");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
let (setup_out, setup_code) = run_in(&first, c.global_home(), &["init", "--yes"]);
assert_eq!(setup_code, 0, "{setup_out}");
let second = c.0.join("Prod-fork");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("Planting another tree would give this product two maps."),
"{out}"
);
assert!(
!already_planted(&second),
"a tree was planted despite the guard"
);
}
#[test]
fn init_new_tree_bypasses_the_second_map_guard() {
let c = Sandbox::new_empty("init-new-tree");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Prod-fork");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--new-tree", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(already_planted(&second));
}
#[test]
fn init_alone_leaves_a_complete_tree() {
let c = Sandbox::new_empty("equiv-init");
c.ok(&["init", "--yes"]);
let log = std::fs::read_to_string(c.0.join(".vivac").join("events")).unwrap();
assert!(
log.contains("\"type\":\"lane.declared\""),
"no founding lane declared: {log}"
);
let config = std::fs::read_to_string(c.0.join(".vivac").join("config")).unwrap();
assert!(
config.contains("this tree holds lanes"),
"the version lock was not set: {config}"
);
assert!(
!c.0.join(".vivac").join("lane").is_file(),
"the tree's own root folder should carry no lane file"
);
}
#[test]
fn init_dry_run_and_yes_together_is_a_usage_error() {
let c = Sandbox::new_empty("init-dry-run-yes-exclusive");
let (out, code) = c.run(&["init", "--dry-run", "--yes"]);
assert_eq!(code, 2, "{out}");
assert!(
out.contains("there is nothing for --yes to confirm"),
"{out}"
);
}
#[test]
fn init_join_with_no_value_refuses_rather_than_planting() {
let c = Sandbox::new_empty("init-join-no-value");
let (out, code) = c.run(&["init", "--join"]);
assert_eq!(code, 2, "{out}");
assert!(
out.contains("Without that word init plants instead of joining"),
"{out}"
);
assert!(
!already_planted(&c.0),
"a tree was planted despite the missing --join value"
);
}
fn names_no_harness(out: &str) {
assert!(!out.contains("claude-code"), "{out}");
assert!(!out.contains("codex"), "{out}");
}
#[test]
fn the_second_map_refusal_names_no_harness() {
let c = Sandbox::new_empty("init-guardian-second-map");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Prod-fork");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
names_no_harness(&out);
}
#[test]
fn join_to_a_target_with_no_tree_yet_names_no_harness() {
let c = Sandbox::new_empty("init-guardian-join-no-tree");
let target = c.0.join("target");
std::fs::create_dir_all(&target).unwrap();
let here = c.0.join("here");
std::fs::create_dir_all(&here).unwrap();
let (out, code) = run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", target.to_str().unwrap()],
);
assert_eq!(code, 1, "{out}");
assert!(out.contains("has no tree yet"), "{out}");
names_no_harness(&out);
}
#[test]
fn joining_an_existing_tree_points_at_migrating_this_folders_own_knowledge() {
let c = Sandbox::new_empty("init-join-migrate");
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", &target_str],
);
assert_eq!(code, 0, "{out}");
assert!(
out.contains("This folder's own knowledge is not in the tree"),
"{out}"
);
assert!(
out.contains("Use the vivac-migrate skill to bring everything this project knows"),
"{out}"
);
assert!(
!out.contains("Nothing has been brought in from anywhere yet"),
"the plant's own paragraph showed up on a join:\n{out}"
);
}
#[test]
fn planting_a_fresh_tree_still_carries_the_plants_own_migrate_paragraph() {
let c = Sandbox::new_empty("init-plant-migrate");
let (out, code) = c.run(&["init", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(
out.contains("Nothing has been brought in from anywhere yet"),
"{out}"
);
assert!(
!out.contains("This folder's own knowledge is not in the tree"),
"{out}"
);
}
fn real_git_repo(at: &Path) {
real_git_repo_with_content(at, "x");
}
fn real_git_repo_with_content(at: &Path, content: &str) {
std::fs::create_dir_all(at).unwrap();
let run = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(at)
.args(args)
.output()
.unwrap();
};
run(&["init", "-q"]);
run(&["config", "user.email", "t@example.com"]);
run(&["config", "user.name", "t"]);
std::fs::write(at.join("f.txt"), content).unwrap();
run(&["add", "."]);
run(&["commit", "-q", "-m", "first"]);
}
#[cfg(windows)]
fn remove_git_fixture(dir: &Path) {
fn clear_read_only(dir: &Path) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
clear_read_only(&path);
} else if let Ok(metadata) = std::fs::metadata(&path) {
let mut perms = metadata.permissions();
if perms.readonly() {
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
let _ = std::fs::set_permissions(&path, perms);
}
}
}
}
clear_read_only(dir);
std::fs::remove_dir_all(dir).ok();
}
#[cfg(not(windows))]
fn remove_git_fixture(dir: &Path) {
std::fs::remove_dir_all(dir).ok();
}
fn clone_repo(src: &Path, destination: &Path) {
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let status = std::process::Command::new("git")
.args(["clone", "-q"])
.arg(src)
.arg(destination)
.status()
.unwrap();
assert!(
status.success(),
"git clone of {src:?} into {destination:?} failed"
);
}
#[test]
fn a_tree_directly_below_refuses_and_writes_nothing() {
let c = Sandbox::new_empty("setup-below-one");
let below = c.0.join("Backend v2");
std::fs::create_dir_all(&below).unwrap();
run_in(&below, c.global_home(), &["init", "--yes"]);
let events_before = std::fs::read_to_string(below.join(".vivac").join("events")).unwrap();
let (out, code) = c.run(&["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("There is already a tree inside this folder, in \"Backend v2\"."),
"{out}"
);
assert!(
out.contains("Planting another one here would split this project: sessions opened in"),
"{out}"
);
assert!(
out.contains("\"Backend v2\" would use that one, and the rest this one."),
"{out}"
);
assert!(
out.contains("Move that tree up here, then run init again. From inside \"Backend v2\":"),
"{out}"
);
assert!(out.contains("vivac relocate .."), "{out}");
assert!(!c.0.join(".vivac").exists(), "a tree was planted above");
assert_eq!(
events_before,
std::fs::read_to_string(below.join(".vivac").join("events")).unwrap(),
"the tree below gained another event"
);
}
#[test]
fn two_trees_below_refuse_with_the_plural_text() {
let c = Sandbox::new_empty("setup-below-two");
let a = c.0.join("Backend v2");
let b = c.0.join("Web Ova");
std::fs::create_dir_all(&a).unwrap();
std::fs::create_dir_all(&b).unwrap();
run_in(&a, c.global_home(), &["init", "--yes"]);
run_in(&b, c.global_home(), &["init", "--yes"]);
let (out, code) = c.run(&["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("There are trees inside this folder, in \"Backend v2\" and \"Web Ova\"."),
"{out}"
);
assert!(
out.contains("vivac cannot merge trees: keep one per product, move it up here with"),
"{out}"
);
assert!(
out.contains("vivac relocate, and leave the others as they are."),
"{out}"
);
assert!(!c.0.join(".vivac").exists());
}
#[test]
fn a_shared_root_commit_refuses_naming_this_folders_own_repos() {
let c = Sandbox::new_empty("setup-registered-basic");
let first = c.0.join("IQuorum");
real_git_repo(&first.join("webapi"));
let (setup_out, setup_code) = run_in(&first, c.global_home(), &["init", "--yes"]);
assert_eq!(setup_code, 0, "{setup_out}");
let second = c.0.join("IQuorum-v2");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("Some repositories here are already tracked by project \"IQuorum\":"),
"{out}"
);
assert!(out.contains("webapi"), "{out}");
assert!(
out.contains("Planting another tree would give this product two maps."),
"{out}"
);
assert!(
out.contains("To work on IQuorum from this folder:"),
"{out}"
);
assert!(out.contains("vivac init --join IQuorum"), "{out}");
assert!(out.contains("To plant a separate tree anyway:"), "{out}");
assert!(out.contains("vivac init --new-tree"), "{out}");
assert!(!already_planted(&second));
}
#[test]
fn one_shared_repository_is_enough_even_with_an_extra_one() {
let c = Sandbox::new_empty("setup-registered-partial");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Prod-v2");
clone_repo(&first.join("webapi"), &second.join("webapi"));
real_git_repo(&second.join("infra"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("Some repositories here are already tracked by project \"Prod\":"),
"{out}"
);
assert!(!already_planted(&second));
}
#[test]
fn a_registered_products_withheld_name_points_at_the_path_remedy() {
let secret_name = "someone@example.com";
let c = Sandbox::new_empty("setup-registered-withheld");
let first = c.0.join(secret_name);
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Prod-v3");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
!out.contains(secret_name),
"the withheld name leaked: {out}"
);
assert!(
out.contains("Some repositories here are already tracked by another project on this"),
"{out}"
);
assert!(out.contains("machine:\n webapi\n"), "{out}");
assert!(
out.contains("Planting another tree would give this product two maps."),
"{out}"
);
assert!(
out.contains("To work on it from this folder, give the path to its folder:"),
"{out}"
);
assert!(
out.contains("vivac init --join <path to that folder>"),
"{out}"
);
assert!(out.contains("To plant a separate tree anyway:"), "{out}");
assert!(out.contains("vivac init --new-tree"), "{out}");
}
#[test]
fn the_refusal_names_relocate_before_new_tree() {
let c = Sandbox::new_empty("setup-registered-relocate-remedy");
let first = c.0.join("IQuorum");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("IQuorum-v2");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("If the tree should live here instead, run this in the folder that holds it:"),
"{out}"
);
assert!(
out.contains("vivac relocate <path to this folder>"),
"{out}"
);
let relocate_at = out.find("vivac relocate").expect("relocate remedy missing");
let new_tree_at = out
.find("vivac init --new-tree")
.expect("new-tree remedy missing");
assert!(
relocate_at < new_tree_at,
"relocate must be named before --new-tree: {out}"
);
}
#[test]
fn the_withheld_name_refusal_also_names_relocate_before_new_tree() {
let secret_name = "someone@example.com";
let c = Sandbox::new_empty("setup-registered-relocate-remedy-withheld");
let first = c.0.join(secret_name);
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Prod-v3");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("If the tree should live here instead, run this in the folder that holds it:"),
"{out}"
);
assert!(
out.contains("vivac relocate <path to this folder>"),
"{out}"
);
let relocate_at = out.find("vivac relocate").expect("relocate remedy missing");
let new_tree_at = out
.find("vivac init --new-tree")
.expect("new-tree remedy missing");
assert!(
relocate_at < new_tree_at,
"relocate must be named before --new-tree: {out}"
);
}
#[test]
fn a_repository_that_is_the_folder_itself_is_named_rather_than_a_bare_dot() {
let c = Sandbox::new_empty("setup-registered-dot");
let first = c.0.join("IQuorum");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("IQuorum-v2");
clone_repo(&first.join("webapi"), &second);
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(out.contains("this folder itself"), "{out}");
assert!(
!out.lines().any(|l| l.trim() == ".."),
"a line read only \"..\":\n{out}"
);
}
#[test]
fn planting_beside_a_registered_product_that_shares_nothing_warns_in_the_plan() {
let c = Sandbox::new_empty("setup-second-map-hint");
let first = c.0.join("IQuorum");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Unrelated");
real_git_repo_with_content(&second.join("app"), "y");
let (out, code) = run_in(&second, c.global_home(), &["init", "--dry-run"]);
assert_eq!(code, 0, "{out}");
assert!(out.contains("This plants a new product."), "{out}");
assert!(
out.contains("Nothing here shares a repository with the\n projects vivac already tracks"),
"{out}"
);
assert!(
out.contains("If it is, stop and use --join <name> instead."),
"{out}"
);
}
#[test]
fn planting_with_an_empty_registry_carries_no_second_map_hint() {
let c = Sandbox::new_empty("setup-second-map-hint-empty");
real_git_repo(&c.0.join("app"));
let (out, code) = c.run(&["init", "--dry-run"]);
assert_eq!(code, 0, "{out}");
assert!(!out.contains("This plants a new product."), "{out}");
}
#[test]
fn a_tree_above_the_joined_one_warns_but_still_completes() {
let c = Sandbox::new_empty("setup-above-warning");
let work = c.0.join("Work");
let mid = work.join("T");
let f = mid.join("sub");
std::fs::create_dir_all(&f).unwrap();
run_in(&work, c.global_home(), &["init", "--yes"]);
common::plant_undeclared(&mid, "setup-above-warning-mid");
let (out, code) = run_in(&f, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(
out.contains("This tree sits inside another one, in folder \"Work\"."),
"{out}"
);
assert!(
out.contains("above this folder use that one: keep one tree per product."),
"{out}"
);
assert!(
f.join(".vivac").join("lane").exists(),
"the subfolder never joined the closer tree"
);
}
#[test]
fn a_product_the_registry_never_learned_about_is_not_recognized() {
let c = Sandbox::new_empty("setup-registered-cold-start");
let first = c.0.join("Untouched");
real_git_repo(&first.join("webapi"));
let second = c.0.join("Untouched-v2");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(
already_planted(&second),
"the known limitation: a fresh tree still gets planted"
);
}
#[test]
fn a_tree_below_wins_over_a_registered_product() {
let c = Sandbox::new_empty("setup-order");
let other_root = std::env::temp_dir().join(format!(
"vivac-setup-order-other-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
real_git_repo(&other_root.join("webapi"));
let (other_out, other_code) = run_in(&other_root, c.global_home(), &["init", "--yes"]);
assert_eq!(
other_code, 0,
"the already-registered project never got set up: {other_out}"
);
clone_repo(&other_root.join("webapi"), &c.0.join("webapi"));
let below = c.0.join("Backend v2");
std::fs::create_dir_all(&below).unwrap();
run_in(&below, c.global_home(), &["init", "--yes"]);
let (out, code) = c.run(&["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("There is already a tree inside this folder, in \"Backend v2\"."),
"{out}"
);
assert!(
!out.contains("already tracked by project"),
"the product-registered refusal must not win here: {out}"
);
std::fs::remove_dir_all(below.join(".vivac")).unwrap();
let (out2, code2) = c.run(&["init", "--yes"]);
assert_eq!(code2, 1, "{out2}");
assert!(out2.contains("already tracked by project"), "{out2}");
remove_git_fixture(&other_root);
}
fn shell_split(line: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
for c in line.chars() {
match c {
'"' => in_quotes = !in_quotes,
c if c.is_whitespace() && !in_quotes => {
if !current.is_empty() {
args.push(std::mem::take(&mut current));
}
}
c => current.push(c),
}
}
if !current.is_empty() {
args.push(current);
}
args
}
#[test]
fn join_by_name_declares_a_lane_and_signs_writes_with_it() {
let c = Sandbox::new_empty("setup-join-name");
let target = c.0.join("IQuorum");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let log_at_target = std::fs::read_to_string(target.join(".vivac").join("events")).unwrap();
let declared_before = log_at_target.matches("\"type\":\"lane.declared\"").count();
let here = c.0.join("IQuorum-v2");
std::fs::create_dir_all(&here).unwrap();
let (out, code) = run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", "IQuorum"],
);
assert_eq!(code, 0, "{out}");
assert!(
here.join(".vivac").join("lane").exists(),
"no lane file appeared in the joining folder"
);
let log_before = std::fs::read_to_string(target.join(".vivac").join("events")).unwrap();
assert_eq!(
log_before.matches("\"type\":\"lane.declared\"").count(),
declared_before + 1,
"the join did not add a second lane.declared of its own: {log_before}"
);
assert!(
log_before.contains("\"name\":\"IQuorum-v2\""),
"the joining folder's own lane was never declared under its own name: {log_before}"
);
let (push_out, push_code) = run_in(
&here,
c.global_home(),
&["push", "Work from the joined folder", "--why", "seed"],
);
assert_eq!(push_code, 0, "{push_out}");
let lane_id = lane_id_of(&here);
assert_ne!(lane_id, "main", "the joined folder signs as main itself");
let log_after = std::fs::read_to_string(target.join(".vivac").join("events")).unwrap();
assert!(
log_after.contains(&format!("\"lane\":\"{lane_id}\"")),
"the write did not sign with the joined lane:\n{log_after}"
);
}
#[test]
fn lane_name_names_the_lane_over_join_too() {
let c = Sandbox::new_empty("setup-join-lane-name");
let target = c.0.join("IQuorum");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("IQuorum-v2");
std::fs::create_dir_all(&here).unwrap();
let (out, code) = run_in(
&here,
c.global_home(),
&[
"init",
"--yes",
"--join",
"IQuorum",
"--lane-name",
"custom-lane",
],
);
assert_eq!(code, 0, "{out}");
let log = std::fs::read_to_string(target.join(".vivac").join("events")).unwrap();
assert!(log.contains("\"name\":\"custom-lane\""), "{log}");
}
#[test]
fn join_by_path_works_the_same_way() {
let c = Sandbox::new_empty("setup-join-path");
let target = c.0.join("Prod");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("Prod-v2");
std::fs::create_dir_all(&here).unwrap();
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", &target_str],
);
assert_eq!(code, 0, "{out}");
assert!(here.join(".vivac").join("lane").exists());
let (push_out, push_code) = run_in(
&here,
c.global_home(),
&["push", "Work from the path-joined folder", "--why", "seed"],
);
assert_eq!(push_code, 0, "{push_out}");
let lane_id = lane_id_of(&here);
assert_ne!(lane_id, "main", "the joined folder signs as main itself");
let log_after = std::fs::read_to_string(target.join(".vivac").join("events")).unwrap();
assert!(
log_after.contains(&format!("\"lane\":\"{lane_id}\"")),
"the write did not sign with the joined lane:\n{log_after}"
);
}
fn run_in_split(dir: &Path, home: &Path, args: &[&str]) -> (String, String, i32) {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_vivac"))
.current_dir(dir)
.env("VIVAC_HOME", home)
.args(args)
.output()
.unwrap();
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.code().unwrap_or(-1),
)
}
#[test]
fn join_to_a_folder_that_is_itself_a_copy_warns_on_stderr() {
let c = Sandbox::new_empty("setup-join-copy");
let original = c.0.join("orig");
std::fs::create_dir_all(&original).unwrap();
run_in(&original, c.global_home(), &["init", "--yes"]);
run_in(
&original,
c.global_home(),
&["push", "a goal", "--why", "so the log has a first event"],
);
let copy = c.0.join("copy");
std::fs::create_dir_all(copy.join(".vivac")).unwrap();
std::fs::copy(
original.join(".vivac").join("events"),
copy.join(".vivac").join("events"),
)
.unwrap();
run_in(©, c.global_home(), &["brief"]);
let here = c.0.join("third");
std::fs::create_dir_all(&here).unwrap();
let copy_str = copy.to_string_lossy().into_owned();
let (stdout, stderr, code) = run_in_split(
&here,
c.global_home(),
&["init", "--yes", "--join", ©_str],
);
assert_eq!(code, 0, "{stdout}{stderr}");
assert!(
stderr.contains("COPY OF ANOTHER TREE"),
"join to a known copy never warned:\n{stderr}"
);
assert!(
!stdout.contains("COPY OF ANOTHER TREE"),
"the warning leaked into stdout:\n{stdout}"
);
}
#[test]
fn setup_planting_in_a_folder_that_is_a_copy_warns_on_stderr() {
let c = Sandbox::new_empty("setup-plant-copy");
let original = c.0.join("orig");
std::fs::create_dir_all(&original).unwrap();
common::plant_undeclared(&original, "setup-plant-copy");
run_in(
&original,
c.global_home(),
&["push", "a goal", "--why", "so the log has a first event"],
);
let copy = c.0.join("copy");
std::fs::create_dir_all(copy.join(".vivac")).unwrap();
std::fs::copy(
original.join(".vivac").join("events"),
copy.join(".vivac").join("events"),
)
.unwrap();
let before = read(©.join(".vivac").join("events")).lines().count();
let (stdout, stderr, code) = run_in_split(©, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 0, "{stdout}{stderr}");
assert!(
read(©.join(".vivac").join("events")).lines().count() > before,
"the write this warning reports on never happened:\n{stdout}"
);
assert!(
stderr.contains("COPY OF ANOTHER TREE"),
"planting into a copy never warned:\n{stderr}"
);
assert!(
!stdout.contains("COPY OF ANOTHER TREE"),
"the warning leaked into stdout:\n{stdout}"
);
}
#[test]
fn join_to_a_folder_with_no_tree_refuses() {
let c = Sandbox::new_empty("setup-join-no-tree");
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let empty_target = c.0.join("NoTreeHere").to_string_lossy().into_owned();
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", &empty_target]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("has no tree yet, so there is nothing to join."),
"{out}"
);
assert!(!here.join(".vivac").exists());
}
#[test]
fn a_project_name_the_registry_does_not_know_is_said_to_be_unknown() {
let c = Sandbox::new_empty("setup-join-unknown-name");
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let (out, code) = run_in(
&here,
c.global_home(),
&["init", "--join", "no-such-project"],
);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("No project named no-such-project in the registry."),
"{out}"
);
assert!(out.contains("vivac vivacs"), "{out}");
assert!(
!out.contains("has no tree yet, so there is nothing to join."),
"the folder-shaped text is still shown to an unknown name:\n{out}"
);
assert!(!here.join(".vivac").exists());
}
#[test]
fn join_from_a_folder_already_a_lane_of_another_tree_refuses() {
let c = Sandbox::new_empty("setup-join-already-lane");
let a = c.0.join("A");
std::fs::create_dir_all(&a).unwrap();
run_in(&a, c.global_home(), &["init", "--yes"]);
let b = c.0.join("B");
std::fs::create_dir_all(&b).unwrap();
run_in(&b, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let (join_out, join_code) = run_in(&here, c.global_home(), &["init", "--yes", "--join", "A"]);
assert_eq!(join_code, 0, "{join_out}");
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", "B"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("This folder is already a lane of another tree."),
"{out}"
);
}
#[test]
fn a_folder_under_a_tree_is_refused_for_the_tree_above_not_a_lane_it_has_not_got() {
let c = Sandbox::new_empty("setup-join-under-a-tree");
let above = c.0.join("Above");
let sub = above.join("Sub");
std::fs::create_dir_all(&sub).unwrap();
run_in(&above, c.global_home(), &["init", "--yes"]);
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let (out, code) = run_in(&sub, c.global_home(), &["init", "--join", "T"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("A tree sits above this folder, in \"Above\","),
"{out}"
);
assert!(
!out.contains("already a lane of another tree"),
"this folder carries no lane of anybody's: {out}"
);
assert!(
!sub.join(".vivac").exists(),
"a refused join must write nothing here"
);
}
#[test]
fn join_from_the_folder_that_holds_its_own_tree_names_it_correctly() {
let c = Sandbox::new_empty("setup-join-own-tree");
let here = c.0.join("HasATree");
std::fs::create_dir_all(&here).unwrap();
run_in(&here, c.global_home(), &["init", "--yes"]);
let other = c.0.join("Other");
std::fs::create_dir_all(&other).unwrap();
run_in(&other, c.global_home(), &["init", "--yes"]);
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", "Other"]);
assert_eq!(code, 1, "{out}");
assert!(out.contains("This folder holds a tree of its own"), "{out}");
assert!(
!out.contains("already a lane of another tree"),
"the wrong text is still shown: {out}"
);
}
#[test]
fn a_tree_below_refuses_even_when_there_is_one_above() {
let c = Sandbox::new_empty("setup-below-and-above");
let work = c.0.join("Work");
let f = work.join("F");
let nested = f.join("Nested");
std::fs::create_dir_all(&nested).unwrap();
run_in(&work, c.global_home(), &["init", "--yes"]);
common::plant_undeclared(&nested, "setup-below-and-above-nested");
let (out, code) = run_in(&f, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("There is already a tree inside this folder, in \"Nested\"."),
"{out}"
);
assert!(
!f.join(".vivac").exists(),
"F must not have joined Work despite the tree below"
);
}
#[test]
fn a_join_with_another_tree_below_names_the_choice_not_the_other_remedy() {
let c = Sandbox::new_empty("setup-below-and-join");
let target = c.0.join("T");
let f = c.0.join("F");
let nested = f.join("Nested");
std::fs::create_dir_all(&target).unwrap();
std::fs::create_dir_all(&nested).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
run_in(&nested, c.global_home(), &["init", "--yes"]);
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&f, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("There is another product's tree below this folder:"),
"{out}"
);
assert!(out.contains("Nested"), "{out}");
assert!(
!out.contains("There is already a tree inside this folder"),
"the plant-only text is still shown to a join:\n{out}"
);
assert!(
out.contains(&format!("cannot be a lane of \"{target_str}\"")),
"{out}"
);
assert!(
out.contains("move it up. From inside Nested:"),
"the remedy does not name the folder to run it from:\n{out}"
);
assert!(
!out.contains("vivac relocate Nested"),
"relocate's own argument is a destination, not the tree that moves:\n{out}"
);
assert!(out.contains("vivac relocate .."), "{out}");
assert!(
!f.join(".vivac").exists(),
"F must not have become a lane of T despite the tree below"
);
}
#[test]
fn several_trees_below_refuse_a_join_by_naming_all_of_them() {
let c = Sandbox::new_empty("setup-below-several-and-join");
let target = c.0.join("T");
let f = c.0.join("F");
let alpha = f.join("Alpha");
let beta = f.join("Beta");
std::fs::create_dir_all(&target).unwrap();
std::fs::create_dir_all(&alpha).unwrap();
std::fs::create_dir_all(&beta).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
run_in(&alpha, c.global_home(), &["init", "--yes"]);
run_in(&beta, c.global_home(), &["init", "--yes"]);
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&f, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("There are other products' trees below this folder:"),
"{out}"
);
assert!(out.contains(" Alpha"), "{out}");
assert!(out.contains(" Beta"), "{out}");
assert!(
out.contains(&format!(
"cannot be a lane of \"{target_str}\" while any of them is there"
)),
"{out}"
);
assert!(
out.contains(&format!(
"Any of them that belongs to \"{target_str}\" can move up, from inside it:"
)),
"{out}"
);
assert!(out.contains("vivac relocate .."), "{out}");
assert!(
out.contains("For the rest, join from a folder that does not contain them."),
"{out}"
);
assert!(
!out.contains("vivac cannot merge trees"),
"the plant-only plural text is still shown to a join:\n{out}"
);
assert!(!f.join(".vivac").exists());
}
#[test]
fn a_join_with_a_withheld_tree_below_names_neither_the_folder_nor_a_count() {
let secret_name = "someone@example.com";
let c = Sandbox::new_empty("setup-below-withheld-and-join");
let target = c.0.join("T");
let f = c.0.join("F");
let hidden = f.join(secret_name);
std::fs::create_dir_all(&target).unwrap();
std::fs::create_dir_all(&hidden).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
run_in(&hidden, c.global_home(), &["init", "--yes"]);
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&f, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(
!out.contains(secret_name),
"the withheld folder name leaked: {out}"
);
assert!(
out.contains("There is another product's tree below this folder, under a name this tool"),
"{out}"
);
assert!(out.contains("will not write down."), "{out}");
assert!(
out.contains(&format!(
"cannot be a lane of \"{target_str}\" while that tree is there"
)),
"{out}"
);
assert!(
out.contains("Join from a folder that does not contain it, or move that tree up from"),
"{out}"
);
assert!(out.contains("inside it: vivac relocate .."), "{out}");
assert!(
!out.contains("From inside"),
"a folder this tool will not name cannot be pointed at: {out}"
);
assert!(!f.join(".vivac").exists());
}
#[test]
fn several_withheld_trees_below_refuse_a_join_naming_none_of_them() {
let first_name = "first@example.com";
let second_name = "second@example.com";
let c = Sandbox::new_empty("setup-below-several-withheld-and-join");
let target = c.0.join("T");
let f = c.0.join("F");
let first = f.join(first_name);
let second = f.join(second_name);
std::fs::create_dir_all(&target).unwrap();
std::fs::create_dir_all(&first).unwrap();
std::fs::create_dir_all(&second).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
run_in(&first, c.global_home(), &["init", "--yes"]);
run_in(&second, c.global_home(), &["init", "--yes"]);
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&f, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(
!out.contains(first_name) && !out.contains(second_name),
"a withheld folder name leaked: {out}"
);
assert!(
out.contains("There are other products' trees below this folder, under names this tool"),
"{out}"
);
assert!(out.contains("will not write down."), "{out}");
assert!(
out.contains(&format!(
"cannot be a lane of \"{target_str}\" while any of them is there"
)),
"{out}"
);
assert!(
out.contains("Join from a folder that does not contain them, or move them up from"),
"{out}"
);
assert!(
out.contains("inside each one: vivac relocate .."),
"{out}"
);
assert!(!f.join(".vivac").exists());
}
#[test]
fn a_join_with_some_trees_below_withheld_lists_only_the_ones_it_can_show() {
let secret_name = "someone@example.com";
let c = Sandbox::new_empty("setup-below-mixed-withheld-and-join");
let target = c.0.join("T");
let f = c.0.join("F");
let hidden = f.join(secret_name);
let visible = f.join("Visible");
std::fs::create_dir_all(&target).unwrap();
std::fs::create_dir_all(&hidden).unwrap();
std::fs::create_dir_all(&visible).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
run_in(&hidden, c.global_home(), &["init", "--yes"]);
run_in(&visible, c.global_home(), &["init", "--yes"]);
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&f, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(
!out.contains(secret_name),
"the withheld folder name leaked: {out}"
);
assert!(
!out.contains("under names this tool will not write down"),
"one route is visible, so this is not the all-withheld text:\n{out}"
);
assert!(
out.contains(&format!(
"cannot be a lane of \"{target_str}\" while any of them is there"
)),
"{out}"
);
let header = "There are other products' trees below this folder:\n";
let after_header = out
.split_once(header)
.map(|(_, rest)| rest)
.unwrap_or_else(|| panic!("the plural, visible-routes header is missing:\n{out}"));
let listing = after_header
.split_once("\n\n")
.map(|(list, _)| list)
.unwrap_or_else(|| panic!("no blank line after the route list:\n{out}"));
assert_eq!(
listing, " Visible",
"the list must name only the visible route, nothing else and no count:\n{out}"
);
assert!(!f.join(".vivac").exists());
}
#[test]
fn the_join_remedy_names_the_folder_to_run_it_from_not_a_destination() {
let c = Sandbox::new_empty("setup-below-remedy-direction");
let target = c.0.join("T");
let f = c.0.join("F");
let deep = f.join("Group").join("Sub");
std::fs::create_dir_all(&target).unwrap();
std::fs::create_dir_all(&deep).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
run_in(&deep, c.global_home(), &["init", "--yes"]);
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&f, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(out.contains(" Group/Sub"), "{out}");
assert!(
out.contains("move it up. From inside Group/Sub:"),
"the remedy does not say where to stand:\n{out}"
);
assert!(
out.contains("vivac relocate .."),
"relocate's own argument has to be the destination, not the tree below:\n{out}"
);
assert!(
!out.contains("vivac relocate Group/Sub"),
"the old remedy pointed relocate at the tree below as if it were a destination:\n{out}"
);
assert!(!f.join(".vivac").exists());
}
#[test]
fn a_relative_join_target_is_recorded_as_an_absolute_path() {
let c = Sandbox::new_empty("setup-join-relative-target");
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
let sub = here.join("sub");
std::fs::create_dir_all(&sub).unwrap();
let (out, code) = run_in(
&sub,
c.global_home(),
&["init", "--yes", "--join", "../../T"],
);
assert_eq!(code, 0, "{out}");
let registry = std::fs::read_to_string(c.global_home().join("projects")).unwrap();
assert!(
!registry.contains(".."),
"a relative path leaked into the machine registry: {registry}"
);
let nested = sub.join("deeper");
std::fs::create_dir_all(&nested).unwrap();
let (brief_out, brief_code) = run_in(&nested, c.global_home(), &["brief"]);
assert_eq!(brief_code, 0, "{brief_out}");
}
#[test]
fn join_refuses_in_the_home_folder_too() {
let c = Sandbox::new_empty("setup-join-home");
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let (out, code) = run_with_home(&c.0, &c.0, c.global_home(), &["init", "--join", "T"]);
assert_eq!(code, 1, "{out}");
assert!(out.contains(&home_folder_text(&printed(&c.0))), "{out}");
assert!(!c.0.join(".vivac").join("lane").exists());
}
#[test]
fn join_to_a_tree_with_no_events_yet_wraps_and_names_nothing() {
let c = Sandbox::new_empty("setup-join-no-events");
let target = c.0.join("Empty");
std::fs::create_dir_all(target.join(".vivac")).unwrap();
std::fs::write(target.join(".vivac").join("events"), "").unwrap();
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("That tree has no events yet, so there is nothing to join:"),
"{out}"
);
assert!(
!out.contains(&target_str),
"the path was echoed back into the message: {out}"
);
assert!(!here.join(".vivac").exists());
}
const ALREADY_THAT_TREE: &str =
"This folder is already a lane of that tree, and init changed nothing in it.";
const LANE_NAME_LEFT: &str = "The lane name it already has was left as it is.";
fn join_state(here: &Path, target: &Path, home: &Path) -> (String, String, String) {
(
lane_id_of(here),
read(&target.join(".vivac").join("events")),
read(&home.join("projects")),
)
}
#[test]
fn a_second_join_of_the_same_tree_leaves_the_stack_alone() {
let c = Sandbox::new_empty("setup-join-again-stack");
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let target_str = target.to_string_lossy().into_owned();
let (first_out, first_code) = run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", &target_str],
);
assert_eq!(first_code, 0, "{first_out}");
let (push_out, push_code) = run_in(
&here,
c.global_home(),
&["push", "Work from the joined folder", "--why", "seed"],
);
assert_eq!(push_code, 0, "{push_out}");
let (stack_before, stack_code) = run_in(&here, c.global_home(), &["stack"]);
assert_eq!(stack_code, 0, "{stack_before}");
assert!(
stack_before.contains("Work from the joined folder"),
"the fixture never had a stack to lose:\n{stack_before}"
);
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 0, "{out}");
let (stack_after, stack_code) = run_in(&here, c.global_home(), &["stack"]);
assert_eq!(stack_code, 0, "{stack_after}");
assert!(
stack_after.contains("Work from the joined folder"),
"the second join took this folder's stack with it:\n{stack_after}"
);
}
#[test]
fn a_second_join_of_the_same_tree_writes_nothing() {
let c = Sandbox::new_empty("setup-join-again-writes");
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let target_str = target.to_string_lossy().into_owned();
let (first_out, first_code) = run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", &target_str],
);
assert_eq!(first_code, 0, "{first_out}");
let before = join_state(&here, &target, c.global_home());
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 0, "{out}");
assert!(out.contains(ALREADY_THAT_TREE), "{out}");
assert!(
!out.contains("This folder is now a lane"),
"a second join still claims it just joined:\n{out}"
);
assert!(
!out.contains(LANE_NAME_LEFT),
"nobody asked for a lane name, so there is nothing to report:\n{out}"
);
let after = join_state(&here, &target, c.global_home());
assert_eq!(before.0, after.0, "the lane id changed under the folder");
assert_eq!(
before.1, after.1,
"the target tree's own log gained an event"
);
assert_eq!(before.2, after.2, "the machine registry changed");
}
#[test]
fn a_second_join_with_another_lane_name_changes_nothing_and_says_so() {
let c = Sandbox::new_empty("setup-join-again-lane-name");
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let target_str = target.to_string_lossy().into_owned();
let (first_out, first_code) = run_in(
&here,
c.global_home(),
&[
"init",
"--yes",
"--join",
&target_str,
"--lane-name",
"first-name",
],
);
assert_eq!(first_code, 0, "{first_out}");
let before = join_state(&here, &target, c.global_home());
assert!(
before.1.contains("\"name\":\"first-name\""),
"the fixture never got the name it joined under:\n{}",
before.1
);
let (same_out, same_code) = run_in(
&here,
c.global_home(),
&["init", "--join", &target_str, "--lane-name", "first-name"],
);
assert_eq!(same_code, 0, "{same_out}");
assert!(same_out.contains(ALREADY_THAT_TREE), "{same_out}");
assert!(
!same_out.contains(LANE_NAME_LEFT),
"the name asked for is the name it has, so nothing was left behind:\n{same_out}"
);
let (out, code) = run_in(
&here,
c.global_home(),
&["init", "--join", &target_str, "--lane-name", "other-name"],
);
assert_eq!(code, 0, "{out}");
assert!(out.contains(ALREADY_THAT_TREE), "{out}");
assert!(
out.contains(LANE_NAME_LEFT),
"a name was asked for and not taken, in silence:\n{out}"
);
let after = join_state(&here, &target, c.global_home());
assert_eq!(before.0, after.0, "the lane id changed under the folder");
assert_eq!(before.1, after.1, "the target tree's own log changed");
assert_eq!(before.2, after.2, "the machine registry changed");
assert!(
!after.1.contains("other-name"),
"the name it was told to leave alone reached the log anyway:\n{}",
after.1
);
}
#[test]
fn a_join_to_a_different_tree_is_still_refused_and_a_first_join_still_works() {
let c = Sandbox::new_empty("setup-join-again-negative");
let a = c.0.join("A");
std::fs::create_dir_all(&a).unwrap();
run_in(&a, c.global_home(), &["init", "--yes"]);
let b = c.0.join("B");
std::fs::create_dir_all(&b).unwrap();
run_in(&b, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let (first_out, first_code) = run_in(&here, c.global_home(), &["init", "--yes", "--join", "A"]);
assert_eq!(first_code, 0, "{first_out}");
assert!(
first_out.contains("Written."),
"a first join stopped saying what it did:\n{first_out}"
);
assert!(
!first_out.contains(ALREADY_THAT_TREE),
"a first join answered as though it had nothing to do:\n{first_out}"
);
assert!(here.join(".vivac").join("lane").exists());
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", "B"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("This folder is already a lane of another tree."),
"{out}"
);
assert!(
!out.contains(ALREADY_THAT_TREE),
"another tree was answered as though it were the same one:\n{out}"
);
}
#[test]
fn a_join_of_a_folder_that_is_both_the_tree_and_its_own_lane_says_so() {
let c = Sandbox::new_seeded("setup-join-self-lane");
let real_project = {
let log = std::fs::read_to_string(c.0.join(".vivac").join("events")).unwrap();
let first: serde_json::Value = serde_json::from_str(log.lines().next().unwrap()).unwrap();
first["id"].as_str().unwrap().to_string()
};
c.append_raw_line(
r#"{"seq":2,"id":"01SEEDSELFJOINAAAAAAAAAAAA","ts":"2026-01-01T00:00:00Z","actor":"a_test0000000","lane":"main","payload":{"type":"lane.claimed","lane":"main"}}"#,
);
std::fs::write(
c.0.join(".vivac").join("lane"),
format!(r#"{{"version":1,"id":"main","project":"{real_project}"}}"#),
)
.unwrap();
let here = c.0.to_string_lossy().into_owned();
let (out, code) = c.run(&["init", "--join", &here]);
assert_eq!(code, 0, "{out}");
assert!(out.contains(ALREADY_THAT_TREE), "{out}");
}
#[test]
fn lane_name_names_main_when_planting_fresh_too() {
let c = Sandbox::new_empty("setup-lane-name-fresh-plant");
let (out, code) = c.run(&["init", "--yes", "--lane-name", "custom-name"]);
assert_eq!(code, 0, "{out}");
let log = c.log();
assert!(log.contains("\"name\":\"custom-name\""), "{log}");
}
#[test]
fn new_tree_plants_despite_a_shared_root_commit() {
let c = Sandbox::new_empty("setup-new-tree");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Prod-fork");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (refused_out, refused_code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(refused_code, 1, "{refused_out}");
assert!(
refused_out.contains("already tracked by project"),
"{refused_out}"
);
let (out, code) = run_in(&second, c.global_home(), &["init", "--new-tree", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(already_planted(&second));
}
#[test]
fn join_and_new_tree_together_is_a_usage_error() {
let c = Sandbox::new_empty("setup-join-new-tree-exclusive");
let (out, code) = c.run(&["init", "--join", "X", "--new-tree"]);
assert_eq!(code, 2, "{out}");
assert!(
out.contains("--join joins a tree that already exists, and --new-tree plants a"),
"{out}"
);
}
#[test]
fn join_with_no_value_refuses_instead_of_planting() {
let c = Sandbox::new_empty("setup-join-no-value");
let (out, code) = c.run(&["init", "--join"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--join"), "{out}");
assert!(
!already_planted(&c.0),
"it planted a tree instead of refusing:\n{out}"
);
}
#[test]
fn lane_name_names_the_lane() {
let c = Sandbox::new_seeded("setup-lane-name-ok");
let second = c.0.join("v2");
std::fs::create_dir_all(&second).unwrap();
let (out, code) = run_in(
&second,
c.global_home(),
&["init", "--yes", "--lane-name", "custom-name"],
);
assert_eq!(code, 0, "{out}");
let log = std::fs::read_to_string(c.0.join(".vivac").join("events")).unwrap();
assert!(log.contains("\"name\":\"custom-name\""), "{log}");
}
#[test]
fn a_lane_name_the_guard_rejects_falls_back_without_failing() {
let secret = "ghp_16C7e42F292c6912E7710c838347Ae178B4a";
let c = Sandbox::new_seeded("setup-lane-name-guard");
let second = c.0.join("v2");
std::fs::create_dir_all(&second).unwrap();
let (out, code) = run_in(
&second,
c.global_home(),
&["init", "--yes", "--lane-name", secret],
);
assert_eq!(code, 0, "{out}");
let log = std::fs::read_to_string(c.0.join(".vivac").join("events")).unwrap();
assert!(!log.contains(secret), "the secret leaked whole: {log}");
assert!(
!log.contains("16C7e42F292c6912E7710c838347Ae178B4a"),
"a fragment of the secret leaked: {log}"
);
let lane_id = lane_id_of(&second);
let reserve_name = format!("lane-{}", &lane_id[..lane_id.len().min(4)]);
assert!(
log.contains(&format!("\"name\":\"{reserve_name}\"")),
"the reserve name never appeared, so no fallback is proven: {log}"
);
}
#[test]
fn the_join_command_printed_by_the_refusal_actually_works() {
let c = Sandbox::new_empty("setup-close-the-loop");
let first = c.0.join("IQ Suite");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("IQ-Suite-v2");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 1, "{out}");
let join_line = out
.lines()
.find(|l| l.trim_start().starts_with("vivac init --join"))
.unwrap_or_else(|| panic!("no --join command line in the refusal:\n{out}"));
assert!(
join_line.contains("\"IQ Suite\""),
"the printed command did not quote the name with a space: {join_line}"
);
let words = shell_split(join_line.trim());
let mut cli_args: Vec<&str> = words[1..].iter().map(String::as_str).collect();
cli_args.push("--yes");
let (join_out, join_code) = run_in(&second, c.global_home(), &cli_args);
assert_eq!(join_code, 0, "{join_out}");
assert!(second.join(".vivac").join("lane").exists());
}
#[test]
fn setup_names_the_founding_lane_after_its_own_folder() {
let c = Sandbox::new_empty("setup-founding-lane-folder-name");
let here = c.0.join("webapi");
std::fs::create_dir_all(&here).unwrap();
let (out, code) = run_in(&here, c.global_home(), &["init", "--yes"]);
assert_eq!(code, 0, "{out}");
let log = std::fs::read_to_string(here.join(".vivac").join("events")).unwrap();
assert!(
log.contains("\"type\":\"lane.declared\",\"lane\":\"main\",\"name\":\"webapi\""),
"{log}"
);
}
#[test]
fn name_plants_the_product_and_the_brief_shows_it() {
let c = Sandbox::new_empty("setup-name-plants");
let (out, code) = c.run(&["init", "--yes", "--name", "IQuorum"]);
assert_eq!(code, 0, "{out}");
let registry = std::fs::read_to_string(c.global_home().join("projects")).unwrap();
assert!(registry.contains("\"name\": \"IQuorum\""), "{registry}");
let brief = c.ok(&["brief"]);
let header = brief.lines().next().unwrap_or("");
assert!(header.contains("project: IQuorum"), "{header}");
}
#[test]
fn a_saved_name_wins_over_a_moved_folder() {
let c = Sandbox::new_empty("setup-name-precedence");
c.ok(&["init", "--yes", "--name", "IQuorum"]);
let moved = c.0.parent().unwrap().join("setup-name-precedence-moved");
std::fs::rename(&c.0, &moved).unwrap();
let (out, code) = run_in(&moved, c.global_home(), &["brief"]);
assert_eq!(code, 0, "{out}");
let header = out.lines().next().unwrap_or("");
assert!(header.contains("project: IQuorum"), "{header}");
assert!(
!header.contains("setup-name-precedence-moved"),
"the folder's new name overrode the name fixed on purpose: {header}"
);
std::fs::remove_dir_all(&moved).ok();
}
#[test]
fn no_name_leaves_the_registry_and_the_header_exactly_as_before() {
let c = Sandbox::new_empty("setup-no-name-regression");
c.ok(&["init", "--yes"]);
let registry = std::fs::read_to_string(c.global_home().join("projects")).unwrap();
assert!(!registry.contains("\"name\""), "{registry}");
let from_folder = c.0.file_name().unwrap().to_string_lossy().into_owned();
let brief = c.ok(&["brief"]);
let header = brief.lines().next().unwrap_or("");
assert!(
header.contains(&format!("project: {from_folder}")),
"{header}"
);
}
#[test]
fn name_with_join_refuses_before_writing_anything() {
let c = Sandbox::new_empty("setup-name-with-join");
let before = list(&c.0);
let (out, code) = c.run(&["init", "--yes", "--join", "somewhere", "--name", "X"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--join"), "{out}");
assert!(out.contains("--name"), "{out}");
assert_eq!(list(&c.0), before, "the disk changed");
assert!(!c.0.join(".vivac").exists());
}
#[test]
fn name_with_undo_refuses_before_writing_anything() {
let c = Sandbox::new_empty("setup-name-with-undo");
let before = list(&c.0);
let (out, code) = c.run(&["init", "--undo", "--name", "X"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--undo"), "{out}");
assert!(out.contains("--name"), "{out}");
assert_eq!(list(&c.0), before, "the disk changed");
}
#[test]
fn name_is_rejected_when_not_planting() {
let c = Sandbox::new_empty("setup-name-not-planting");
let root = c.0.join("root");
std::fs::create_dir_all(&root).unwrap();
run_in(&root, c.global_home(), &["init", "--yes"]);
let sub = root.join("sub");
std::fs::create_dir_all(&sub).unwrap();
let (out, code) = run_in(&sub, c.global_home(), &["init", "--yes", "--name", "X"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--name"), "{out}");
assert!(!sub.join(".vivac").exists());
}
#[test]
fn name_with_no_value_refuses() {
let c = Sandbox::new_empty("setup-name-no-value");
let (out, code) = c.run(&["init", "--yes", "--name"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("--name"), "{out}");
assert!(!c.0.join(".vivac").exists());
}
#[test]
fn name_the_guard_rejects_refuses_and_writes_nothing() {
let c = Sandbox::new_empty("setup-name-guard-rejects");
let (out, code) = c.run(&[
"init",
"--yes",
"--name",
"sk-ant-api03-abcdefghijklmnopqrstuvwxyz012345",
]);
assert_eq!(code, 3, "{out}");
assert!(out.contains("Refused"), "{out}");
assert!(!c.0.join(".vivac").exists());
}
#[test]
fn name_shape_is_checked_before_the_guard() {
let c = Sandbox::new_empty("setup-name-shape");
let (out, code) = c.run(&["init", "--yes", "--name", " "]);
assert_eq!(code, 2, "{out}");
assert!(!c.0.join(".vivac").exists());
let too_long = "x".repeat(101);
let d = Sandbox::new_empty("setup-name-shape-long");
let (out, code) = d.run(&["init", "--yes", "--name", &too_long]);
assert_eq!(code, 2, "{out}");
assert!(!d.0.join(".vivac").exists());
}
#[test]
fn a_second_folder_sharing_repos_is_refused_with_the_fixed_name_and_join_works() {
let c = Sandbox::new_empty("setup-name-sharing-refusal");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
run_in(
&first,
c.global_home(),
&["init", "--yes", "--name", "IQuorum"],
);
let second = c.0.join("Prod-fork");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (refused_out, refused_code) = run_in(&second, c.global_home(), &["init", "--yes"]);
assert_eq!(refused_code, 1, "{refused_out}");
assert!(
refused_out.contains("already tracked by project \"IQuorum\""),
"{refused_out}"
);
assert!(
refused_out.contains("vivac init --join IQuorum"),
"{refused_out}"
);
let (out, code) = run_in(
&second,
c.global_home(),
&["init", "--yes", "--join", "IQuorum"],
);
assert_eq!(code, 0, "{out}");
assert!(second.join(".vivac").join("lane").is_file());
}
#[test]
fn the_plan_names_the_product_when_planting_fresh() {
let c = Sandbox::new_empty("setup-name-plan-fresh");
let (out, code) = c.run(&["init", "--yes", "--name", "IQuorum"]);
assert_eq!(code, 0, "{out}");
let lane_name = c.0.file_name().unwrap().to_string_lossy().into_owned();
assert!(
lane_line_containing(
&out,
".vivac/events",
&format!(
"record: this folder is lane \"{lane_name}\" of \"IQuorum\", with its \
repositories"
),
),
"{out}"
);
}
#[test]
fn the_plan_names_the_product_with_new_tree_and_name() {
let c = Sandbox::new_empty("setup-name-plan-new-tree");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
run_in(&first, c.global_home(), &["init", "--yes"]);
let second = c.0.join("Prod-fork");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(
&second,
c.global_home(),
&["init", "--new-tree", "--yes", "--name", "Fork"],
);
assert_eq!(code, 0, "{out}");
let lane_name = second.file_name().unwrap().to_string_lossy().into_owned();
assert!(
lane_line_containing(
&out,
".vivac/events",
&format!(
"record: this folder is lane \"{lane_name}\" of \"Fork\", with its \
repositories"
),
),
"{out}"
);
}
#[test]
fn name_that_collides_with_another_project_warns_in_the_plan_and_still_writes() {
let c = Sandbox::new_empty("setup-name-collision");
let first = c.0.join("first");
std::fs::create_dir_all(&first).unwrap();
run_in(
&first,
c.global_home(),
&["init", "--yes", "--name", "IQuorum"],
);
let second = c.0.join("second");
std::fs::create_dir_all(&second).unwrap();
let (out, code) = run_in(
&second,
c.global_home(),
&["init", "--yes", "--name", "IQuorum"],
);
assert_eq!(code, 0, "{out}");
assert!(
out.contains("\"IQuorum\" already names another project on this machine"),
"{out}"
);
assert!(already_planted(&second));
let registry = std::fs::read_to_string(c.global_home().join("projects")).unwrap();
assert_eq!(
registry.matches("\"name\": \"IQuorum\"").count(),
2,
"{registry}"
);
let (join_out, join_code) = run_in(
&second,
c.global_home(),
&["init", "--yes", "--join", "IQuorum"],
);
assert_eq!(join_code, 2, "{join_out}");
assert!(
join_out.contains("names 2 projects on this machine"),
"{join_out}"
);
}
#[test]
fn setup_name_collision_wraps_a_long_name_rather_than_running_past_the_width() {
let c = Sandbox::new_empty("setup-name-collision-width");
let first = c.0.join("first");
std::fs::create_dir_all(&first).unwrap();
let long = "A Name Chosen On Purpose To Run Longer Than One Line Of The Plan Could Hold";
run_in(&first, c.global_home(), &["init", "--yes", "--name", long]);
let second = c.0.join("second");
std::fs::create_dir_all(&second).unwrap();
let (out, code) = run_in(
&second,
c.global_home(),
&["init", "--dry-run", "--name", long],
);
assert_eq!(code, 0, "{out}");
assert!(out.contains("already names another project"), "{out}");
const SUB_LINE_INDENT: usize = 8;
for line in out.lines() {
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
if indent == SUB_LINE_INDENT || trimmed.starts_with("vivac init") {
continue;
}
assert!(
line.chars().count() <= 76,
"a plan line ran past 76 columns: {line:?}\nfull output:\n{out}"
);
}
}
#[test]
fn name_with_no_collision_shows_no_warning() {
let c = Sandbox::new_empty("setup-name-no-collision");
let (out, code) = c.run(&["init", "--yes", "--name", "SoloProject"]);
assert_eq!(code, 0, "{out}");
assert!(!out.contains("already names another project"), "{out}");
}
#[test]
fn join_with_no_terminal_and_no_yes_refuses_without_writing() {
let c = Sandbox::new_empty("setup-join-no-terminal");
let target = c.0.join("T");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("F");
std::fs::create_dir_all(&here).unwrap();
let target_str = target.to_string_lossy().into_owned();
let (out, code) = run_in(&here, c.global_home(), &["init", "--join", &target_str]);
assert_eq!(code, 1, "{out}");
assert!(out.contains("there is no terminal here to ask"), "{out}");
assert!(
out.contains(&format!("vivac init --join {target_str} --dry-run")),
"{out}"
);
assert!(
out.contains(&format!("vivac init --join {target_str} --yes")),
"{out}"
);
assert!(!here.join(".vivac").exists(), "the lane was written anyway");
assert!(
!here.join(".claude").exists(),
"the harness was written anyway"
);
}
#[test]
fn undo_after_a_join_that_wrote_nothing_removes_the_lane_file_and_unblocks_relocate() {
let c = Sandbox::new_empty("setup-undo-lane-unwritten");
let target = c.0.join("Target");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("Joiner");
std::fs::create_dir_all(&here).unwrap();
run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", "Target"],
);
assert!(here.join(".vivac").join("lane").exists());
let (out, code) = run_in(&here, c.global_home(), &["init", "--undo", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(out.contains("remove this folder's lane"), "{out}");
assert!(
!here.join(".vivac").join("lane").exists(),
"the lane file must be gone once its lane never wrote"
);
let (relocate_out, relocate_code) = run_in(
&target,
c.global_home(),
&["relocate", here.to_str().unwrap()],
);
assert_eq!(relocate_code, 0, "{relocate_out}");
}
#[test]
fn undo_after_a_join_that_wrote_something_keeps_the_lane_file() {
let c = Sandbox::new_empty("setup-undo-lane-written");
let target = c.0.join("Target");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("Joiner");
std::fs::create_dir_all(&here).unwrap();
run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", "Target"],
);
run_in(
&here,
c.global_home(),
&["push", "work from the joined folder", "--why", "seed"],
);
let (out, code) = run_in(&here, c.global_home(), &["init", "--undo", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(
plan_words(&out).contains(
"left as it is: this lane has written to the tree, and removing it would orphan \
what it wrote"
),
"{out}"
);
assert!(
here.join(".vivac").join("lane").exists(),
"the lane file must stay once its lane has written"
);
}
#[test]
fn undo_leaves_a_vivac_dir_whose_gitignore_was_hand_edited() {
let c = Sandbox::new_empty("setup-undo-vivac-dir-hand-edited-gitignore");
let target = c.0.join("Target");
std::fs::create_dir_all(&target).unwrap();
run_in(&target, c.global_home(), &["init", "--yes"]);
let here = c.0.join("Joiner");
std::fs::create_dir_all(&here).unwrap();
run_in(
&here,
c.global_home(),
&["init", "--yes", "--join", "Target"],
);
let gitignore = here.join(".vivac").join(".gitignore");
let mut contents = read(&gitignore);
contents.push_str("!keep-me\n");
std::fs::write(&gitignore, &contents).unwrap();
let (out, code) = run_in(&here, c.global_home(), &["init", "--undo", "--yes"]);
assert_eq!(code, 0, "{out}");
assert!(
plan_words(&out).contains("remove this folder's lane"),
"{out}"
);
assert!(
plan_words(&out).contains("left as it is: it holds more than this lane"),
"{out}"
);
assert!(
!here.join(".vivac").join("lane").exists(),
"the lane file must still go once its lane never wrote"
);
assert!(
here.join(".vivac").exists(),
"the folder must stay: its .gitignore carries a line this tool never wrote"
);
assert_eq!(
read(&gitignore),
contents,
"the hand-added line must survive"
);
}
#[test]
fn bare_init_with_no_terminal_and_no_yes_refuses_and_writes_nothing() {
let c = Sandbox::new_empty("f721-bare-no-terminal");
real_git_repo(&c.0);
std::process::Command::new("git")
.arg("-C")
.arg(&c.0)
.args([
"remote",
"add",
"origin",
"https://example.invalid/f721.git",
])
.output()
.unwrap();
let (out, code) = c.run(&["init"]);
assert_ne!(code, 0, "{out}");
assert!(out.contains("--yes"), "{out}");
assert!(
!c.0.join(".vivac").join("events").exists(),
"a refused bare init must not write the log:\n{out}"
);
}
#[test]
fn the_seeded_helper_hands_back_a_complete_tree_not_a_bare_one() {
let c = Sandbox::new_seeded("f721-seeded-helper-complete");
let log = std::fs::read_to_string(c.0.join(".vivac").join("events")).unwrap();
assert!(
log.contains("\"type\":\"lane.declared\""),
"no founding lane declared: {log}"
);
let config = std::fs::read_to_string(c.0.join(".vivac").join("config")).unwrap();
assert!(
config.contains("this tree holds lanes"),
"the version lock was not set: {config}"
);
}
#[test]
fn a_bare_init_on_a_clone_of_an_already_registered_product_is_refused() {
let c = Sandbox::new_empty("f721-bare-second-map");
let first = c.0.join("Prod");
real_git_repo(&first.join("webapi"));
let (setup_out, setup_code) = run_in(&first, c.global_home(), &["init", "--yes"]);
assert_eq!(setup_code, 0, "{setup_out}");
let second = c.0.join("Prod-fork");
clone_repo(&first.join("webapi"), &second.join("webapi"));
let (out, code) = run_in(&second, c.global_home(), &["init"]);
assert_eq!(code, 1, "{out}");
assert!(
out.contains("Planting another tree would give this product two maps."),
"{out}"
);
assert!(
!already_planted(&second),
"a bare init must not have planted a second tree"
);
}