use std::path::Path;
use super::{
REGISTRY_DIR_NAME, dispatched_agent_worktree_enabled, registry_data_dir_under,
worktree_enabled_for_origin, worktree_enabled_for_origin_at, worktree_enabled_for_project,
worktree_enabled_in, worktree_override_in_project,
};
use crate::project::{Project, ProjectRegistry};
fn project(name: &str, repo_url: &str, worktree: Option<bool>) -> Project {
Project {
name: name.into(),
repo_url: repo_url.into(),
default_branch: "main".into(),
stack_hint: None,
tags: vec![],
description: None,
gh_user: None,
gh_account: None,
github: None,
commit_name: None,
commit_email: None,
worktree,
}
}
#[test]
fn worktree_enabled_in_defaults_true() {
let projects = vec![project(
"writing",
"https://github.com/bobmatnyc/writing",
Some(false),
)];
assert!(
worktree_enabled_in(&projects, "https://github.com/acme/unregistered"),
"an unmatched origin must default to worktree isolation ON"
);
assert!(
worktree_enabled_in(&[], "https://github.com/acme/anything"),
"an EMPTY registry must default to worktree isolation ON"
);
}
#[test]
fn worktree_enabled_in_honors_false() {
let opted_out = vec![project(
"cto",
"https://github.com/bob-duetto/cto",
Some(false),
)];
assert!(
!worktree_enabled_in(&opted_out, "https://github.com/bob-duetto/cto"),
"worktree: false must disable isolation for the matched project"
);
let unset = vec![project("cto", "https://github.com/bob-duetto/cto", None)];
assert!(
worktree_enabled_in(&unset, "https://github.com/bob-duetto/cto"),
"an absent `worktree` key must resolve to ON (unwrap_or(true))"
);
let opted_in = vec![project(
"cto",
"https://github.com/bob-duetto/cto",
Some(true),
)];
assert!(
worktree_enabled_in(&opted_in, "https://github.com/bob-duetto/cto"),
"worktree: true must resolve to ON"
);
}
#[test]
fn worktree_enabled_in_matches_ssh_form() {
let projects = vec![project(
"writing",
"https://github.com/bobmatnyc/writing",
Some(false),
)];
for origin in [
"git@github.com:bobmatnyc/writing.git",
"https://github.com/bobmatnyc/writing.git",
"https://github.com/BobMatNYC/Writing",
] {
assert!(
!worktree_enabled_in(&projects, origin),
"{origin} must match the registered project's opt-out"
);
}
assert!(
worktree_enabled_in(&projects, "https://github.com/bobmatnyc/trusty-tools"),
"a DIFFERENT repo must be unaffected by another project's opt-out"
);
}
#[tokio::test]
async fn worktree_enabled_for_origin_defaults_true_when_unregistered() {
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
assert!(
worktree_enabled_for_origin(®istry, "https://github.com/acme/unregistered").await,
"an unregistered repo must default to worktree isolation ON"
);
}
#[tokio::test]
async fn worktree_enabled_for_origin_honors_registered_false() {
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"writing",
"https://github.com/bobmatnyc/writing",
Some(false),
))
.await
.expect("register writing project");
assert!(
!worktree_enabled_for_origin(®istry, "https://github.com/bobmatnyc/writing.git").await,
"the registered project's opt-out must be honored (repo_url matching tolerates .git suffix)"
);
assert!(
worktree_enabled_for_origin(®istry, "https://github.com/bobmatnyc/trusty-tools").await,
"a DIFFERENT repo must be unaffected by another project's opt-out"
);
}
#[tokio::test]
async fn worktree_enabled_at_reads_registry_from_disk() {
let dir = crate::test_support::hermetic_temp_dir();
{
let writer = ProjectRegistry::load(dir.path())
.await
.expect("load writer");
writer
.register(project(
"writing",
"https://github.com/bobmatnyc/writing",
Some(false),
))
.await
.expect("register");
writer
.register(project(
"trusty-tools",
"https://github.com/bobmatnyc/trusty-tools",
None,
))
.await
.expect("register");
}
assert!(
!worktree_enabled_for_origin_at(dir.path(), "git@github.com:bobmatnyc/writing.git").await,
"a reader with no daemon state must see the on-disk opt-out"
);
assert!(
worktree_enabled_for_origin_at(dir.path(), "https://github.com/bobmatnyc/trusty-tools")
.await,
"a project with no `worktree` key must still resolve to ON"
);
}
#[tokio::test]
async fn worktree_enabled_at_defaults_true_when_registry_absent() {
let dir = crate::test_support::hermetic_temp_dir();
let missing = dir.path().join("does-not-exist");
assert!(
!missing.exists(),
"fixture precondition: {} must not exist",
missing.display()
);
assert!(
worktree_enabled_for_origin_at(&missing, "https://github.com/bobmatnyc/writing").await,
"an absent registry must never disable worktree isolation"
);
}
#[tokio::test]
async fn worktree_enabled_at_defaults_true_on_malformed_registry() {
let dir = crate::test_support::hermetic_temp_dir();
std::fs::write(dir.path().join("projects.json"), "{ not json").expect("write malformed");
assert!(
worktree_enabled_for_origin_at(dir.path(), "https://github.com/bobmatnyc/writing").await,
"a malformed registry must default to worktree isolation ON"
);
}
#[test]
fn registry_dir_name_is_frozen() {
assert_eq!(
REGISTRY_DIR_NAME, "project-registry",
"renaming this orphans every existing projects.json on disk"
);
let root = Path::new("/tmp/tm-test-framework-root/.trusty-mpm");
let resolved = registry_data_dir_under(root);
assert_eq!(
resolved.strip_prefix(root).ok(),
Some(Path::new("project-registry")),
"the registry dir must be a single component directly under the \
framework root, got {}",
resolved.display()
);
}
fn project_dir_with_config(body: &str) -> tempfile::TempDir {
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path()
.join(crate::core::project_config::PROJECT_CONFIG_FILE),
body,
)
.expect("write project config");
dir
}
#[tokio::test]
async fn worktree_project_config_overrides_registry() {
let proj = project_dir_with_config("worktree = false\n");
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"widget",
"https://github.com/acme/widget",
Some(true),
))
.await
.expect("register");
assert!(
!worktree_enabled_for_project(proj.path(), ®istry, "https://github.com/acme/widget")
.await,
"the project's committed config must outrank the machine-global registry"
);
}
#[tokio::test]
async fn worktree_project_config_can_force_isolation_on() {
let proj = project_dir_with_config("worktree = true\n");
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"widget",
"https://github.com/acme/widget",
Some(false),
))
.await
.expect("register");
assert!(
worktree_enabled_for_project(proj.path(), ®istry, "https://github.com/acme/widget")
.await,
"a project that requires isolation must override a local registry opt-out"
);
}
#[tokio::test]
async fn worktree_project_config_absent_falls_back_to_registry() {
let proj = tempfile::TempDir::new().expect("tempdir");
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"writing",
"https://github.com/bobmatnyc/writing",
Some(false),
))
.await
.expect("register");
assert!(
!worktree_enabled_for_project(
proj.path(),
®istry,
"https://github.com/bobmatnyc/writing"
)
.await,
"with no project config the registry opt-out must still apply"
);
}
#[tokio::test]
async fn worktree_defaults_true_with_neither_layer() {
let proj = tempfile::TempDir::new().expect("tempdir");
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
assert!(
worktree_enabled_for_project(
proj.path(),
®istry,
"https://github.com/acme/unregistered"
)
.await,
"neither layer deciding must leave the built-in `true` default intact"
);
}
#[tokio::test]
async fn worktree_project_config_malformed_falls_back_to_registry() {
let proj = project_dir_with_config("worktre = false\n");
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"widget",
"https://github.com/acme/widget",
Some(true),
))
.await
.expect("register");
assert!(
worktree_enabled_for_project(proj.path(), ®istry, "https://github.com/acme/widget")
.await,
"a rejected project config must contribute nothing, leaving the registry to decide"
);
assert_eq!(
worktree_override_in_project(proj.path()),
None,
"a file that fails deny_unknown_fields must yield no override at all"
);
}
#[tokio::test]
async fn worktree_clone_branch_resolves_without_a_project_dir() {
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"writing",
"https://github.com/bobmatnyc/writing",
Some(false),
))
.await
.expect("register");
assert!(
!worktree_enabled_for_origin(®istry, "https://github.com/bobmatnyc/writing").await,
"the clone-based branch must keep resolving from the registry alone"
);
let missing = dir.path().join("not-cloned-yet");
assert!(
!worktree_enabled_for_project(&missing, ®istry, "https://github.com/bobmatnyc/writing")
.await,
"a non-existent project dir must fall through to the registry"
);
}
#[tokio::test]
async fn agent_isolation_stays_enabled_for_a_worktree_true_project() {
let dir = crate::test_support::hermetic_temp_dir();
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"isolated",
"https://github.com/acme/isolated",
Some(true),
))
.await
.expect("register");
registry
.register(project(
"on-main",
"https://github.com/acme/on-main",
Some(false),
))
.await
.expect("register");
let proj = tempfile::TempDir::new().expect("tempdir");
assert!(
worktree_enabled_for_project(proj.path(), ®istry, "https://github.com/acme/isolated")
.await,
"#5274 row 4: an explicit `worktree: true` must keep agent isolation ON"
);
assert!(
worktree_enabled_for_project(proj.path(), ®istry, "https://github.com/acme/unheard-of")
.await,
"#5274 row 4: a project with no registry entry must default to agent \
isolation ON — flipping this default is the defect this test guards"
);
assert!(
!worktree_enabled_for_project(proj.path(), ®istry, "https://github.com/acme/on-main")
.await,
"`worktree: false` must still reach the main-checkout answer, or the two \
assertions above would pass for a resolver hard-wired to `true`"
);
}
#[test]
fn agent_worktree_defaults_true_without_a_config() {
let empty = tempfile::TempDir::new().expect("tempdir");
assert!(
dispatched_agent_worktree_enabled(empty.path()),
"a project with no .trusty-mpm.toml keeps ADR-0048's grant"
);
let other_keys = project_dir_with_config("default_model = \"opus\"\n");
assert!(
dispatched_agent_worktree_enabled(other_keys.path()),
"a config that declines to decide keeps ADR-0048's grant"
);
let missing = empty.path().join("never-created");
assert!(
dispatched_agent_worktree_enabled(&missing),
"a directory that does not exist keeps ADR-0048's grant"
);
}
#[test]
fn agent_worktree_opt_out_is_honoured() {
let opted_out = project_dir_with_config("agent_worktree = false\n");
assert!(!dispatched_agent_worktree_enabled(opted_out.path()));
let explicit_on = project_dir_with_config("agent_worktree = true\n");
assert!(dispatched_agent_worktree_enabled(explicit_on.path()));
}
#[test]
fn agent_worktree_malformed_config_keeps_isolation() {
for body in [
"agent_worktre = false\n", "agent_worktree = \"false\"\n", "agent_worktree = false\nthis(", ] {
let dir = project_dir_with_config(body);
assert!(
dispatched_agent_worktree_enabled(dir.path()),
"an unusable config must keep isolation ON, got OFF for: {body}"
);
}
}
#[test]
fn agent_worktree_is_independent_of_the_session_worktree_key() {
let session_opt_out = project_dir_with_config("worktree = false\n");
assert!(
dispatched_agent_worktree_enabled(session_opt_out.path()),
"`worktree = false` must not reach the dispatched-agent decision"
);
let opposed = project_dir_with_config("worktree = true\nagent_worktree = false\n");
assert!(
!dispatched_agent_worktree_enabled(opposed.path()),
"the two keys are answered independently, in both directions"
);
}