use super::*;
use crate::core::content_source::test_support::rc;
use super::super::deployment_check::{carrier_reachable, warn_if_no_persona_carrier};
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 set_home(home: &std::path::Path) -> HomeGuard {
let prior = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", home) };
HomeGuard(prior)
}
fn disk_threshold_home(pct: u8) -> tempfile::TempDir {
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("create config dir");
std::fs::write(
dir.join("config.yaml"),
format!("disk:\n max_usage_pct: {pct}\n"),
)
.expect("write config");
home
}
struct StubTmux {
alive: Vec<String>,
}
impl crate::session_manager::ManagedTmuxDriver for StubTmux {
fn create_session(&self, _name: &str, _workdir: &str) -> Result<(), ManagedError> {
unimplemented!("not exercised by find_reusable_inproject_session tests")
}
fn kill_session(&self, _name: &str) -> Result<(), ManagedError> {
unimplemented!("not exercised by find_reusable_inproject_session tests")
}
fn send_line(&self, _name: &str, _text: &str) -> Result<(), ManagedError> {
unimplemented!("not exercised by find_reusable_inproject_session tests")
}
fn capture(&self, _name: &str, _lines: usize) -> Result<String, ManagedError> {
unimplemented!("not exercised by find_reusable_inproject_session tests")
}
fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
Ok(self.alive.clone())
}
}
#[rustfmt::skip]
fn stub_record(
source_id: Option<&str>,
state: ManagedSessionState,
tmux_name: &str,
) -> SessionRecord {
SessionRecord {
id: ManagedSessionId::new(),
tmux_name: tmux_name.to_owned(),
cwd: std::path::PathBuf::from("/tmp/project"),
task: "task".into(),
state,
created_at: chrono::Utc::now(),
last_activity_at: None,
workspace_path: None, repo_url: None,
branch: None, pending_decision: None,
proposed_default: None, correlation: Default::default(),
runtime: Default::default(),
ephemeral: false, workspace_owned: false,
source_id: source_id.map(str::to_owned),
claude_session_id: None, scrollback_path: None,
last_cwd: None, deliverable_id: None,
pane_id: None, injection_status: Default::default(),
tmux_server: None,
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
}
}
#[test]
fn find_reusable_inproject_session_matches_active_live_session() {
let records = vec![stub_record(
Some("bobmatnyc/trusty-tools"),
ManagedSessionState::Active,
"tmpm-trusty-tools-abc123",
)];
let tmux = StubTmux {
alive: vec!["tmpm-trusty-tools-abc123".to_owned()],
};
let found = find_reusable_inproject_session(&records, "bobmatnyc/trusty-tools", &tmux);
assert!(
found.is_some(),
"an Active record with a live tmux session for the same source_id must be reused"
);
assert_eq!(found.unwrap().tmux_name, "tmpm-trusty-tools-abc123");
}
#[test]
fn find_reusable_inproject_session_ignores_stopped_or_dead_or_other_project() {
let records = vec![
stub_record(
Some("bobmatnyc/xflux"),
ManagedSessionState::Active,
"tmpm-xflux-live",
),
stub_record(
Some("bobmatnyc/trusty-tools"),
ManagedSessionState::Stopped,
"tmpm-trusty-tools-stopped",
),
stub_record(
Some("bobmatnyc/trusty-tools"),
ManagedSessionState::Active,
"tmpm-trusty-tools-dead-tmux",
),
];
let tmux = StubTmux {
alive: vec!["tmpm-xflux-live".to_owned()],
};
let found = find_reusable_inproject_session(&records, "bobmatnyc/trusty-tools", &tmux);
assert!(
found.is_none(),
"must not reuse a different project's session, a Stopped record, \
or an Active record whose tmux session is no longer alive; got: {found:?}"
);
}
#[test]
fn reconnect_candidate_none_when_force_new() {
let records = vec![stub_record(
Some("bobmatnyc/trusty-tools"),
ManagedSessionState::Active,
"tmpm-trusty-tools-abc123",
)];
let tmux = StubTmux {
alive: vec!["tmpm-trusty-tools-abc123".to_owned()],
};
let forced = reconnect_candidate(true, &records, "bobmatnyc/trusty-tools", &tmux);
assert!(
forced.is_none(),
"force_new must skip the reconnect even when a live session exists"
);
}
#[test]
fn reconnect_candidate_reconnects_when_not_forced() {
let records = vec![stub_record(
Some("bobmatnyc/trusty-tools"),
ManagedSessionState::Active,
"tmpm-trusty-tools-abc123",
)];
let tmux = StubTmux {
alive: vec!["tmpm-trusty-tools-abc123".to_owned()],
};
let found = reconnect_candidate(false, &records, "bobmatnyc/trusty-tools", &tmux);
assert_eq!(
found.map(|r| r.tmux_name),
Some("tmpm-trusty-tools-abc123".to_owned()),
"without force_new the #1707 reconnect must still adopt a live session"
);
}
#[test]
#[serial_test::serial]
fn prepare_inproject_session_writes_statusline() {
let tmp_home = tempfile::TempDir::new().expect("tmp home");
let _home = set_home(tmp_home.path());
let worktree = tempfile::TempDir::new().expect("tmp worktree");
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let session_id = ManagedSessionId::new();
prepare_inproject_session(
&fw,
&session_id,
worktree.path(),
Some("https://github.com/owner/repo"),
)
.expect("prep succeeds (#4752: only a compiled-prompt write failure is fatal)");
let settings_path = worktree.path().join(".claude").join("settings.json");
let content = std::fs::read_to_string(&settings_path).unwrap_or_else(|e| {
panic!(
"prepare_inproject_session must write {}: {e}",
settings_path.display()
)
});
assert!(
content.contains("statusLine"),
"prepared worktree settings.json must carry the statusLine key \
(the #1913 symptom); got: {content}"
);
}
#[tokio::test]
#[serial_test::serial]
async fn prepare_inproject_session_emits_stage_events_in_order() {
use crate::core::provisioning_stage::{ProvisioningStage, StageEmitter, scoped};
let tmp_home = tempfile::TempDir::new().expect("tmp home");
let _home = set_home(tmp_home.path());
let worktree = tempfile::TempDir::new().expect("tmp worktree");
let fw = crate::core::paths::FrameworkPaths::under(tmp_home.path());
let session_id = ManagedSessionId::new();
let (tx, mut rx) = tokio::sync::broadcast::channel(32);
let emitter = StageEmitter::new(session_id.to_string(), "https://github.com/owner/repo", tx);
scoped(emitter, async {
prepare_inproject_session(
&fw,
&session_id,
worktree.path(),
Some("https://github.com/owner/repo"),
)
.expect("prep succeeds (#4752: only a compiled-prompt write failure is fatal)");
})
.await;
let mut stages = Vec::new();
while let Ok(value) = rx.try_recv() {
assert_eq!(value["kind"], "provisioning_stage");
assert_eq!(value["repo_url"], "https://github.com/owner/repo");
stages.push(value["stage"].as_str().unwrap().to_string());
}
assert_eq!(
stages,
vec![
ProvisioningStage::DeployingAgents.wire_name(),
ProvisioningStage::DeployingSkills.wire_name(),
ProvisioningStage::BuildingInstructions.wire_name(),
ProvisioningStage::ConfiguringMcp.wire_name(),
],
"prepare_inproject_session's call tree must emit exactly these \
four stages, in order, when a StageEmitter scope is active"
);
}
#[tokio::test]
#[serial_test::serial]
async fn reserve_inproject_worktree_uses_semantic_name_not_uuid() {
let home = disk_threshold_home(100);
let _home = set_home(home.path());
let data_root = tempfile::TempDir::new().expect("tmp data root");
let state = std::sync::Arc::new(
crate::daemon::state::DaemonState::with_root_isolated_managed(
data_root.path().to_path_buf(),
)
.await,
);
let base_dir = tempfile::TempDir::new().expect("tmp base dir");
let base = base_dir.path().to_path_buf();
assert!(
std::process::Command::new("git")
.arg("init")
.current_dir(&base)
.status()
.map(|s| s.success())
.unwrap_or(false),
"git init must succeed in this test fixture"
);
for (k, v) in [("user.email", "t@example.com"), ("user.name", "T")] {
let _ = std::process::Command::new("git")
.args(["-C", base.to_str().unwrap(), "config", k, v])
.status();
}
assert!(
std::process::Command::new("git")
.args([
"-C",
base.to_str().unwrap(),
"commit",
"--allow-empty",
"-m",
"init",
])
.status()
.map(|s| s.success())
.unwrap_or(false),
"git commit must succeed in this test fixture"
);
let session_id = ManagedSessionId::new();
let params = SpawnParams {
repo_url: base.to_string_lossy().into_owned(),
git_ref: "main".into(),
task: "task".into(),
name_hint: None,
runtime: None,
ephemeral: Some(true),
mcp_initiated: false,
inject_task: None,
deliverable_id: None,
force_new: false,
worktree: true,
};
let config = crate::core::trusty_tools_config::TrustyToolsConfig::default();
let (worktree, reserved_name) = reserve_inproject_worktree(
&state,
&session_id,
¶ms,
&base,
&base,
"trusty-tools",
&config,
)
.await
.expect("reserve_inproject_worktree must succeed against a real git repo");
assert_eq!(
reserved_name, "tm-trusty-tools-01",
"the resolved name must be the SEMANTIC tm-<repo>-NN form, not the raw session UUID"
);
assert!(
worktree.ends_with(&reserved_name),
"the worktree path must end with the resolved semantic name, got {}",
worktree.display()
);
assert!(
!worktree.to_string_lossy().contains(&session_id.to_string()),
"the worktree path must NOT contain the raw session UUID (issue #2032), got {}",
worktree.display()
);
assert!(
worktree.is_dir(),
"the worktree directory must exist on disk, got {}",
worktree.display()
);
}
fn committed_repo_8994() -> tempfile::TempDir {
let dir = tempfile::TempDir::new().expect("tmp base dir");
let git = |args: &[&str]| {
let ok = std::process::Command::new("git")
.arg("-C")
.arg(dir.path())
.args(args)
.status()
.is_ok_and(|s| s.success());
assert!(ok, "fixture: git {args:?}");
};
git(&["init", "-q"]);
git(&["config", "user.email", "t@example.com"]);
git(&["config", "user.name", "T"]);
git(&["commit", "-q", "--allow-empty", "-m", "init"]);
dir
}
async fn reserve_8994(base: &std::path::Path) -> Result<(std::path::PathBuf, String), String> {
let data_root = tempfile::TempDir::new().expect("tmp data root");
let state = std::sync::Arc::new(
crate::daemon::state::DaemonState::with_root_isolated_managed(
data_root.path().to_path_buf(),
)
.await,
);
let params = SpawnParams {
repo_url: base.to_string_lossy().into_owned(),
git_ref: "main".into(),
task: "task".into(),
name_hint: None,
runtime: None,
ephemeral: Some(true),
mcp_initiated: false,
inject_task: None,
deliverable_id: None,
force_new: false,
worktree: true,
};
let config = crate::core::trusty_tools_config::TrustyToolsConfig::default();
let id = ManagedSessionId::new();
reserve_inproject_worktree(&state, &id, ¶ms, base, base, "trusty-tools", &config).await
}
#[tokio::test]
#[serial_test::serial]
async fn reserve_inproject_worktree_refuses_when_the_ledger_cannot_record() {
let home = disk_threshold_home(100);
let ledger = crate::core::worktree_ledger::WorktreeLedger::under_home(home.path());
std::fs::create_dir_all(ledger.path()).expect("block the ledger path");
let _home = set_home(home.path());
let base = committed_repo_8994();
let err = reserve_8994(base.path())
.await
.expect_err("an unwritable ledger must refuse the provisioning call");
assert!(
err.contains("worktree ledger"),
"the error must name the ledger: {err}"
);
let worktrees = base.path().join(".worktrees");
let created: Vec<_> = std::fs::read_dir(&worktrees)
.map(|it| it.filter_map(Result::ok).map(|e| e.path()).collect())
.unwrap_or_default();
assert!(created.is_empty(), "no worktree may exist: {created:?}");
}
#[tokio::test]
#[serial_test::serial]
async fn reserve_inproject_worktree_records_the_created_tree() {
use crate::core::worktree_ledger::{Origin, WorktreeLedger, fold::fold, ledger_key};
let home = disk_threshold_home(100);
let _home = set_home(home.path());
let base = committed_repo_8994();
let (worktree, _) = reserve_8994(base.path()).await.expect("reserve");
let read = WorktreeLedger::under_home(home.path())
.read()
.expect("ledger");
let state = fold(&read.events);
assert_eq!(state.live.len(), 1, "{state:?}");
let entry = &state.live[&ledger_key(&worktree)];
assert_eq!(entry.origin, Origin::TmDaemon);
assert_eq!(entry.repo, ledger_key(base.path()));
assert_eq!(entry.branch.as_deref(), Some("session/tm-trusty-tools-01"));
}
#[test]
fn ensure_deployment_complete_noops_for_unknown_workspace() {
let id = ManagedSessionId::new();
let fw = crate::core::paths::FrameworkPaths::under("/nonexistent-fw-base-for-test");
let result = ensure_deployment_complete(
&fw,
std::path::Path::new("/unknown"),
None,
&id,
RepairHost::default(),
);
assert!(result.is_ok());
let missing = std::path::Path::new("/this/path/does/not/exist/anywhere");
let result = ensure_deployment_complete(&fw, missing, None, &id, RepairHost::default());
assert!(result.is_ok());
}
#[test]
fn ensure_deployment_complete_ok_when_already_complete() {
use crate::core::agent_manifest::AgentManifest;
use crate::core::paths::FrameworkPaths;
use crate::core::skill_manifest::SkillManifest;
let tmp = tempfile::tempdir().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let mut fw = FrameworkPaths::for_managed_project(tmp.path(), &workspace);
fw.trusty_mpm_root = None;
let agents_dir = fw.agent_deploy_dir();
std::fs::create_dir_all(&agents_dir).unwrap();
AgentManifest::default().save(&agents_dir).unwrap();
let skills_dir = fw.skill_deploy_dir();
std::fs::create_dir_all(&skills_dir).unwrap();
SkillManifest::default().save(&skills_dir).unwrap();
let claude_dir = workspace.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"outputStyle": "trusty-mpm", "hooks": {"SessionStart": []}}"#,
)
.unwrap();
let style_dir = claude_dir.join("output-styles");
std::fs::create_dir_all(&style_dir).unwrap();
let default_style = crate::core::bundle::OUTPUT_STYLES[0];
std::fs::write(
style_dir.join(default_style.file_name),
default_style.content(rc()),
)
.unwrap();
let id = ManagedSessionId::new();
let result = ensure_deployment_complete(&fw, &workspace, None, &id, RepairHost::default());
assert!(result.is_ok(), "expected Ok, got {result:?}");
}
#[test]
fn carrier_reachable_true_when_no_output_style_gap() {
let tmp = tempfile::tempdir().unwrap();
assert!(
carrier_reachable(&[], tmp.path()),
"no output-style gap at all must be treated as carrier-reachable"
);
}
#[test]
fn carrier_reachable_true_when_prompt_file_present_despite_style_gap() {
let tmp = tempfile::tempdir().unwrap();
let stash_dir = tmp.path().join(".trusty-mpm");
std::fs::create_dir_all(&stash_dir).unwrap();
std::fs::write(stash_dir.join("last-instructions.md"), "you are the PM").unwrap();
let gaps = vec![crate::core::deploy_validate::DeploymentGap::OutputStyleKeyMissing];
assert!(
carrier_reachable(&gaps, tmp.path()),
"a present, non-empty prompt-file stash must satisfy the carrier check \
even when the output-style carrier has a gap"
);
}
#[test]
fn carrier_reachable_false_when_neither_carrier_present() {
let tmp = tempfile::tempdir().unwrap();
let gaps = vec![
crate::core::deploy_validate::DeploymentGap::OutputStyleFileMissing(
"trusty-mpm".to_string(),
),
];
assert!(
!carrier_reachable(&gaps, tmp.path()),
"an output-style gap with no prompt-file stash must be unreachable"
);
}
#[test]
fn carrier_reachable_false_when_prompt_file_present_but_empty() {
let tmp = tempfile::tempdir().unwrap();
let stash_dir = tmp.path().join(".trusty-mpm");
std::fs::create_dir_all(&stash_dir).unwrap();
std::fs::write(stash_dir.join("last-instructions.md"), "").unwrap();
let gaps = vec![
crate::core::deploy_validate::DeploymentGap::OutputStyleUnknownId("bogus".to_string()),
];
assert!(!carrier_reachable(&gaps, tmp.path()));
}
#[test]
fn warn_if_no_persona_carrier_does_not_panic_when_neither_carrier_present() {
let tmp = tempfile::tempdir().unwrap();
let gaps = vec![crate::core::deploy_validate::DeploymentGap::OutputStyleKeyMissing];
let id = ManagedSessionId::new();
warn_if_no_persona_carrier(&gaps, tmp.path(), &id);
}
#[cfg(unix)]
#[test]
#[serial_test::serial]
fn ensure_deployment_complete_does_not_abort_when_no_carrier_reachable() {
use std::os::unix::fs::PermissionsExt;
let fake_home = tempfile::tempdir().unwrap();
let _home = set_home(fake_home.path());
let tmp = tempfile::tempdir().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::set_permissions(&workspace, std::fs::Permissions::from_mode(0o555)).unwrap();
let probe = workspace.join(".probe");
if std::fs::write(&probe, "x").is_ok() {
let _ = std::fs::remove_file(&probe);
let _ = std::fs::set_permissions(&workspace, std::fs::Permissions::from_mode(0o755));
eprintln!(
"skipping ensure_deployment_complete_does_not_abort_when_no_carrier_reachable: \
read-only directory did not block a write (likely running as root)"
);
return;
}
let mut fw = crate::core::paths::FrameworkPaths::for_managed_project(tmp.path(), &workspace);
fw.trusty_mpm_root = None;
let id = ManagedSessionId::new();
let result = ensure_deployment_complete(&fw, &workspace, None, &id, RepairHost::default());
let _ = std::fs::set_permissions(&workspace, std::fs::Permissions::from_mode(0o755));
assert!(
result.is_err(),
"expected the pre-existing incomplete-after-repair contract to still hold \
(unrelated to the new carrier self-check); got {result:?}"
);
}
use super::super::launch_on_main::{has_concurrent_main_checkout_session, spawn_managed_on_main};
#[tokio::test]
#[serial_test::serial]
async fn spawn_managed_on_main_creates_record_without_worktree() {
let tmp_home = tempfile::TempDir::new().expect("tmp home");
let _home = set_home(tmp_home.path());
let data_root = tempfile::TempDir::new().expect("tmp data root");
let state = std::sync::Arc::new(
crate::daemon::state::DaemonState::with_root_isolated_managed(
data_root.path().to_path_buf(),
)
.await,
);
let checkout = tempfile::TempDir::new().expect("tmp checkout dir");
let local_path = checkout.path();
assert!(
std::process::Command::new("git")
.arg("init")
.current_dir(local_path)
.status()
.map(|s| s.success())
.unwrap_or(false),
"git init must succeed in this test fixture"
);
let session_id = ManagedSessionId::new();
let params = SpawnParams {
repo_url: local_path.to_string_lossy().into_owned(),
git_ref: "main".into(),
task: "".into(),
name_hint: None,
runtime: None,
ephemeral: Some(true),
mcp_initiated: false,
inject_task: None,
deliverable_id: None,
force_new: false,
worktree: false,
};
let record = spawn_managed_on_main(
&state,
&session_id,
¶ms,
crate::runtime::RuntimeKind::ClaudeCode,
local_path,
&super::super::local_only_spawn::SessionSource::github("acme", "writing"),
)
.await
.expect("spawn_managed_on_main must succeed against a real git repo");
assert_eq!(
record.cwd, local_path,
"the session cwd must be the main checkout itself, not a worktree"
);
assert_eq!(
record.workspace_path.as_deref(),
Some(local_path),
"workspace_path must point at the main checkout"
);
assert!(
!record.workspace_owned,
"workspace_owned must be false — decommission must never auto-delete \
the operator's own checkout"
);
assert!(
!local_path.join(".worktrees").exists(),
"no .worktrees/ directory must ever be created for a launch-on-main session"
);
assert_eq!(
record.source_id.as_deref(),
Some("acme/writing"),
"source_id must still be set so reconnect works normally"
);
}
#[tokio::test]
#[serial_test::serial]
async fn spawn_managed_on_main_never_writes_task_md_into_the_checkout() {
let tmp_home = tempfile::TempDir::new().expect("tmp home");
let _home = set_home(tmp_home.path());
let data_root = tempfile::TempDir::new().expect("tmp data root");
let state = std::sync::Arc::new(
crate::daemon::state::DaemonState::with_root_isolated_managed(
data_root.path().to_path_buf(),
)
.await,
);
let checkout = tempfile::TempDir::new().expect("tmp checkout dir");
let local_path = checkout.path();
assert!(
std::process::Command::new("git")
.arg("init")
.current_dir(local_path)
.status()
.map(|s| s.success())
.unwrap_or(false),
"git init must succeed in this test fixture"
);
let session_id = ManagedSessionId::new();
let params = SpawnParams {
repo_url: local_path.to_string_lossy().into_owned(),
git_ref: "main".into(),
task: "qa-8066 verification throwaway".into(),
name_hint: None,
runtime: None,
ephemeral: Some(true),
mcp_initiated: false,
inject_task: None,
deliverable_id: None,
force_new: false,
worktree: false,
};
spawn_managed_on_main(
&state,
&session_id,
¶ms,
crate::runtime::RuntimeKind::ClaudeCode,
local_path,
&super::super::local_only_spawn::SessionSource::github("acme", "writing"),
)
.await
.expect("spawn_managed_on_main must succeed against a real git repo");
assert!(
!local_path.join("TASK.md").exists(),
"TASK.md must never be written into a live checkout (#7879)"
);
let porcelain = std::process::Command::new("git")
.args(["status", "--porcelain"])
.current_dir(local_path)
.output()
.expect("git status must run");
let porcelain = String::from_utf8_lossy(&porcelain.stdout);
assert!(
!porcelain.contains("TASK.md"),
"the checkout's `git status --porcelain` must not gain a TASK.md entry, got:\n{porcelain}"
);
}
#[tokio::test]
#[serial_test::serial]
async fn spawn_managed_on_main_hands_the_adapter_the_prepared_reachability() {
const SRC: &str = include_str!("launch_on_main.rs");
assert!(
SRC.contains("memory_reachable,\n state.framework_root(),"),
"launch-on-main must hand the adapter the reachability preparation \
resolved, and the daemon's own framework root"
);
let tmp_home = tempfile::TempDir::new().expect("tmp home");
let _home = set_home(tmp_home.path());
let data_root = tempfile::TempDir::new().expect("tmp data root");
let state = std::sync::Arc::new(
crate::daemon::state::DaemonState::with_root_isolated_managed(
data_root.path().to_path_buf(),
)
.await,
);
let checkout = tempfile::TempDir::new().expect("tmp checkout dir");
let local_path = checkout.path();
assert!(
std::process::Command::new("git")
.arg("init")
.current_dir(local_path)
.status()
.map(|s| s.success())
.unwrap_or(false),
"git init must succeed in this test fixture"
);
let params = SpawnParams {
repo_url: local_path.to_string_lossy().into_owned(),
git_ref: "main".into(),
task: "".into(),
name_hint: None,
runtime: None,
ephemeral: Some(true),
mcp_initiated: false,
inject_task: None,
deliverable_id: None,
force_new: false,
worktree: false,
};
crate::core::memory_reachable::ADAPTER_REPROBES_ON_THIS_THREAD.with(|n| n.set(0));
spawn_managed_on_main(
&state,
&ManagedSessionId::new(),
¶ms,
crate::runtime::RuntimeKind::ClaudeCode,
local_path,
&super::super::local_only_spawn::SessionSource::github("acme", "writing"),
)
.await
.expect("spawn_managed_on_main must succeed against a real git repo");
assert_eq!(
crate::core::memory_reachable::ADAPTER_REPROBES_ON_THIS_THREAD.with(std::cell::Cell::get),
0,
"the adapter must reuse the reachability preparation resolved, not probe again"
);
}
#[test]
fn both_launch_gates_reuse_the_resolved_reachability() {
fn flat(src: &str) -> String {
src.split_whitespace().collect::<Vec<_>>().join(" ")
}
let on_main = flat(include_str!("launch_on_main.rs"));
assert!(
on_main.contains(
"ensure_deployment_complete( &fw, local_path, record.repo_url.as_deref(), \
session_id, RepairHost::new(memory_reachable, state.user_home()), )"
),
"launch-on-main must hand the gate the reachability preparation resolved"
);
let inproject = flat(include_str!("lifecycle.rs"));
assert!(
inproject.contains(
"let host = RepairHost::new(reachable, state.user_home()); \
let url = record.repo_url.as_deref(); if let Err(reason) = \
ensure_deployment_complete(&fw, &worktree, url, session_id, host)"
),
"the in-project spawn must hand the gate the reachability preparation resolved"
);
}
#[test]
fn spawn_managed_on_main_warns_on_concurrent_main_checkout_session() {
let checkout = std::path::Path::new("/Users/op/projects/writing");
let other = std::path::Path::new("/Users/op/projects/other");
let mut same = stub_record(None, ManagedSessionState::Active, "tm-writing-01");
same.cwd = checkout.to_path_buf();
let found = has_concurrent_main_checkout_session(std::slice::from_ref(&same), checkout)
.expect("an Active session sharing the exact cwd must be detected");
assert_eq!(
found.tmux_name, "tm-writing-01",
"the detector must return the colliding record so the caller can name it"
);
assert!(
has_concurrent_main_checkout_session(std::slice::from_ref(&same), other).is_none(),
"a session on a different checkout must never collide"
);
let mut stopped = stub_record(None, ManagedSessionState::Stopped, "tm-writing-02");
stopped.cwd = checkout.to_path_buf();
assert!(
has_concurrent_main_checkout_session(std::slice::from_ref(&stopped), checkout).is_none(),
"a non-Active session on the same checkout must not count as a live collision"
);
assert!(
has_concurrent_main_checkout_session(&[], checkout).is_none(),
"no sessions means no collision"
);
}