use std::fs;
use std::path::Path;
use std::sync::{LazyLock, Mutex, MutexGuard};
use atelier_sdk::{Act, Actor, ActorKind, Error, GateOutcome, Instruction, SyncOutcome, Workspace};
fn env_lock() -> MutexGuard<'static, ()> {
static LOCK: LazyLock<Mutex<()>> = LazyLock::new(Mutex::default);
LOCK.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[expect(unsafe_code, reason = "set_var wires the workspace to the test config")]
fn set_actor(config_home: &Path) {
fs::create_dir_all(config_home).expect("create config home");
fs::write(
config_home.join("config.toml"),
"[actor]\nname = \"test-actor\"\nkind = \"human\"\n",
)
.expect("write actor config");
unsafe {
std::env::set_var("ATELIER_CONFIG_HOME", config_home);
}
}
fn actor() -> Actor {
Actor {
name: "scribe".to_owned(),
kind: ActorKind::Agent,
}
}
fn instruction() -> Instruction {
Instruction {
summary: "revise the folder".to_owned(),
run_ref: None,
verbatim: None,
}
}
fn acts_of(ws: &mut Workspace, act: Act) -> Vec<String> {
ws.journal(100)
.expect("read the journal")
.into_iter()
.filter(|entry| entry.act == act)
.map(|entry| entry.reference.unwrap_or_default())
.collect()
}
#[test]
fn a_landing_mirrors_the_root_import_home() {
let _guard = env_lock();
let config = tempfile::tempdir().unwrap();
set_actor(config.path());
let root = tempfile::tempdir().unwrap();
let mut ws = Workspace::init(root.path()).unwrap();
let origin = tempfile::tempdir().unwrap();
fs::write(origin.path().join("a.txt"), "a\n").unwrap();
fs::create_dir(origin.path().join("sub")).unwrap();
fs::write(origin.path().join("sub").join("old.txt"), "old\n").unwrap();
ws.attach(origin.path()).unwrap();
let session = ws.open_session(&actor(), &instruction()).unwrap();
ws.session_write(session.id, "a.txt", "a revised\n")
.unwrap();
ws.session_write(session.id, "new.txt", "brand new\n")
.unwrap();
fs::remove_file(session.working_copy.join("sub").join("old.txt")).unwrap();
let outcome = ws.land(session.id).unwrap();
let GateOutcome::Landed { landings } = outcome else {
panic!("the land must land, got {outcome:?}");
};
assert_eq!(
fs::read_to_string(origin.path().join("a.txt")).unwrap(),
"a revised\n"
);
assert_eq!(
fs::read_to_string(origin.path().join("new.txt")).unwrap(),
"brand new\n"
);
assert!(!origin.path().join("sub").exists());
assert_eq!(
acts_of(&mut ws, Act::Sync),
vec![landings[0].snapshot.clone()]
);
assert_eq!(acts_of(&mut ws, Act::SyncParked), Vec::<String>::new());
}
#[test]
fn an_origin_edited_out_of_band_parks_and_force_overwrites() {
let _guard = env_lock();
let config = tempfile::tempdir().unwrap();
set_actor(config.path());
let root = tempfile::tempdir().unwrap();
let mut ws = Workspace::init(root.path()).unwrap();
let origin = tempfile::tempdir().unwrap();
fs::write(origin.path().join("notes.txt"), "shared\n").unwrap();
ws.attach_mount(origin.path(), "docs").unwrap();
fs::write(origin.path().join("notes.txt"), "the human's version\n").unwrap();
let session = ws.open_session(&actor(), &instruction()).unwrap();
ws.session_write(session.id, "docs/notes.txt", "the session's version\n")
.unwrap();
let outcome = ws.land(session.id).unwrap();
let GateOutcome::Landed { landings } = outcome else {
panic!("the land must land, got {outcome:?}");
};
let landed = landings[1].snapshot.clone();
assert_eq!(
fs::read_to_string(origin.path().join("notes.txt")).unwrap(),
"the human's version\n"
);
assert_eq!(
acts_of(&mut ws, Act::SyncParked),
vec![format!("docs {landed} origin changed")]
);
let retry = ws.sync(Some("docs"), false).unwrap();
assert_eq!(
retry,
SyncOutcome::Parked {
snapshot: landed.clone()
}
);
assert_eq!(
fs::read_to_string(origin.path().join("notes.txt")).unwrap(),
"the human's version\n"
);
let forced = ws.sync(Some("docs"), true).unwrap();
assert_eq!(
forced,
SyncOutcome::Synced {
snapshot: landed.clone()
}
);
assert_eq!(
fs::read_to_string(origin.path().join("notes.txt")).unwrap(),
"the session's version\n"
);
assert_eq!(acts_of(&mut ws, Act::Sync), vec![format!("docs {landed}")]);
let second = ws.open_session(&actor(), &instruction()).unwrap();
ws.session_write(second.id, "docs/notes.txt", "a second revision\n")
.unwrap();
let outcome = ws.land(second.id).unwrap();
let GateOutcome::Landed { landings } = outcome else {
panic!("the second land must land, got {outcome:?}");
};
let second_landed = landings[1].snapshot.clone();
assert_eq!(
fs::read_to_string(origin.path().join("notes.txt")).unwrap(),
"a second revision\n"
);
assert_eq!(
acts_of(&mut ws, Act::Sync),
vec![format!("docs {second_landed}"), format!("docs {landed}")]
);
}
#[test]
fn git_sources_never_sync_back() {
let _guard = env_lock();
let config = tempfile::tempdir().unwrap();
set_actor(config.path());
let root = tempfile::tempdir().unwrap();
let mut ws = Workspace::init(root.path()).unwrap();
let repo = tempfile::tempdir().unwrap();
let git = |args: &[&str]| {
let output = std::process::Command::new("git")
.args(args)
.current_dir(repo.path())
.env("GIT_AUTHOR_NAME", "upstream")
.env("GIT_AUTHOR_EMAIL", "upstream@example.com")
.env("GIT_COMMITTER_NAME", "upstream")
.env("GIT_COMMITTER_EMAIL", "upstream@example.com")
.output()
.expect("run git");
assert!(output.status.success(), "git {args:?}: {output:?}");
};
git(&["init", "-q", "-b", "master", "."]);
fs::write(repo.path().join("lib.rs"), "pub fn lib() {}\n").unwrap();
git(&["add", "."]);
git(&["commit", "-qm", "the pre-attach commit"]);
ws.attach_mount(repo.path(), "sdk").unwrap();
let session = ws.open_session(&actor(), &instruction()).unwrap();
ws.session_write(session.id, "sdk/lib.rs", "pub fn lib() { push() }\n")
.unwrap();
let outcome = ws.land(session.id).unwrap();
assert!(matches!(outcome, GateOutcome::Landed { .. }), "{outcome:?}");
assert_eq!(acts_of(&mut ws, Act::Sync), Vec::<String>::new());
assert_eq!(acts_of(&mut ws, Act::SyncParked), Vec::<String>::new());
assert_eq!(
fs::read_to_string(repo.path().join("lib.rs")).unwrap(),
"pub fn lib() {}\n"
);
let error = ws.sync(Some("sdk"), false).unwrap_err();
assert!(
matches!(&error, Error::Config(message) if message.contains("git source")),
"got: {error:?}"
);
}