use std::path::Path;
use super::*;
use crate::session_manager::ManagedSessionId;
struct HomeGuard(Option<String>);
impl Drop for HomeGuard {
fn drop(&mut self) {
match self.0 {
Some(ref p) => unsafe { std::env::set_var("HOME", p) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
fn scratch_home() -> (tempfile::TempDir, HomeGuard) {
let home = tempfile::TempDir::new().expect("tmp home");
let dir = home.path().join(".trusty-tools").join("trusty-mpm");
std::fs::create_dir_all(&dir).expect("config dir");
std::fs::write(dir.join("config.yaml"), "disk:\n max_usage_pct: 100\n").expect("config");
let prior = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", home.path()) };
(home, HomeGuard(prior))
}
fn git(dir: &Path, args: &[&str]) -> String {
let out = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(["-c", "user.name=t", "-c", "user.email=t@example.com"])
.args(args)
.output()
.expect("git runs");
assert!(out.status.success(), "git {args:?}: {out:?}");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn repo(dir: &Path, origin: Option<&str>) {
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["commit", "-q", "--allow-empty", "-m", "init"]);
if let Some(url) = origin {
git(dir, &["remote", "add", "origin", url]);
}
}
fn params(dir: &Path, worktree: bool) -> SpawnParams {
SpawnParams {
repo_url: dir.to_string_lossy().into_owned(),
git_ref: "main".into(),
task: String::new(),
name_hint: None,
runtime: None,
ephemeral: Some(true),
mcp_initiated: false,
inject_task: None,
deliverable_id: None,
force_new: true,
worktree,
}
}
async fn isolated_state(root: &Path) -> Arc<DaemonState> {
Arc::new(DaemonState::with_root_isolated_managed(root.to_path_buf()).await)
}
#[test]
fn session_source_local_only_carries_no_github_identity() {
let source = SessionSource::local_only(Path::new("/x/supervisor"));
assert_eq!(source.source_id, None);
assert_eq!(source.repo_url, None);
assert_eq!(source.name, "supervisor");
let gh = SessionSource::github("acme", "widget");
assert_eq!(gh.source_id.as_deref(), Some("acme/widget"));
assert_eq!(
gh.repo_url.as_deref(),
Some("https://github.com/acme/widget")
);
}
#[tokio::test]
#[serial_test::serial]
async fn a_local_only_repo_spawns_on_its_main_checkout_with_gh_disabled() {
let (_home, _guard) = scratch_home();
let data = tempfile::TempDir::new().expect("data root");
let state = isolated_state(data.path()).await;
let checkout = tempfile::TempDir::new().expect("checkout");
repo(checkout.path(), None);
let record = super::super::lifecycle::spawn_managed(
&state,
ManagedSessionId::new(),
params(checkout.path(), false),
)
.await
.expect("a local-only repo spawns");
assert_eq!(record.cwd, checkout.path());
assert_eq!(record.repo_url, None, "no synthetic GitHub URL");
assert_eq!(record.source_id, None);
assert!(!checkout.path().join(".worktrees").exists());
let vars = super::super::lifecycle::resolve_gh_env(&state, checkout.path()).await;
let config_dir = vars.iter().find(|(k, _)| k == "GH_CONFIG_DIR");
assert_eq!(
config_dir.map(|(_, v)| v.as_str()),
Some(crate::core::remote_mode::LOCAL_ONLY_GH_CONFIG_DIR),
"{vars:?}"
);
}
#[tokio::test]
#[serial_test::serial]
async fn a_local_only_repo_spawns_a_worktree_from_its_local_head() {
let (_home, _guard) = scratch_home();
let data = tempfile::TempDir::new().expect("data root");
let state = isolated_state(data.path()).await;
let checkout = tempfile::TempDir::new().expect("checkout");
repo(checkout.path(), None);
let head = git(checkout.path(), &["rev-parse", "HEAD"]);
let record = super::super::lifecycle::spawn_managed(
&state,
ManagedSessionId::new(),
params(checkout.path(), true),
)
.await
.expect("a local-only worktree spawns");
let worktree = record.cwd.clone();
assert!(
worktree.starts_with(checkout.path().join(".worktrees")),
"{worktree:?}"
);
assert_eq!(git(&worktree, &["rev-parse", "HEAD"]), head);
let branch = git(&worktree, &["symbolic-ref", "--short", "HEAD"]);
assert!(branch.starts_with("session/"), "{branch}");
let status = git(checkout.path(), &["status", "--porcelain"]);
assert!(!status.contains(".worktrees"), "{status}");
let upstream = std::process::Command::new("git")
.arg("-C")
.arg(&worktree)
.args(["rev-parse", "--abbrev-ref", "@{upstream}"])
.output()
.expect("git runs");
assert!(
!upstream.status.success(),
"no upstream in a local-only repo"
);
}
#[test]
fn a_local_spawn_refusal_never_quotes_the_origin_token() {
const TOKEN: &str = "ghp_9124LocalSpawnSynthetic0000";
let id = ManagedSessionId::new();
for origin in [
format!("https://octo:{TOKEN}@example.test/"),
format!("https://octo:{TOKEN}@github.com/acme/widget.git"),
] {
let checkout = tempfile::TempDir::new().expect("checkout");
repo(checkout.path(), Some(&origin));
let msg = spawn_managed_local(&id, ¶ms(checkout.path(), false));
assert!(msg.starts_with("spawn failed"), "not a refusal: {msg}");
assert!(!msg.contains(TOKEN), "the refusal quoted the origin token");
assert!(msg.contains("https://***@"), "the origin lost its shape");
}
}
#[tokio::test]
#[serial_test::serial]
async fn a_repo_with_an_origin_is_not_routed_local_only() {
let (_home, _guard) = scratch_home();
let data = tempfile::TempDir::new().expect("data root");
let state = isolated_state(data.path()).await;
let checkout = tempfile::TempDir::new().expect("checkout");
repo(checkout.path(), Some("https://github.com/acme/widget.git"));
assert_eq!(
crate::core::remote_mode::is_local_only_root(checkout.path()),
Ok(false)
);
let vars = super::super::lifecycle::resolve_gh_env(&state, checkout.path()).await;
assert!(vars.is_empty(), "an unpinned origin is unchanged: {vars:?}");
}