Files
max-sixty__worktrunk/tests/integration_tests/approvals.rs
Maximilian Roos 92dfb686bb feat(approvals): let wt config approvals add --yes record approvals without a TTY (#3819)
`wt config approvals add` refused every non-interactive run — even with
`--yes`, whose hint then suggested the flag already passed — so there
was no way to pre-approve a project's commands unattended. An
orchestrator (tend's Codex Cloud container was the motivating case) had
to hand-write `approvals.toml` from `wt config approvals list
--format=json` output, a third-party reimplementation of `add` that
breaks whenever the schema changes. The `wt config approvals` docs
already promised "`--yes` to bypass prompts in CI" and described `stale`
entries as "what `--yes` would silently re-approve"; behavior now
matches them.

The two `--yes` meanings stay distinct: on a command that runs project
commands it grants consent for that run alone and records nothing
(unchanged), while on `add` — whose product is the record — it lists
what it trusts and writes it. `add` no longer routes through
`approve_command_batch` (the execution gate) for this: it prompts or
announces, then saves itself, which also makes a failed `approvals.toml`
write fail the command instead of warning behind a `✓ saved` line and
exit 0 — an orchestrator reading only the exit code would otherwise walk
into the prompt it just paid to avoid.

The non-interactive hint's pre-approval suggestion now carries `--yes`
(`run wt config approvals add --yes`), since a hint reached in CI must
name a command that runs there. Per the existing `list --format=json`
docs, `add --yes` re-approves templates edited since an earlier approval
without comment; the `add` help now says so and points at the `stale`
field for reading them first, and the worktrunk skill's escalation rule
tells agents not to reach for it on a user's behalf.

> _This was written by Claude Code on behalf of max-sixty_
2026-08-14 02:39:29 -07:00

738 lines
26 KiB
Rust

//! Integration tests for the `wt config approvals` subcommands
use crate::common::{
BareRepoTest, TestRepo, TestRepoBase, make_snapshot_cmd, make_snapshot_cmd_with_global_flags,
repo, set_temp_home_env, setup_snapshot_settings, setup_snapshot_settings_with_home,
setup_temp_snapshot_settings, temp_home, wt_command,
};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::fs;
use tempfile::TempDir;
use worktrunk::config::Approvals;
/// Helper to snapshot add-approvals command
fn snapshot_add_approvals(test_name: &str, repo: &TestRepo, args: &[&str]) {
let settings = setup_snapshot_settings(repo);
settings.bind(|| {
let mut cmd = make_snapshot_cmd(repo, "config", &[], None);
cmd.arg("approvals").arg("add").args(args);
assert_cmd_snapshot!(test_name, cmd);
});
}
/// Helper to snapshot clear-approvals command
fn snapshot_clear_approvals(test_name: &str, repo: &TestRepo, args: &[&str]) {
let settings = setup_snapshot_settings(repo);
settings.bind(|| {
let mut cmd = make_snapshot_cmd(repo, "config", &[], None);
cmd.arg("approvals").arg("clear").args(args);
assert_cmd_snapshot!(test_name, cmd);
});
}
/// Snapshot the list command in both formats from one setup. The sections and
/// the JSON payload answer the same question about the same state, so every
/// scenario below covers both without restating its config.
fn snapshot_list_approvals(test_name: &str, repo: &TestRepo) {
let settings = setup_snapshot_settings(repo);
settings.bind(|| {
let mut cmd = make_snapshot_cmd(repo, "config", &[], None);
cmd.arg("approvals").arg("list");
assert_cmd_snapshot!(test_name, cmd);
let mut cmd = make_snapshot_cmd(repo, "config", &[], None);
cmd.arg("approvals").arg("list").arg("--format=json");
assert_cmd_snapshot!(format!("{test_name}_json"), cmd);
});
}
// ============================================================================
// list tests
// ============================================================================
#[rstest]
fn test_list_approvals_no_config(repo: TestRepo) {
snapshot_list_approvals("list_approvals_no_config", &repo);
}
/// The main listing: hooks, an alias, and commit-message guidance, with a mix
/// of approved and unapproved commands, plus a stale approval whose command
/// was removed from the config.
#[rstest]
fn test_list_approvals_mixed(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(
r#"pre-merge = "cargo test"
[post-start]
dev = "npm run dev"
[aliases]
deploy = "echo deploying {{ branch }}"
[commit.generation]
template-append = "Use conventional commits."
"#,
);
repo.commit("Add config");
let project_id = repo.project_id();
let mut approvals = Approvals::default();
approvals
.approve_commands(
project_id,
vec![
"cargo test".to_string(),
"echo deploying {{ branch }}".to_string(),
"some removed command".to_string(),
],
repo.test_approvals_path(),
)
.unwrap();
snapshot_list_approvals("list_approvals_mixed", &repo);
}
/// With everything approved, the UNAPPROVED section shows an explicit
/// `(none)` so the all-clear is visible at a glance.
#[rstest]
fn test_list_approvals_all_approved(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(r#"pre-merge = "cargo test""#);
repo.commit("Add config");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"cargo test".to_string(),
repo.test_approvals_path(),
)
.unwrap();
snapshot_list_approvals("list_approvals_all_approved", &repo);
}
/// Without a project config there is nothing to require approval, but
/// approvals already on record still list — all as stale.
#[rstest]
fn test_list_approvals_stale_only(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"orphan command".to_string(),
repo.test_approvals_path(),
)
.unwrap();
snapshot_list_approvals("list_approvals_stale_only", &repo);
}
// ============================================================================
// add-approvals tests
// ============================================================================
#[rstest]
fn test_add_approvals_no_config(repo: TestRepo) {
snapshot_add_approvals("add_approvals_no_config", &repo, &[]);
}
#[rstest]
fn test_add_approvals_all_with_none_approved(repo: TestRepo) {
repo.write_project_config(r#"pre-start = "echo 'test'""#);
repo.commit("Add config");
snapshot_add_approvals("add_approvals_all_none_approved", &repo, &["--all"]);
}
#[rstest]
fn test_add_approvals_empty_config(repo: TestRepo) {
repo.write_project_config("");
repo.commit("Add empty config");
snapshot_add_approvals("add_approvals_empty_config", &repo, &[]);
}
/// `wt hook approvals` is the deprecated alias for `wt config approvals`.
/// Both `add` and `clear` must emit the deprecation warning and still forward
/// to the same handler.
#[rstest]
#[case::add("add", "hook_approvals_deprecated_add")]
#[case::clear("clear", "hook_approvals_deprecated_clear")]
fn test_hook_approvals_emits_deprecation_warning(
repo: TestRepo,
#[case] action: &str,
#[case] snapshot_name: &str,
) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = make_snapshot_cmd(&repo, "hook", &[], None);
cmd.arg("approvals").arg(action);
assert_cmd_snapshot!(snapshot_name, cmd);
});
}
/// `--yes` is what makes `add` usable unattended: with no terminal to prompt
/// on it lists what it trusts and writes the approvals. The flag is global, so
/// it records the same thing on either side of the subcommand.
#[rstest]
fn test_add_approvals_yes_records_without_a_terminal(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(
r#"pre-merge = "cargo test"
[aliases]
deploy = "echo deploying"
"#,
);
repo.commit("Add config");
snapshot_add_approvals("add_approvals_yes", &repo, &["--yes"]);
let recorded = fs::read_to_string(repo.test_approvals_path()).unwrap();
assert!(recorded.contains("cargo test"), "{recorded}");
assert!(recorded.contains("echo deploying"), "{recorded}");
// Same run again from the global flag position, which reaches the command
// through clap's global `--yes` rather than the subcommand's own parse.
fs::remove_file(repo.test_approvals_path()).unwrap();
let output = make_snapshot_cmd_with_global_flags(&repo, "config", &[], None, &["--yes"])
.args(["approvals", "add"])
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
assert_eq!(
fs::read_to_string(repo.test_approvals_path()).unwrap(),
recorded
);
}
/// The record is all `add` produces, so a write it cannot make fails the
/// command. A warning plus exit 0 would leave an orchestrator that pre-approves
/// and reads the exit code walking into the prompt it just paid to avoid.
#[rstest]
fn test_add_approvals_yes_fails_when_approvals_cannot_be_saved(repo: TestRepo) {
repo.write_project_config(r#"pre-merge = "cargo test""#);
repo.commit("Add config");
// A regular file where the approvals directory would go: creating the
// parent fails on every platform, with no permission bits involved.
let blocker = repo.test_approvals_path().with_file_name("blocker");
fs::write(&blocker, "").unwrap();
let output = repo
.wt_command()
.args(["config", "approvals", "add", "--yes"])
.env("WORKTRUNK_APPROVALS_PATH", blocker.join("approvals.toml"))
.output()
.unwrap();
assert!(!output.status.success(), "{output:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Failed to save command approval"),
"{stderr}"
);
assert!(
!stderr.contains("Commands approved"),
"a failed write must not report success: {stderr}"
);
}
/// Regression: `wt config approvals add` must walk project aliases as well as
/// hooks. With only an alias declared (no hook commands), the alias should
/// appear in the approval batch.
#[rstest]
fn test_add_approvals_includes_aliases(repo: TestRepo) {
repo.write_project_config(
r#"[aliases]
deploy = "echo deploying {{ branch }}"
"#,
);
repo.commit("Add alias-only project config");
snapshot_add_approvals("add_approvals_includes_aliases", &repo, &[]);
}
// ============================================================================
// clear-approvals tests
// ============================================================================
#[rstest]
fn test_clear_approvals_no_approvals(repo: TestRepo) {
snapshot_clear_approvals("clear_approvals_no_approvals", &repo, &[]);
}
#[rstest]
fn test_clear_approvals_with_approvals(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"pre-start = "echo 'test'""#);
repo.commit("Add config");
// Manually approve the command using the same project id wt will compute.
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
// Now clear approvals
snapshot_clear_approvals("clear_approvals_with_approvals", &repo, &[]);
}
#[rstest]
fn test_clear_approvals_global_no_approvals(repo: TestRepo) {
snapshot_clear_approvals("clear_approvals_global_no_approvals", &repo, &["--global"]);
}
#[rstest]
fn test_clear_approvals_global_with_approvals(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"pre-start = "echo 'test'""#);
repo.commit("Add config");
// Manually approve the command using the same project id wt will compute.
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
// Now clear all global approvals
snapshot_clear_approvals(
"clear_approvals_global_with_approvals",
&repo,
&["--global"],
);
}
/// A hand-written pattern entry approves commands `clear` deliberately can't
/// touch; the hint names the entry so the surviving approval is traceable.
#[rstest]
fn test_clear_approvals_only_pattern_entry(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"pre-start = "echo 'test'""#);
repo.commit("Add config");
// Written raw: pattern entries are hand-edited by definition (the write
// paths refuse `*`).
fs::write(
repo.test_approvals_path(),
"[projects.\"*\"]\napproved-commands = [\"echo 'test'\"]\n",
)
.unwrap();
snapshot_clear_approvals("clear_approvals_only_pattern_entry", &repo, &[]);
}
/// Clearing the exact entry succeeds, and the hint says why `list` will still
/// show the pattern entry's command as approved.
#[rstest]
fn test_clear_approvals_exact_cleared_pattern_remains(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"pre-start = "echo 'test'""#);
repo.commit("Add config");
fs::write(
repo.test_approvals_path(),
"[projects.\"*\"]\napproved-commands = [\"echo 'other'\"]\n",
)
.unwrap();
// The exact entry rides the same file: mutations reload it from disk
// under the lock, so the pattern entry survives alongside.
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
snapshot_clear_approvals("clear_approvals_exact_cleared_pattern_remains", &repo, &[]);
}
#[rstest]
fn test_clear_approvals_after_clear(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"pre-start = "echo 'test'""#);
repo.commit("Add config");
// Manually approve the command using the same project id wt will compute.
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
// Clear approvals
let mut cmd = make_snapshot_cmd(&repo, "config", &[], None);
cmd.arg("approvals").arg("clear");
cmd.output().unwrap();
// Try to clear again (should show "no approvals")
snapshot_clear_approvals("clear_approvals_after_clear", &repo, &[]);
}
/// `clear --stale` removes only the approvals whose commands left the project
/// config, echoing what was removed; valid approvals survive, so the follow-up
/// list shows the remaining command approved with no stale block.
#[rstest]
fn test_clear_approvals_stale(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(r#"pre-merge = "cargo test""#);
repo.commit("Add config");
let mut approvals = Approvals::default();
approvals
.approve_commands(
repo.project_id(),
vec![
"cargo test".to_string(),
"some removed command".to_string(),
"other removed command".to_string(),
],
repo.test_approvals_path(),
)
.unwrap();
snapshot_clear_approvals("clear_approvals_stale", &repo, &["--stale"]);
snapshot_list_approvals("list_approvals_after_clear_stale", &repo);
}
/// With every approval matching a config command, `clear --stale` is a no-op.
#[rstest]
fn test_clear_approvals_stale_none(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(r#"pre-merge = "cargo test""#);
repo.commit("Add config");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"cargo test".to_string(),
repo.test_approvals_path(),
)
.unwrap();
snapshot_clear_approvals("clear_approvals_stale_none", &repo, &["--stale"]);
}
/// Without a project config there is no frame to compute staleness against,
/// so `clear --stale` errors like `add` — it must NOT treat every recorded
/// approval as stale and wipe the project's approvals.
#[rstest]
fn test_clear_approvals_stale_no_config(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"orphan command".to_string(),
repo.test_approvals_path(),
)
.unwrap();
snapshot_clear_approvals("clear_approvals_stale_no_config", &repo, &["--stale"]);
// The recorded approval survives the failed clear.
let content = fs::read_to_string(repo.test_approvals_path()).unwrap();
assert!(content.contains("orphan command"));
}
/// `--stale` is scoped to the current project's config, so it cannot combine
/// with `--global`.
#[rstest]
fn test_clear_approvals_stale_global_conflict(repo: TestRepo) {
snapshot_clear_approvals(
"clear_approvals_stale_global_conflict",
&repo,
&["--stale", "--global"],
);
}
/// `clear` reads approvals from the legacy `[projects.X]` section in `config.toml`
/// when `approvals.toml` is absent, and clears them. Exercises the
/// `Approvals::load` fallback path documented in `src/config/approvals.rs`.
#[rstest]
fn test_clear_approvals_legacy_config_storage(repo: TestRepo, temp_home: TempDir) {
// Remove origin so project_identifier uses full canonical path
repo.run_git(&["remote", "remove", "origin"]);
// Get the canonical path for the project identifier (escaped for TOML)
let project_id_str = repo.project_id();
// Write approved commands as a sibling config.toml of the test approvals path.
// The fallback reads config.toml from the same directory as approvals.toml.
let config_path = repo.test_approvals_path().with_file_name("config.toml");
fs::write(
&config_path,
format!(
r#"worktree-path = "../{{{{ repo }}}}.{{{{ branch }}}}"
[projects.'{project_id_str}']
approved-commands = ["cargo build", "cargo test", "npm install"]
"#
),
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK_CONFIG_PATH", &config_path);
cmd.args(["config", "approvals", "clear"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!("clear_approvals_legacy_config_storage", cmd);
});
}
#[rstest]
fn test_clear_approvals_multiple_approvals(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(
r#"
pre-start = "echo 'first'"
post-start = "echo 'second'"
[pre-commit]
lint = "echo 'third'"
"#,
);
repo.commit("Add config with multiple commands");
// Manually approve all commands using the same project id wt will compute.
let project_id = repo.project_id();
let mut approvals = Approvals::default();
approvals
.approve_command(
project_id.clone(),
"echo 'first'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
approvals
.approve_command(
project_id.clone(),
"echo 'second'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
approvals
.approve_command(
project_id,
"echo 'third'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
// Now clear approvals (should show count of 3)
snapshot_clear_approvals("clear_approvals_multiple_approvals", &repo, &[]);
}
// ============================================================================
// add-approvals additional coverage tests
// ============================================================================
#[rstest]
fn test_add_approvals_all_already_approved(repo: TestRepo) {
// Remove origin so project_identifier uses the canonical worktree path —
// matches what `Repository::project_identifier` computes at runtime.
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"pre-start = "echo 'test'""#);
repo.commit("Add config");
// Manually approve the command using the same project id wt will compute.
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
repo.test_approvals_path(),
)
.unwrap();
// Try to add approvals - should show "all already approved"
snapshot_add_approvals("add_approvals_all_already_approved", &repo, &[]);
}
#[rstest]
fn test_add_approvals_project_config_no_commands(repo: TestRepo) {
// Create project config with only non-hook settings
repo.write_project_config(
r#"# Project config without any hook sections
[list]
url = "http://localhost:8080"
"#,
);
repo.commit("Add config without hooks");
// Try to add approvals - should show "no commands configured"
snapshot_add_approvals("add_approvals_no_commands", &repo, &[]);
}
// ============================================================================
// bare repository tests
// ============================================================================
/// Regression test for #1744: `wt config approvals add` should find project config
/// in a bare repo's primary worktree. `config create --project` should place it
/// there (not in the bare repo root), consistent with `ProjectConfig::load`.
#[test]
fn test_add_approvals_bare_repo_config_in_primary_worktree() {
let test = BareRepoTest::new();
let main_worktree = test.create_worktree("main", "main");
test.commit_in(&main_worktree, "Initial commit");
// Write project config in the primary worktree's .config/wt.toml
// This is where `config create --project` should place it for bare repos
let config_dir = main_worktree.join(".config");
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(
config_dir.join("wt.toml"),
r#"pre-start = "echo 'hello'"
"#,
)
.unwrap();
let settings = setup_temp_snapshot_settings(test.temp_path());
settings.bind(|| {
// Run `wt config approvals add --all` from the main worktree
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(&main_worktree)
.args(["config", "approvals", "add", "--all"]);
assert_cmd_snapshot!("add_approvals_bare_repo_config_in_primary_worktree", cmd);
});
}
/// Test that `project_config_path` returns None (and config create errors)
/// when no linked worktrees exist in a bare repo.
#[test]
fn test_config_create_project_bare_repo_no_worktrees_errors() {
let test = BareRepoTest::new();
// Don't create any worktrees — no config location available
// Run `wt config create --project` from the bare repo root — should fail
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["config", "create", "--project"]);
let output = cmd.output().unwrap();
assert!(
!output.status.success(),
"wt config create --project should fail with no worktrees"
);
// Config should NOT be created at the bare repo root
let bare_root_config = test.bare_repo_path().join(".config").join("wt.toml");
assert!(
!bare_root_config.exists(),
"Config should NOT be created in bare repo root at {:?}",
bare_root_config
);
}
/// `config approvals add` and `hook show` should error in a bare repo with
/// no linked worktrees (project_config_path returns None).
#[test]
fn test_hook_commands_bare_repo_no_worktrees_errors() {
let test = BareRepoTest::new();
// config approvals add --all should fail
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["config", "approvals", "add", "--all"]);
let output = cmd.output().unwrap();
assert!(
!output.status.success(),
"config approvals add should fail with no worktrees"
);
// hook show should fail
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["hook", "show"]);
let output = cmd.output().unwrap();
assert!(
!output.status.success(),
"hook show should fail with no worktrees"
);
}
/// Regression test for #1744: `wt config create --project` in a bare repo
/// should create config in the primary worktree, not the bare repo root.
#[test]
fn test_config_create_project_bare_repo_uses_primary_worktree() {
let test = BareRepoTest::new();
let main_worktree = test.create_worktree("main", "main");
test.commit_in(&main_worktree, "Initial commit");
// Run `wt config create --project` from the bare repo root
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["config", "create", "--project"]);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt config create --project failed:\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
// Config should be in the primary worktree, NOT the bare repo root
let primary_config = main_worktree.join(".config").join("wt.toml");
let bare_root_config = test.bare_repo_path().join(".config").join("wt.toml");
assert!(
primary_config.exists(),
"Config should be created in primary worktree at {:?}",
primary_config
);
assert!(
!bare_root_config.exists(),
"Config should NOT be created in bare repo root at {:?}",
bare_root_config
);
}