From 126ab89c3fe2902236d2881d41ece6b83f4d4977 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 6 Sep 2026 12:38:19 -0700 Subject: [PATCH 1/6] Replace config update --print with --output --- CHANGELOG.md | 2 + docs/src/content/docs/config.md | 69 +++++++++ .../skills/worktrunk/reference/config.md | 69 +++++++++ skills/worktrunk/reference/config.md | 69 +++++++++ src/cli/config.rs | 20 ++- src/cli/mod.rs | 1 + src/commands/config/update.rs | 99 ++++++++----- src/config/deprecation.rs | 11 +- src/main.rs | 2 +- tests/integration_tests/config_show.rs | 136 ++++++++++++++---- tests/integration_tests/help.rs | 1 + .../integration_tests/output_system_guard.rs | 4 +- ...ation_tests__help__help_config_update.snap | 97 +++++++++++++ 13 files changed, 503 insertions(+), 77 deletions(-) create mode 100644 tests/snapshots/integration__integration_tests__help__help_config_update.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index eac162b8e..778c8fd1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Improved +- **Config migration output names its destination**: `wt config update --output ` writes the migration artifact to that file instead of applying it in place, and `--output=-` writes it to stdout. Output mode includes project config when run from a linked worktree. (Breaking: `--print` was removed.) + - **`wt switch --execute` takes a program, not a shell string**: `-x` names one program, with everything after `--` passed as literal argv. Worktrunk spawns it as a child rather than running it in your interactive shell, so shell functions and its `cd` no longer reach you. `-x sh -- -c '…'` recovers shell syntax, not functions. (Breaking: existing `-x` strings, plus `WORKTRUNK_DIRECTIVE_EXEC_FILE` and `WORKTRUNK_SHELL`.) ([#3977](https://github.com/max-sixty/worktrunk/pull/3977), closes [#2860](https://github.com/max-sixty/worktrunk/issues/2860), fixes [#3944](https://github.com/max-sixty/worktrunk/issues/3944), thanks @omgreenfield for testing the migration path) - **Worktrunk decides tracking for the branches it creates**: a new branch gets an upstream only when its name matches the remote branch it starts from, whatever `branch.autoSetupMerge` says: `--create release --base origin/release` tracks, `--create feature --base origin/release` does not. Under `autoSetupMerge = false` this previously exited 128 after creating the worktree. ([#3913](https://github.com/max-sixty/worktrunk/pull/3913), [#3950](https://github.com/max-sixty/worktrunk/pull/3950), fixes [#3937](https://github.com/max-sixty/worktrunk/issues/3937), thanks @mjakl for reporting and diagnosing) diff --git a/docs/src/content/docs/config.md b/docs/src/content/docs/config.md index 675a9f934..c26ca5ee4 100644 --- a/docs/src/content/docs/config.md +++ b/docs/src/content/docs/config.md @@ -809,6 +809,75 @@ Global Options: Skip approval prompts ``` +## wt config update + +Update deprecated config settings. + +Updates deprecated settings in user and project config files +to their current equivalents, removes deprecated keys that have no equivalent +and reports each one, and adopts defaults that a future release switches — +currently `[list] json-schema = 2` — so the switch happens as a reviewed config +edit rather than at upgrade. Shows a diff and asks for confirmation. + +Migrations are computed in memory on demand; nothing is written outside this +command. Set `--output ` to write the migration artifact to that +destination instead of applying it in place. Use `-` for stdout. + +### Examples + +Preview and apply updates: +```console +$ wt config update +``` + +Apply without confirmation: +```console +$ wt config update --yes +``` + +Write the migration artifact to a file: +```console +$ wt config update --output migrated.toml +``` + +Write the migration artifact to stdout: +```console +$ wt config update --output=- +``` + +### Command reference + +```text wt-command-reference +wt config update - Update deprecated config settings + +Usage: wt config update [OPTIONS] + +Options: + --output + Output migrated config (- for stdout) + + -h, --help + Print help (see a summary with '-h') + +Global Options: + -C + Working directory for this command + + --config + User config file path + + --config-set + Override config with inline TOML, e.g. --config-set list.full=true (repeatable) + + -v, --verbose... + Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug + logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to + apply the same level everywhere — including shell completion, which no flag can reach + + -y, --yes + Skip approval prompts +``` + ## wt config approvals Manage command approvals. diff --git a/plugins/worktrunk/skills/worktrunk/reference/config.md b/plugins/worktrunk/skills/worktrunk/reference/config.md index a3bf26f24..eb0ad2102 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/config.md +++ b/plugins/worktrunk/skills/worktrunk/reference/config.md @@ -801,6 +801,75 @@ Global Options: Skip approval prompts ``` +## wt config update + +Update deprecated config settings. + +Updates deprecated settings in user and project config files +to their current equivalents, removes deprecated keys that have no equivalent +and reports each one, and adopts defaults that a future release switches — +currently `[list] json-schema = 2` — so the switch happens as a reviewed config +edit rather than at upgrade. Shows a diff and asks for confirmation. + +Migrations are computed in memory on demand; nothing is written outside this +command. Set `--output ` to write the migration artifact to that +destination instead of applying it in place. Use `-` for stdout. + +### Examples + +Preview and apply updates: +```console +$ wt config update +``` + +Apply without confirmation: +```console +$ wt config update --yes +``` + +Write the migration artifact to a file: +```console +$ wt config update --output migrated.toml +``` + +Write the migration artifact to stdout: +```console +$ wt config update --output=- +``` + +### Command reference + +``` +wt config update - Update deprecated config settings + +Usage: wt config update [OPTIONS] + +Options: + --output + Output migrated config (- for stdout) + + -h, --help + Print help (see a summary with '-h') + +Global Options: + -C + Working directory for this command + + --config + User config file path + + --config-set + Override config with inline TOML, e.g. --config-set list.full=true (repeatable) + + -v, --verbose... + Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug + logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to + apply the same level everywhere — including shell completion, which no flag can reach + + -y, --yes + Skip approval prompts +``` + ## wt config approvals Manage command approvals. diff --git a/skills/worktrunk/reference/config.md b/skills/worktrunk/reference/config.md index a3bf26f24..eb0ad2102 100644 --- a/skills/worktrunk/reference/config.md +++ b/skills/worktrunk/reference/config.md @@ -801,6 +801,75 @@ Global Options: Skip approval prompts ``` +## wt config update + +Update deprecated config settings. + +Updates deprecated settings in user and project config files +to their current equivalents, removes deprecated keys that have no equivalent +and reports each one, and adopts defaults that a future release switches — +currently `[list] json-schema = 2` — so the switch happens as a reviewed config +edit rather than at upgrade. Shows a diff and asks for confirmation. + +Migrations are computed in memory on demand; nothing is written outside this +command. Set `--output ` to write the migration artifact to that +destination instead of applying it in place. Use `-` for stdout. + +### Examples + +Preview and apply updates: +```console +$ wt config update +``` + +Apply without confirmation: +```console +$ wt config update --yes +``` + +Write the migration artifact to a file: +```console +$ wt config update --output migrated.toml +``` + +Write the migration artifact to stdout: +```console +$ wt config update --output=- +``` + +### Command reference + +``` +wt config update - Update deprecated config settings + +Usage: wt config update [OPTIONS] + +Options: + --output + Output migrated config (- for stdout) + + -h, --help + Print help (see a summary with '-h') + +Global Options: + -C + Working directory for this command + + --config + User config file path + + --config-set + Override config with inline TOML, e.g. --config-set list.full=true (repeatable) + + -v, --verbose... + Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug + logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to + apply the same level everywhere — including shell completion, which no flag can reach + + -y, --yes + Skip approval prompts +``` + ## wt config approvals Manage command approvals. diff --git a/src/cli/config.rs b/src/cli/config.rs index 66f41f577..d367052ee 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::{Args, Subcommand}; use super::SwitchFormat; @@ -538,7 +540,8 @@ currently `[list] json-schema = 2` — so the switch happens as a reviewed confi edit rather than at upgrade. Shows a diff and asks for confirmation. Migrations are computed in memory on demand; nothing is written outside this -command. Use `--print` to see the migrated TOML without touching any file. +command. Set `--output ` to write the migration artifact to that +destination instead of applying it in place. Use `-` for stdout. ## Examples @@ -552,15 +555,20 @@ Apply without confirmation: $ wt config update --yes ``` -Print the migrated config to stdout (no changes written): +Write the migration artifact to a file: ```console -$ wt config update --print +$ wt config update --output migrated.toml +``` + +Write the migration artifact to stdout: +```console +$ wt config update --output=- ```"# )] Update { - /// Print the migrated config to stdout instead of writing it - #[arg(long)] - print: bool, + /// Output migrated config (`-` for stdout) + #[arg(long, value_name = "PATH")] + output: Option, }, /// Manage command approvals diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 69f200b9b..d3222d492 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2597,6 +2597,7 @@ $ wt --config-set 'projects."github.com/owner/repo".worktree-path = "/tmp/scratc Hooks, aliases and `step.copy-ignored.exclude` accumulate rather than replace, so an env-set hook and a project's hook both run. + "#)] diff --git a/src/commands/config/update.rs b/src/commands/config/update.rs index 2da0d7d8c..bbdf75fda 100644 --- a/src/commands/config/update.rs +++ b/src/commands/config/update.rs @@ -1,11 +1,12 @@ //! Config update command. //! -//! Updates deprecated settings in user and project config files by -//! re-migrating in memory and overwriting the file. The previous `.new` file -//! flow was removed — nothing writes to disk outside this command. +//! Computes user- and project-config migrations in memory. The default mode +//! previews and applies them atomically; output mode writes one migration +//! artifact to the named destination instead. The previous `.new` file flow +//! was removed — nothing writes to disk outside this command. use std::fmt::Write as _; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::Context; use color_print::cformat; @@ -13,10 +14,11 @@ use worktrunk::config::{ ConfigFileKind, DeprecationInfo, DeprecationKind, compute_migrated_content, config_path, copy_approved_commands_to_approvals_file, format_deprecation_warnings, format_migration_diff, }; -use worktrunk::git::Repository; +use worktrunk::git::{Repository, resolve_input_path}; +use worktrunk::path::format_path_for_display; use worktrunk::styling::{ - eprint, eprintln, format_bash_with_gutter, hint_message, info_message, print, println, - success_message, suggest_command_in_dir, + eprint, eprintln, format_bash_with_gutter, hint_message, info_message, print, success_message, + suggest_command_in_dir, }; use crate::output::prompt::{PromptResponse, prompt_yes_no_preview}; @@ -34,43 +36,24 @@ struct UpdateCandidate { } /// Handle the `wt config update` command. -pub fn handle_config_update(yes: bool, print: bool) -> anyhow::Result<()> { +pub fn handle_config_update(yes: bool, output: Option) -> anyhow::Result<()> { let mut candidates = Vec::new(); + let read_only = output.is_some(); if let Some(candidate) = check_user_config()? { candidates.push(candidate); } - if let Some(candidate) = check_project_config()? { + if let Some(candidate) = check_project_config(read_only)? { candidates.push(candidate); } - if candidates.is_empty() { - if print { - // --print on a clean config is a no-op; stay quiet on stdout. - return Ok(()); - } - eprintln!("{}", info_message("No deprecated settings found")); + if let Some(output) = output { + write_migrated_output(&output, &candidates)?; return Ok(()); } - if print { - // Emit migrated content to stdout. Multiple configs → separate with a - // labeled header so the output is still parseable. `--print` is for - // piping, so stderr stays empty. - let multi = candidates.len() > 1; - for (idx, candidate) in candidates.iter().enumerate() { - if multi { - if idx > 0 { - println!(); - } - println!( - "# {} ({})", - candidate.info.label(), - candidate.config_path.display() - ); - } - print!("{}", candidate.migrated); - } + if candidates.is_empty() { + eprintln!("{}", info_message("No deprecated settings found")); return Ok(()); } @@ -122,6 +105,49 @@ pub fn handle_config_update(yes: bool, print: bool) -> anyhow::Result<()> { Ok(()) } +/// Write the migration artifact to a path, or to stdout when the path is `-`. +fn write_migrated_output(output: &Path, candidates: &[UpdateCandidate]) -> anyhow::Result<()> { + let artifact = format_migrated_output(candidates); + + if output == Path::new("-") { + // A clean config produces no output, so stdout composes in a pipe. + print!("{artifact}"); + return Ok(()); + } + + let output = resolve_input_path(output); + worktrunk::utils::write_atomically(&output, &artifact).with_context(|| { + format!( + "Failed to write output @ {}", + format_path_for_display(&output) + ) + })?; + Ok(()) +} + +/// Format the migration artifact shared by file and stdout destinations. +fn format_migrated_output(candidates: &[UpdateCandidate]) -> String { + let mut artifact = String::new(); + let multi = candidates.len() > 1; + + for (idx, candidate) in candidates.iter().enumerate() { + if multi { + if idx > 0 { + artifact.push('\n'); + } + let _ = writeln!( + artifact, + "# {} ({})", + candidate.info.label(), + candidate.config_path.display() + ); + } + artifact.push_str(&candidate.migrated); + } + + artifact +} + /// Format update preview for display. /// /// Renders the per-pattern deprecation warnings followed by the diff. The @@ -177,7 +203,7 @@ fn check_user_config() -> anyhow::Result> { })) } -fn check_project_config() -> anyhow::Result> { +fn check_project_config(read_only: bool) -> anyhow::Result> { let repo = match Repository::current() { Ok(repo) => repo, Err(_) => return Ok(None), @@ -192,6 +218,7 @@ fn check_project_config() -> anyhow::Result> { } let is_linked = repo.current_worktree().is_linked().unwrap_or(true); + let actionable = read_only || !is_linked; let original = std::fs::read_to_string(&config_path).context("Failed to read project config")?; @@ -199,7 +226,7 @@ fn check_project_config() -> anyhow::Result> { let result = worktrunk::config::check_and_migrate( &config_path, &original, - !is_linked, // only actionable from main worktree + actionable, ConfigFileKind::Project, Some(&repo), false, @@ -209,7 +236,7 @@ fn check_project_config() -> anyhow::Result> { return Ok(None); }; - if is_linked { + if !actionable { let cmd = suggest_command_in_dir(repo.repo_path()?, "config", &["update"], &[]); eprintln!("{}", hint_message("To update project config:")); eprintln!("{}", format_bash_with_gutter(&cmd)); diff --git a/src/config/deprecation.rs b/src/config/deprecation.rs index 740c349c6..ecc3ec1ab 100644 --- a/src/config/deprecation.rs +++ b/src/config/deprecation.rs @@ -26,7 +26,7 @@ //! content (for serde) and a `DeprecationInfo` describing what needs fixing. //! Users materialize migrations explicitly via `wt config update` (which //! overwrites the config file and copies approved-commands to `approvals.toml`) -//! or inspect them via `wt config show` / `wt config update --print`. +//! or export them via `wt config update --output ` (`-` for stdout). //! //! Per-path warning dedup still applies within a process so `wt list` doesn't //! spam the same deprecation message from multiple config layers. @@ -1584,12 +1584,13 @@ pub struct CheckAndMigrateResult { /// /// Pure with respect to the filesystem — never rewrites config or copies /// approvals. The user materializes migrations by running `wt config update` -/// (or `wt config update --print`). Deprecation warnings still go to stderr +/// or exports the result instead of applying it in place via +/// `wt config update --output `. Deprecation warnings still go to stderr /// when `emit_inline_warnings` is set. /// -/// Set `warn_and_migrate` to false for project config on feature worktrees — -/// the warning is only actionable from the main worktree where the user would -/// run `wt config update`. +/// Set `warn_and_migrate` to false when project config is not actionable. A +/// linked worktree cannot update the file, but read-only output remains +/// actionable and passes true. /// /// `kind` names the config file being checked; it derives the warning label /// and scopes kind-specific rules (`DeprecationRule::PendingDefault`). diff --git a/src/main.rs b/src/main.rs index b30b02345..6c2b9df77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -639,7 +639,7 @@ fn handle_config_command(action: ConfigCommand, yes: bool) -> anyhow::Result<()> ConfigCommand::Shell { action } => handle_config_shell_command(action, yes), ConfigCommand::Create { project } => handle_config_create(project), ConfigCommand::Show { full, format } => handle_config_show(full, format), - ConfigCommand::Update { print } => handle_config_update(yes, print), + ConfigCommand::Update { output } => handle_config_update(yes, output), ConfigCommand::Approvals { action } => match action { ApprovalsCommand::List { format } => list_approvals(format), ApprovalsCommand::Add { all } => add_approvals(all, yes), diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index f3e49bffd..0abc47857 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3205,40 +3205,59 @@ fn test_config_update_project_config_from_linked_worktree_shows_hint(repo: TestR ); } -/// `wt config update --print` with both user- and project-config deprecations -/// emits both, separated by labeled headers on stdout. +/// File and stdout output receive the same labeled artifact when both config +/// files have migrations, without changing either source config. #[rstest] -fn test_config_update_print_emits_both_configs(repo: TestRepo) { +fn test_config_update_output_destinations_emit_same_artifact(repo: TestRepo) { let user_config_path = repo.test_config_path(); - fs::write( - user_config_path, - r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" -"#, - ) - .unwrap(); - repo.write_project_config( - r#"pre-start = "ln -sf {{ main_worktree }}/node_modules" -"#, - ); + let user_original = r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" +"#; + fs::write(user_config_path, user_original).unwrap(); + let project_original = r#"pre-start = "ln -sf {{ main_worktree }}/node_modules" +"#; + repo.write_project_config(project_original); repo.commit("Add deprecated project config"); - let output = repo + let stdout_output = repo .wt_command() - .args(["config", "update", "--print"]) + .args(["config", "update", "--output=-"]) .output() .unwrap(); - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout_output.status.success()); + assert!(stdout_output.stderr.is_empty()); + let stdout = String::from_utf8_lossy(&stdout_output.stdout); assert!(stdout.contains("# User config")); assert!(stdout.contains("# Project config")); assert!(stdout.contains("{{ repo }}")); assert!(stdout.contains("pre-start")); + + fs::write(repo.root_path().join("migrated.toml"), "stale\n").unwrap(); + let mut file_command = repo.wt_command(); + file_command + .current_dir(repo.root_path().parent().unwrap()) + .arg("-C") + .arg(repo.root_path()) + .args(["config", "update", "--output=migrated.toml"]); + let file_output = file_command.output().unwrap(); + assert!(file_output.status.success()); + assert!(file_output.stdout.is_empty()); + assert!(file_output.stderr.is_empty()); + assert_eq!( + fs::read(repo.root_path().join("migrated.toml")).unwrap(), + stdout_output.stdout, + "file and stdout destinations must receive the same artifact" + ); + assert_eq!(fs::read_to_string(user_config_path).unwrap(), user_original); + assert_eq!( + fs::read_to_string(repo.root_path().join(".config").join("wt.toml")).unwrap(), + project_original + ); } -/// `wt config update --print` on a clean config exits silently with empty +/// `wt config update --output=-` on a clean config exits silently with empty /// stdout — no "nothing to do" noise to corrupt a pipe. #[rstest] -fn test_config_update_print_on_clean_config_is_silent(repo: TestRepo) { +fn test_config_update_output_stdout_on_clean_config_is_silent(repo: TestRepo) { fs::write( repo.test_config_path(), r#"worktree-path = "../{{ repo }}.{{ branch }}" @@ -3251,7 +3270,7 @@ json-schema = 1 let output = repo .wt_command() - .args(["config", "update", "--print"]) + .args(["config", "update", "--output", "-"]) .output() .unwrap(); assert!(output.status.success()); @@ -3261,10 +3280,10 @@ json-schema = 1 ); } -/// `wt config update --print` emits the migrated TOML to stdout without +/// `wt config update --output=-` emits the migrated TOML to stdout without /// touching the config file. Stderr stays empty so the output is pipeable. #[rstest] -fn test_config_update_print_emits_migrated_without_writing(repo: TestRepo) { +fn test_config_update_output_stdout_emits_migrated_without_writing(repo: TestRepo) { let config_path = repo.test_config_path(); let original = r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" "#; @@ -3272,18 +3291,18 @@ fn test_config_update_print_emits_migrated_without_writing(repo: TestRepo) { let output = repo .wt_command() - .args(["config", "update", "--print"]) + .args(["config", "update", "--output=-"]) .output() .unwrap(); assert!( output.status.success(), - "config update --print should succeed: {}", + "config update --output=- should succeed: {}", String::from_utf8_lossy(&output.stderr) ); assert!( output.stderr.is_empty(), - "--print must keep stderr empty for pipe-friendliness, got: {}", + "--output=- must keep stderr empty for pipe-friendliness, got: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); @@ -3294,11 +3313,74 @@ fn test_config_update_print_emits_migrated_without_writing(repo: TestRepo) { assert_eq!( fs::read_to_string(config_path).unwrap(), original, - "--print must not modify the config file" + "--output=- must not modify the config file" ); assert!( !config_path.with_extension("toml.new").exists(), - "--print must not write a .new file" + "--output=- must not write a .new file" + ); +} + +/// Read-only output includes project config from a linked worktree without +/// changing either checkout's copy. +#[rstest] +fn test_config_update_output_stdout_from_linked_worktree(repo: TestRepo) { + repo.write_project_config( + r#"pre-start = "ln -sf {{ main_worktree }}/node_modules" +"#, + ); + repo.commit("Add deprecated project config"); + + let project_config_path = repo.root_path().join(".config").join("wt.toml"); + let before = fs::read_to_string(&project_config_path).unwrap(); + let feature_path = repo.root_path().parent().unwrap().join("feature-output"); + repo.run_git(&[ + "worktree", + "add", + feature_path.to_str().unwrap(), + "-b", + "feature-output", + ]); + + let output = repo + .wt_command() + .args(["config", "update", "--output=-"]) + .current_dir(&feature_path) + .output() + .unwrap(); + + assert!(output.status.success()); + assert!(output.stderr.is_empty()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("{{ repo }}")); + assert!(!stdout.contains("{{ main_worktree }}")); + assert_eq!( + fs::read_to_string(feature_path.join(".config").join("wt.toml")).unwrap(), + before, + "linked-worktree project config must remain unchanged" + ); + assert_eq!( + fs::read_to_string(project_config_path).unwrap(), + before, + "main-worktree project config must remain unchanged" + ); +} + +/// The former output flag is no longer accepted. +#[rstest] +fn test_config_update_rejects_print(repo: TestRepo) { + let output = repo + .wt_command() + .args(["config", "update", "--print"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr) + .ansi_strip() + .into_owned(); + assert!( + stderr.contains("unexpected argument '--print'"), + "unexpected error: {stderr}" ); } diff --git a/tests/integration_tests/help.rs b/tests/integration_tests/help.rs index 9e83248da..0a53d854a 100644 --- a/tests/integration_tests/help.rs +++ b/tests/integration_tests/help.rs @@ -103,6 +103,7 @@ fn test_merge_help_describes_exact_shape_no_rebase() { #[case("help_config_shell", "config shell --help")] #[case("help_config_create", "config create --help")] #[case("help_config_show", "config show --help")] +#[case("help_config_update", "config update --help")] #[case("help_config_plugins", "config plugins --help")] #[case("help_config_plugins_codex", "config plugins codex --help")] #[case( diff --git a/tests/integration_tests/output_system_guard.rs b/tests/integration_tests/output_system_guard.rs index 2dde96b60..86da6be60 100644 --- a/tests/integration_tests/output_system_guard.rs +++ b/tests/integration_tests/output_system_guard.rs @@ -74,7 +74,7 @@ const STDOUT_ALLOWED_PATHS: &[&str] = &[ "commands/configure_shell.rs", // JSON output for wt switch --format=json "commands/worktree/switch.rs", - // Migrated TOML output for wt config update --print (pipeable) + // Migrated TOML output for wt config update --output=- (pipeable) "commands/config/update.rs", // Hook listing for wt hook show (paged), and the wt hook --dry-run preview "commands/hook_commands.rs", @@ -574,7 +574,7 @@ fn test_stdout_surfaces_survive_a_closed_consumer(repo: TestRepo) { &["list", "--full"][..], &["list", "statusline"][..], &["list", "--format=json"][..], - &["config", "update", "--print"][..], + &["config", "update", "--output=-"][..], ] { let mut command = repo.wt_command(); let mut child = command diff --git a/tests/snapshots/integration__integration_tests__help__help_config_update.snap b/tests/snapshots/integration__integration_tests__help__help_config_update.snap new file mode 100644 index 000000000..d1c652504 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__help__help_config_update.snap @@ -0,0 +1,97 @@ +--- +source: tests/integration_tests/help.rs +info: + program: wt + args: + - config + - update + - "--help" + env: + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_ALLOW_PROTOCOL: file + GIT_CONFIG_COUNT: "2" + GIT_CONFIG_GLOBAL: /nonexistent/wt/gitconfig + GIT_CONFIG_KEY_0: user.useConfigOnly + GIT_CONFIG_KEY_1: rerere.enabled + GIT_CONFIG_SYSTEM: /nonexistent/wt/gitconfig + GIT_CONFIG_VALUE_0: "true" + GIT_CONFIG_VALUE_1: "false" + LANG: C + LC_ALL: C + LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]" + TERM: alacritty + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_BASH_INSTALLED: "0" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_CODEX_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_FISH_INSTALLED: "0" + WORKTRUNK_TEST_GEMINI_INSTALLED: "0" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_OPENCODE_INSTALLED: "0" + WORKTRUNK_TEST_PARENT_SHELL: "" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0" + WORKTRUNK_TEST_PROBE_TIMEOUT_MS: "60000" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + WORKTRUNK_TEST_ZSH_INSTALLED: "0" +--- +success: true +exit_code: 0 +----- stdout ----- +wt config update - Update deprecated config settings + +Usage: wt config update [OPTIONS] + +Options: + --output  + Output migrated config (- for stdout) + + -h, --help + Print help (see a summary with '-h') + +Global Options: + -C  + Working directory for this command + + --config  + User config file path + + --config-set  + Override config with inline TOML, e.g. --config-set list.full=true (repeatable) + + -v, --verbose... + Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere — including shell completion, which no flag can reach + + -y, --yes + Skip approval prompts + +Updates deprecated settings in user and project config files +to their current equivalents, removes deprecated keys that have no equivalent +and reports each one, and adopts defaults that a future release switches — +currently [list] json-schema = 2 — so the switch happens as a reviewed config +edit rather than at upgrade. Shows a diff and asks for confirmation. + +Migrations are computed in memory on demand; nothing is written outside this +command. Set --output  to write the migration artifact to that +destination instead of applying it in place. Use - for stdout. + +Examples + +Preview and apply updates: +  wt config update + +Apply without confirmation: +  wt config update --yes + +Write the migration artifact to a file: +  wt config update --output migrated.toml + +Write the migration artifact to stdout: +  wt config update --output=- + +----- stderr ----- From bbc9982b533eda333de922ed2db40ad7601a49d4 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 6 Sep 2026 13:05:55 -0700 Subject: [PATCH 2/6] Preserve clean config output destinations --- docs/src/content/docs/config.md | 3 ++ .../skills/worktrunk/reference/config.md | 3 ++ skills/worktrunk/reference/config.md | 3 ++ src/cli/config.rs | 3 ++ src/commands/config/update.rs | 7 ++++ tests/integration_tests/config_show.rs | 34 +++++++++++++++++++ ...ation_tests__help__help_config_update.snap | 3 ++ 7 files changed, 56 insertions(+) diff --git a/docs/src/content/docs/config.md b/docs/src/content/docs/config.md index c26ca5ee4..6d073730e 100644 --- a/docs/src/content/docs/config.md +++ b/docs/src/content/docs/config.md @@ -823,6 +823,9 @@ Migrations are computed in memory on demand; nothing is written outside this command. Set `--output ` to write the migration artifact to that destination instead of applying it in place. Use `-` for stdout. +Output artifacts omit deprecated `approved-commands`; only in-place updates +move those entries to `approvals.toml`. + ### Examples Preview and apply updates: diff --git a/plugins/worktrunk/skills/worktrunk/reference/config.md b/plugins/worktrunk/skills/worktrunk/reference/config.md index eb0ad2102..abe32bd44 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/config.md +++ b/plugins/worktrunk/skills/worktrunk/reference/config.md @@ -815,6 +815,9 @@ Migrations are computed in memory on demand; nothing is written outside this command. Set `--output ` to write the migration artifact to that destination instead of applying it in place. Use `-` for stdout. +Output artifacts omit deprecated `approved-commands`; only in-place updates +move those entries to `approvals.toml`. + ### Examples Preview and apply updates: diff --git a/skills/worktrunk/reference/config.md b/skills/worktrunk/reference/config.md index eb0ad2102..abe32bd44 100644 --- a/skills/worktrunk/reference/config.md +++ b/skills/worktrunk/reference/config.md @@ -815,6 +815,9 @@ Migrations are computed in memory on demand; nothing is written outside this command. Set `--output ` to write the migration artifact to that destination instead of applying it in place. Use `-` for stdout. +Output artifacts omit deprecated `approved-commands`; only in-place updates +move those entries to `approvals.toml`. + ### Examples Preview and apply updates: diff --git a/src/cli/config.rs b/src/cli/config.rs index 422c1d8ce..f9da7ab10 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -599,6 +599,9 @@ Migrations are computed in memory on demand; nothing is written outside this command. Set `--output ` to write the migration artifact to that destination instead of applying it in place. Use `-` for stdout. +Output artifacts omit deprecated `approved-commands`; only in-place updates +move those entries to `approvals.toml`. + ## Examples Preview and apply updates: diff --git a/src/commands/config/update.rs b/src/commands/config/update.rs index bbdf75fda..6ccabb6f2 100644 --- a/src/commands/config/update.rs +++ b/src/commands/config/update.rs @@ -115,6 +115,13 @@ fn write_migrated_output(output: &Path, candidates: &[UpdateCandidate]) -> anyho return Ok(()); } + if candidates.is_empty() { + // A clean config has no artifact. Replacing the destination with an + // empty file could silently discard user data. + eprintln!("{}", info_message("No deprecated settings found")); + return Ok(()); + } + let output = resolve_input_path(output); worktrunk::utils::write_atomically(&output, &artifact).with_context(|| { format!( diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index 38ce73e14..91cfc5203 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3476,6 +3476,40 @@ json-schema = 1 ); } +/// File output with no migrations leaves an existing destination untouched. +#[rstest] +fn test_config_update_output_file_on_clean_config_preserves_destination(repo: TestRepo) { + let config_path = repo.test_config_path(); + let original = r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[list] +json-schema = 1 +"#; + fs::write(config_path, original).unwrap(); + + let destination = repo.root_path().join("migrated.toml"); + fs::write(&destination, "important user data\n").unwrap(); + + let output = repo + .wt_command() + .args(["config", "update", "--output=migrated.toml"]) + .output() + .unwrap(); + + assert!(output.status.success()); + assert!(output.stdout.is_empty()); + assert!( + String::from_utf8_lossy(&output.stderr).contains("No deprecated settings found"), + "unexpected stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + fs::read_to_string(destination).unwrap(), + "important user data\n" + ); + assert_eq!(fs::read_to_string(config_path).unwrap(), original); +} + /// `wt config update --output=-` emits the migrated TOML to stdout without /// touching the config file. Stderr stays empty so the output is pipeable. #[rstest] diff --git a/tests/snapshots/integration__integration_tests__help__help_config_update.snap b/tests/snapshots/integration__integration_tests__help__help_config_update.snap index d1c652504..fa73cdd4d 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_update.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_update.snap @@ -80,6 +80,9 @@ Migrations are computed in memory on demand; nothing is written outside this command. Set --output  to write the migration artifact to that destination instead of applying it in place. Use - for stdout. +Output artifacts omit deprecated approved-commands; only in-place updates +move those entries to approvals.toml. + Examples Preview and apply updates: From 5742e2f614598edc3f9668a1b11459c526878710 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 6 Sep 2026 13:24:27 -0700 Subject: [PATCH 3/6] Cover config output write failures --- tests/integration_tests/config_show.rs | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index 91cfc5203..3ec3c347b 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3510,6 +3510,35 @@ json-schema = 1 assert_eq!(fs::read_to_string(config_path).unwrap(), original); } +/// File output reports the destination when its parent directory is missing. +#[rstest] +fn test_config_update_output_file_surfaces_write_failure(repo: TestRepo) { + fs::write( + repo.test_config_path(), + r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" +"#, + ) + .unwrap(); + + let output = repo + .wt_command() + .args([ + "config", + "update", + "--output=missing-directory/migrated.toml", + ]) + .output() + .unwrap(); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Failed to write output @") + && stderr.contains("missing-directory/migrated.toml"), + "unexpected stderr: {stderr}" + ); +} + /// `wt config update --output=-` emits the migrated TOML to stdout without /// touching the config file. Stderr stays empty so the output is pipeable. #[rstest] From 5c53650bee5e86f857baf5f1ef7ddaf10d64a3c1 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 6 Sep 2026 13:36:01 -0700 Subject: [PATCH 4/6] Reject combined config file output --- docs/src/content/docs/config.md | 6 +- .../skills/worktrunk/reference/config.md | 6 +- skills/worktrunk/reference/config.md | 6 +- src/cli/config.rs | 6 +- src/commands/config/update.rs | 24 +++++--- tests/integration_tests/config_show.rs | 56 ++++++++++++++++--- ...ation_tests__help__help_config_update.snap | 6 +- 7 files changed, 82 insertions(+), 28 deletions(-) diff --git a/docs/src/content/docs/config.md b/docs/src/content/docs/config.md index 6d073730e..066ecfa18 100644 --- a/docs/src/content/docs/config.md +++ b/docs/src/content/docs/config.md @@ -820,8 +820,10 @@ currently `[list] json-schema = 2` — so the switch happens as a reviewed confi edit rather than at upgrade. Shows a diff and asks for confirmation. Migrations are computed in memory on demand; nothing is written outside this -command. Set `--output ` to write the migration artifact to that -destination instead of applying it in place. Use `-` for stdout. +command. Set `--output ` to write a migrated config to that destination +instead of applying it in place. Use `-` for stdout. When both user and project +config need migration, stdout emits a labeled inspection artifact and file +output fails rather than combining the configs. Output artifacts omit deprecated `approved-commands`; only in-place updates move those entries to `approvals.toml`. diff --git a/plugins/worktrunk/skills/worktrunk/reference/config.md b/plugins/worktrunk/skills/worktrunk/reference/config.md index abe32bd44..da928286a 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/config.md +++ b/plugins/worktrunk/skills/worktrunk/reference/config.md @@ -812,8 +812,10 @@ currently `[list] json-schema = 2` — so the switch happens as a reviewed confi edit rather than at upgrade. Shows a diff and asks for confirmation. Migrations are computed in memory on demand; nothing is written outside this -command. Set `--output ` to write the migration artifact to that -destination instead of applying it in place. Use `-` for stdout. +command. Set `--output ` to write a migrated config to that destination +instead of applying it in place. Use `-` for stdout. When both user and project +config need migration, stdout emits a labeled inspection artifact and file +output fails rather than combining the configs. Output artifacts omit deprecated `approved-commands`; only in-place updates move those entries to `approvals.toml`. diff --git a/skills/worktrunk/reference/config.md b/skills/worktrunk/reference/config.md index abe32bd44..da928286a 100644 --- a/skills/worktrunk/reference/config.md +++ b/skills/worktrunk/reference/config.md @@ -812,8 +812,10 @@ currently `[list] json-schema = 2` — so the switch happens as a reviewed confi edit rather than at upgrade. Shows a diff and asks for confirmation. Migrations are computed in memory on demand; nothing is written outside this -command. Set `--output ` to write the migration artifact to that -destination instead of applying it in place. Use `-` for stdout. +command. Set `--output ` to write a migrated config to that destination +instead of applying it in place. Use `-` for stdout. When both user and project +config need migration, stdout emits a labeled inspection artifact and file +output fails rather than combining the configs. Output artifacts omit deprecated `approved-commands`; only in-place updates move those entries to `approvals.toml`. diff --git a/src/cli/config.rs b/src/cli/config.rs index f9da7ab10..4003c29ae 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -596,8 +596,10 @@ currently `[list] json-schema = 2` — so the switch happens as a reviewed confi edit rather than at upgrade. Shows a diff and asks for confirmation. Migrations are computed in memory on demand; nothing is written outside this -command. Set `--output ` to write the migration artifact to that -destination instead of applying it in place. Use `-` for stdout. +command. Set `--output ` to write a migrated config to that destination +instead of applying it in place. Use `-` for stdout. When both user and project +config need migration, stdout emits a labeled inspection artifact and file +output fails rather than combining the configs. Output artifacts omit deprecated `approved-commands`; only in-place updates move those entries to `approvals.toml`. diff --git a/src/commands/config/update.rs b/src/commands/config/update.rs index 6ccabb6f2..dee5ab5cc 100644 --- a/src/commands/config/update.rs +++ b/src/commands/config/update.rs @@ -8,7 +8,7 @@ use std::fmt::Write as _; use std::path::{Path, PathBuf}; -use anyhow::Context; +use anyhow::{Context, bail}; use color_print::cformat; use worktrunk::config::{ ConfigFileKind, DeprecationInfo, DeprecationKind, compute_migrated_content, config_path, @@ -107,18 +107,24 @@ pub fn handle_config_update(yes: bool, output: Option) -> anyhow::Resul /// Write the migration artifact to a path, or to stdout when the path is `-`. fn write_migrated_output(output: &Path, candidates: &[UpdateCandidate]) -> anyhow::Result<()> { - let artifact = format_migrated_output(candidates); + let stdout = output == Path::new("-"); - if output == Path::new("-") { - // A clean config produces no output, so stdout composes in a pipe. - print!("{artifact}"); + if candidates.is_empty() { + if !stdout { + eprintln!("{}", info_message("No deprecated settings found")); + } return Ok(()); } - if candidates.is_empty() { - // A clean config has no artifact. Replacing the destination with an - // empty file could silently discard user data. - eprintln!("{}", info_message("No deprecated settings found")); + if !stdout && candidates.len() > 1 { + bail!( + "Cannot write multiple migrated configs to one file; use --output=- to inspect them or run wt config update to apply them in place" + ); + } + + let artifact = format_migrated_output(candidates); + if stdout { + print!("{artifact}"); return Ok(()); } diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index 3ec3c347b..36cc83337 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3401,10 +3401,10 @@ fn test_config_update_project_config_from_linked_worktree_shows_hint(repo: TestR ); } -/// File and stdout output receive the same labeled artifact when both config -/// files have migrations, without changing either source config. +/// Multiple migrated configs can be inspected on stdout but cannot be written +/// as one config file, which would change TOML table scope between documents. #[rstest] -fn test_config_update_output_destinations_emit_same_artifact(repo: TestRepo) { +fn test_config_update_output_file_rejects_multiple_configs(repo: TestRepo) { let user_config_path = repo.test_config_path(); let user_original = r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" "#; @@ -3427,7 +3427,11 @@ fn test_config_update_output_destinations_emit_same_artifact(repo: TestRepo) { assert!(stdout.contains("{{ repo }}")); assert!(stdout.contains("pre-start")); - fs::write(repo.root_path().join("migrated.toml"), "stale\n").unwrap(); + fs::write( + repo.root_path().join("migrated.toml"), + "important user data\n", + ) + .unwrap(); let mut file_command = repo.wt_command(); file_command .current_dir(repo.root_path().parent().unwrap()) @@ -3435,13 +3439,17 @@ fn test_config_update_output_destinations_emit_same_artifact(repo: TestRepo) { .arg(repo.root_path()) .args(["config", "update", "--output=migrated.toml"]); let file_output = file_command.output().unwrap(); - assert!(file_output.status.success()); + assert!(!file_output.status.success()); assert!(file_output.stdout.is_empty()); - assert!(file_output.stderr.is_empty()); assert_eq!( - fs::read(repo.root_path().join("migrated.toml")).unwrap(), - stdout_output.stdout, - "file and stdout destinations must receive the same artifact" + fs::read_to_string(repo.root_path().join("migrated.toml")).unwrap(), + "important user data\n" + ); + assert!( + String::from_utf8_lossy(&file_output.stderr) + .contains("Cannot write multiple migrated configs to one file"), + "unexpected stderr: {}", + String::from_utf8_lossy(&file_output.stderr) ); assert_eq!(fs::read_to_string(user_config_path).unwrap(), user_original); assert_eq!( @@ -3450,6 +3458,36 @@ fn test_config_update_output_destinations_emit_same_artifact(repo: TestRepo) { ); } +/// File and stdout output receive the same bytes for one migrated config. +#[rstest] +fn test_config_update_output_destinations_emit_same_config(repo: TestRepo) { + let config_path = repo.test_config_path(); + let original = r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" +"#; + fs::write(config_path, original).unwrap(); + + let stdout_output = repo + .wt_command() + .args(["config", "update", "--output=-"]) + .output() + .unwrap(); + assert!(stdout_output.status.success()); + assert!(stdout_output.stderr.is_empty()); + + let destination = repo.root_path().join("migrated.toml"); + fs::write(&destination, "stale\n").unwrap(); + let file_output = repo + .wt_command() + .args(["config", "update", "--output=migrated.toml"]) + .output() + .unwrap(); + assert!(file_output.status.success()); + assert!(file_output.stdout.is_empty()); + assert!(file_output.stderr.is_empty()); + assert_eq!(fs::read(destination).unwrap(), stdout_output.stdout); + assert_eq!(fs::read_to_string(config_path).unwrap(), original); +} + /// `wt config update --output=-` on a clean config exits silently with empty /// stdout — no "nothing to do" noise to corrupt a pipe. #[rstest] diff --git a/tests/snapshots/integration__integration_tests__help__help_config_update.snap b/tests/snapshots/integration__integration_tests__help__help_config_update.snap index fa73cdd4d..9919c3286 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_update.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_update.snap @@ -77,8 +77,10 @@ currently [list] json-schema = 2 — so the switch happens as a reviewed edit rather than at upgrade. Shows a diff and asks for confirmation. Migrations are computed in memory on demand; nothing is written outside this -command. Set --output  to write the migration artifact to that -destination instead of applying it in place. Use - for stdout. +command. Set --output  to write a migrated config to that destination +instead of applying it in place. Use - for stdout. When both user and project +config need migration, stdout emits a labeled inspection artifact and file +output fails rather than combining the configs. Output artifacts omit deprecated approved-commands; only in-place updates move those entries to approvals.toml. From 2e5bc4973db900e78f8b6265e2891e21b64132f7 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 6 Sep 2026 14:03:22 -0700 Subject: [PATCH 5/6] Update config page TOC contract --- docs/tests/built-site.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tests/built-site.test.mjs b/docs/tests/built-site.test.mjs index a9caf8009..053bf60e2 100644 --- a/docs/tests/built-site.test.mjs +++ b/docs/tests/built-site.test.mjs @@ -223,7 +223,7 @@ test('build preserves the public route contract', async () => { assert.ok(configToc, 'config page is missing its desktop table of contents'); assert.equal( [...configToc.matchAll(/ Date: Sun, 6 Sep 2026 14:07:39 -0700 Subject: [PATCH 6/6] Clarify multi-config output error --- src/commands/config/update.rs | 6 +++--- tests/integration_tests/config_show.rs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/commands/config/update.rs b/src/commands/config/update.rs index dee5ab5cc..a2d4c7a23 100644 --- a/src/commands/config/update.rs +++ b/src/commands/config/update.rs @@ -117,9 +117,9 @@ fn write_migrated_output(output: &Path, candidates: &[UpdateCandidate]) -> anyho } if !stdout && candidates.len() > 1 { - bail!( - "Cannot write multiple migrated configs to one file; use --output=- to inspect them or run wt config update to apply them in place" - ); + bail!(cformat!( + "Cannot write user config and project config migrations to one file; use --output=- to inspect both or run wt config update to apply them in place" + )); } let artifact = format_migrated_output(candidates); diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index 36cc83337..3897c328d 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3445,11 +3445,12 @@ fn test_config_update_output_file_rejects_multiple_configs(repo: TestRepo) { fs::read_to_string(repo.root_path().join("migrated.toml")).unwrap(), "important user data\n" ); + let stderr = String::from_utf8_lossy(&file_output.stderr) + .ansi_strip() + .into_owned(); assert!( - String::from_utf8_lossy(&file_output.stderr) - .contains("Cannot write multiple migrated configs to one file"), - "unexpected stderr: {}", - String::from_utf8_lossy(&file_output.stderr) + stderr.contains("Cannot write user config and project config migrations to one file"), + "unexpected stderr: {stderr}" ); assert_eq!(fs::read_to_string(user_config_path).unwrap(), user_original); assert_eq!(