use super::*;
use crate::test_environment::Environment;
use std::path::PathBuf;
struct Moved {
workshop: wsb_prj::Workshop,
root: PathBuf,
host: tempfile::TempDir,
_prj: tempfile::TempDir,
_env: Environment,
}
fn moved() -> Moved {
let prj = tempfile::tempdir().unwrap();
let host = tempfile::tempdir().unwrap();
let root = crate::platform::canonicalize(prj.path()).unwrap();
let env = Environment::set(&[
("PRJ", &root.to_string_lossy()),
(crate::daemon::HOST_DIR_ENV, &host.path().to_string_lossy()),
]);
for project in ["demo2", "demo-api"] {
std::fs::create_dir_all(root.join(project).join(wsb_prj::MAIN_DIR)).unwrap();
}
let old_state = root.join(".state/workspaces/demo/worktrees/main");
std::fs::create_dir_all(old_state.join("logs")).unwrap();
std::fs::write(old_state.join("logs/run.jsonl"), "{}\n").unwrap();
let store = crate::store::open().unwrap();
let db = store.get_connection().unwrap();
let old_dir = root.join("demo/main-worktree").to_string_lossy().into_owned();
for worktree in ["demo/main", "demo-api/main"] {
db.execute(
"INSERT INTO tests (worktree, repo, language, function_name, file_path, line_number)
VALUES (?1, '.', 'rust', 'test1_x', 'src/lib.rs', 1)",
[worktree],
)
.unwrap();
db.execute(
"INSERT INTO workbench_state (worktree, state_json) VALUES (?1, '{\"expanded_paths\":[\"src\"]}')",
[worktree],
)
.unwrap();
}
db.execute(
"INSERT INTO worktrees (worktree_id, project, name, shared_worktree, super_root, last_seen)
VALUES ('demo:main', 'demo', 'main', 'aaaa@main', ?1, 0),
('demo-api:main', 'demo-api', 'main', 'bbbb@main', '/elsewhere', 0)",
[&old_dir],
)
.unwrap();
db.execute(
"INSERT INTO sessions (id, project, worktree, agent, cwd) VALUES
('s1', 'demo', 'main', 'claude', ?1 || '/src'),
('s2', 'demo-api', 'main', 'claude', '/elsewhere')",
[&old_dir],
)
.unwrap();
let layout = serde_json::json!({ "workbench": { "groups": [{
"id": "group:1",
"tabs": [
{ "key": "file:demo:main|src/lib.rs", "input": { "kind": "file", "worktree": "demo:main", "path": "src/lib.rs" } },
{ "key": "project:demo", "input": { "kind": "project", "project": "demo" } },
{ "key": "overview:demo-api:main", "input": { "kind": "overview", "worktree": "demo-api:main" } },
],
"active": "file:demo:main|src/lib.rs",
}]}});
db.execute(
"INSERT INTO workbench_layout (id, layout_json, updated_at) VALUES (1, ?1, '')",
[layout.to_string()],
)
.unwrap();
Moved { workshop: wsb_prj::Workshop::at(&root).unwrap(), root, host, _prj: prj, _env: env }
}
fn rows(sql: &str) -> Vec<String> {
let store = crate::store::open().unwrap();
let db = store.get_connection().unwrap();
let mut statement = db.prepare(sql).unwrap();
let mut found: Vec<String> = statement.query_map([], |row| row.get(0)).unwrap().map(Result::unwrap).collect();
found.sort();
found
}
#[test]
fn test20212_what_wsb_keeps_follows_a_renamed_project() {
let moved = moved();
let done = rename(&moved.workshop, "demo", "demo2").unwrap();
let state = moved.root.join(".state/workspaces");
assert_eq!(std::fs::read_to_string(state.join("demo2/worktrees/main/logs/run.jsonl")).unwrap(), "{}\n");
assert!(!state.join("demo").exists(), "{done:?}");
assert_eq!(rows("SELECT worktree FROM tests"), ["demo-api/main", "demo2/main"]);
assert_eq!(rows("SELECT worktree FROM workbench_state"), ["demo-api/main", "demo2/main"]);
assert_eq!(rows("SELECT worktree_id || ' ' || project FROM worktrees"), ["demo-api:main demo-api", "demo2:main demo2"]);
let new_dir = moved.root.join("demo2/main-worktree").to_string_lossy().into_owned();
assert_eq!(rows("SELECT super_root FROM worktrees WHERE project = 'demo2'"), [new_dir.clone()]);
assert_eq!(rows("SELECT project || ' ' || cwd FROM sessions WHERE id = 's1'"), [format!("demo2 {new_dir}/src")]);
assert_eq!(rows("SELECT project FROM sessions WHERE id = 's2'"), ["demo-api"]);
let layout: serde_json::Value =
serde_json::from_str(&rows("SELECT layout_json FROM workbench_layout")[0]).unwrap();
let group = &layout["workbench"]["groups"][0];
assert_eq!(group["tabs"][0]["key"], "file:demo2:main|src/lib.rs");
assert_eq!(group["tabs"][0]["input"]["worktree"], "demo2:main");
assert_eq!(group["tabs"][1]["key"], "project:demo2");
assert_eq!(group["tabs"][1]["input"]["project"], "demo2");
assert_eq!(group["tabs"][2]["key"], "overview:demo-api:main");
assert_eq!(group["active"], "file:demo2:main|src/lib.rs");
assert_eq!(rename(&moved.workshop, "demo", "demo2").unwrap(), ["nothing is kept under demo"]);
}
#[test]
fn test20213_what_ran_under_the_new_name_first_is_merged() {
let moved = moved();
let ahead = moved.root.join(".state/workspaces/demo2/worktrees/main/logs");
std::fs::create_dir_all(&ahead).unwrap();
std::fs::write(ahead.join("since.jsonl"), "{}\n").unwrap();
{
let store = crate::store::open().unwrap();
let db = store.get_connection().unwrap();
db.execute("INSERT INTO workbench_state (worktree, state_json) VALUES ('demo2/main', '{\"expanded_paths\":[]}')", [])
.unwrap();
}
rename(&moved.workshop, "demo", "demo2").unwrap();
let logs = moved.root.join(".state/workspaces/demo2/worktrees/main/logs");
assert!(logs.join("since.jsonl").is_file() && logs.join("run.jsonl").is_file());
assert_eq!(
rows("SELECT state_json FROM workbench_state WHERE worktree = 'demo2/main'"),
["{\"expanded_paths\":[\"src\"]}"]
);
}
#[test]
fn test20214_a_rename_waits_for_the_folder_the_daemon_and_the_writers() {
let moved = moved();
let untouched = || moved.root.join(".state/workspaces/demo/worktrees/main/logs/run.jsonl").is_file();
std::fs::create_dir_all(moved.root.join("demo/main-worktree")).unwrap();
let refused = rename(&moved.workshop, "demo", "demo2").unwrap_err().to_string();
assert!(refused.contains("move its folder"), "{refused}");
std::fs::remove_dir_all(moved.root.join("demo")).unwrap();
let daemon = moved.host.path().join("daemon.json");
std::fs::write(&daemon, format!("{{\"pid\":{}}}", std::process::id())).unwrap();
let refused = rename(&moved.workshop, "demo", "demo2").unwrap_err().to_string();
assert!(refused.contains("wsb project rename demo demo2"), "{refused}");
std::fs::remove_file(&daemon).unwrap();
let writers = moved.root.join(".state/workspaces/demo/worktrees/main").join(crate::local_state::WRITERS_DIR);
std::fs::create_dir_all(&writers).unwrap();
let holder = crate::worktree_lock::Holder {
command: "wsb checkpoint".into(),
argv: Vec::new(),
pid: std::process::id(),
host: crate::worktree_lock::host_name(),
taken_at: String::new(),
reason: "committing".into(),
protects: String::new(),
};
std::fs::write(writers.join("me.json"), serde_json::to_string(&holder).unwrap()).unwrap();
let refused = rename(&moved.workshop, "demo", "demo2").unwrap_err().to_string();
assert!(refused.contains("wsb checkpoint") && refused.contains("at work"), "{refused}");
assert!(untouched());
assert_eq!(rows("SELECT worktree FROM tests WHERE worktree = 'demo/main'"), ["demo/main"]);
}