From c5eff9bc0f5e7599875c321dbe982ba5cbfed8b3 Mon Sep 17 00:00:00 2001 From: DJ Date: Mon, 30 Mar 2026 19:48:16 -0700 Subject: [PATCH 01/12] ci: add cross-platform test matrix Run the verify job on ubuntu-24.04, macos-latest, and windows-latest using a matrix strategy with fail-fast disabled so all platforms report results independently. Refs #11 (item 3) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc8c481..2d470de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,12 @@ permissions: jobs: verify: - name: Verify - runs-on: ubuntu-24.04 + name: Verify (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-latest, windows-latest] steps: - name: Check out repository uses: actions/checkout@v5 From 52b77c85a23d67fcfb3dd75f105874b33a9e1bc0 Mon Sep 17 00:00:00 2001 From: DJ Date: Tue, 31 Mar 2026 20:14:29 -0700 Subject: [PATCH 02/12] fix: resolve Windows cross-compilation errors in test code Replace direct uses of std::os::unix::process::ExitStatusExt in test code with cfg-gated helper functions that compile on both Unix and Windows platforms. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/commit/mod.rs | 34 +++++++++++++++++++++++++++++++--- src/cli/merge/mod.rs | 16 +++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/cli/commit/mod.rs b/src/cli/commit/mod.rs index aaa5513..a973c70 100644 --- a/src/cli/commit/mod.rs +++ b/src/cli/commit/mod.rs @@ -136,7 +136,35 @@ mod tests { use crate::core::commit::{CommitEntry, CommitOptions, CommitOutcome}; use crate::core::restack::RestackPreview; use clap::FromArgMatches; - use std::os::unix::process::ExitStatusExt; + use std::process::ExitStatus; + + /// Create an `ExitStatus` representing a successful (code 0) process. + fn exit_status_success() -> ExitStatus { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + ExitStatus::from_raw(0) + } + #[cfg(windows)] + { + use std::os::windows::process::ExitStatusExt; + ExitStatus::from_raw(0) + } + } + + /// Create an `ExitStatus` representing a failed (non-zero) process. + fn exit_status_failure() -> ExitStatus { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + ExitStatus::from_raw(1 << 8) // encodes exit code 1 + } + #[cfg(windows)] + { + use std::os::windows::process::ExitStatusExt; + ExitStatus::from_raw(1) + } + } #[test] fn converts_cli_args_into_core_commit_options() { @@ -197,7 +225,7 @@ mod tests { #[test] fn formats_commit_output_with_summary_and_blank_line_before_log() { let outcome = CommitOutcome { - status: std::process::ExitStatus::from_raw(0), + status: exit_status_success(), commit_succeeded: true, summary_line: Some("10 files changed, 2245 insertions(+)".into()), recent_commits: vec![CommitEntry { @@ -220,7 +248,7 @@ mod tests { #[test] fn formats_commit_output_with_restack_section() { let outcome = CommitOutcome { - status: std::process::ExitStatus::from_raw(1 << 8), + status: exit_status_failure(), commit_succeeded: true, summary_line: Some("1 file changed, 1 insertion(+)".into()), recent_commits: vec![CommitEntry { diff --git a/src/cli/merge/mod.rs b/src/cli/merge/mod.rs index d4afb16..6633336 100644 --- a/src/cli/merge/mod.rs +++ b/src/cli/merge/mod.rs @@ -236,8 +236,22 @@ mod tests { use super::{MergeArgs, format_merge_plan, format_merge_success_output}; use crate::core::merge::{MergeMode, MergeOptions, MergePlan, MergeTreeNode}; use crate::core::restack::RestackPreview; + use std::process::ExitStatus; use uuid::Uuid; + fn exit_status_success() -> ExitStatus { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + ExitStatus::from_raw(0) + } + #[cfg(windows)] + { + use std::os::windows::process::ExitStatusExt; + ExitStatus::from_raw(0) + } + } + #[test] fn converts_cli_args_into_core_merge_options() { let options = MergeOptions::from(MergeArgs { @@ -307,7 +321,7 @@ mod tests { restack_plan: vec![], }, &crate::core::merge::MergeOutcome { - status: std::os::unix::process::ExitStatusExt::from_raw(0), + status: exit_status_success(), switched_to_target_from: Some("feat/auth-api".into()), restacked_branches: vec![RestackPreview { branch_name: "feat/auth-api-tests".into(), From 15ebdcc4de130ae85a7688c5b782d07d425d1db6 Mon Sep 17 00:00:00 2001 From: DJ Date: Thu, 2 Apr 2026 04:58:00 -0700 Subject: [PATCH 03/12] fix(test): make PR base update test work on Windows CI The test `reports_pull_request_base_updates_before_each_retarget` failed on Windows because it used a Unix shell script as a fake `gh` executable which Windows cannot execute. The real `gh` CLI was found instead, failing due to missing GH_TOKEN in CI. Three fixes: - Use a .cmd batch script on Windows instead of a shell script - Use platform-appropriate PATH separator (`;` on Windows, `:` on Unix) - Create the fake executable with .cmd extension on Windows so Command::new("gh") can discover it Co-Authored-By: Claude Opus 4.6 (1M context) --- src/core/sync.rs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/core/sync.rs b/src/core/sync.rs index 354bd1d..d2d32c8 100644 --- a/src/core/sync.rs +++ b/src/core/sync.rs @@ -1579,14 +1579,20 @@ mod tests { fn install_fake_executable(bin_dir: &Path, name: &str, script: &str) { fs::create_dir_all(bin_dir).unwrap(); - let path = bin_dir.join(name); - fs::write(&path, script).unwrap(); #[cfg(unix)] { + let path = bin_dir.join(name); + fs::write(&path, script).unwrap(); let mut permissions = fs::metadata(&path).unwrap().permissions(); permissions.set_mode(0o755); fs::set_permissions(path, permissions).unwrap(); } + #[cfg(windows)] + { + // On Windows, Command::new("gh") finds gh.cmd in PATH + let path = bin_dir.join(format!("{name}.cmd")); + fs::write(&path, script).unwrap(); + } } fn path_with_prepend(dir: &Path) -> String { @@ -1594,7 +1600,8 @@ mod tests { if existing_path.is_empty() { dir.display().to_string() } else { - format!("{}:{existing_path}", dir.display()) + let sep = if cfg!(windows) { ";" } else { ":" }; + format!("{}{sep}{existing_path}", dir.display()) } } @@ -1836,14 +1843,17 @@ mod tests { let bin_dir = repo.join("fake-bin"); let log_path = repo.join("gh.log"); - install_fake_executable( - &bin_dir, - "gh", - &format!( - "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"{}\"\n", - log_path.display() - ), + #[cfg(unix)] + let script = format!( + "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"{}\"\n", + log_path.display() + ); + #[cfg(windows)] + let script = format!( + "@echo off\r\necho %* >> \"{}\"\r\n", + log_path.display() ); + install_fake_executable(&bin_dir, "gh", &script); fs::write(&log_path, "").unwrap(); let _path_guard = EnvVarGuard::set("PATH", path_with_prepend(&bin_dir)); From 6fad6d02f49656f7c2df4d2ce0f5e980a34b06c1 Mon Sep 17 00:00:00 2001 From: DJ Date: Thu, 2 Apr 2026 11:01:39 -0700 Subject: [PATCH 04/12] fix(style): collapse multi-line format! to satisfy rustfmt on Windows Co-Authored-By: Claude Opus 4.6 (1M context) --- src/core/sync.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/sync.rs b/src/core/sync.rs index d2d32c8..d45d132 100644 --- a/src/core/sync.rs +++ b/src/core/sync.rs @@ -1849,10 +1849,7 @@ mod tests { log_path.display() ); #[cfg(windows)] - let script = format!( - "@echo off\r\necho %* >> \"{}\"\r\n", - log_path.display() - ); + let script = format!("@echo off\r\necho %* >> \"{}\"\r\n", log_path.display()); install_fake_executable(&bin_dir, "gh", &script); fs::write(&log_path, "").unwrap(); let _path_guard = EnvVarGuard::set("PATH", path_with_prepend(&bin_dir)); From 07e754b05c9400b121904ea56abda6497a15e595 Mon Sep 17 00:00:00 2001 From: DJ Date: Thu, 2 Apr 2026 19:43:27 -0700 Subject: [PATCH 05/12] fix(test): use DAGGER_GH_BIN env var to resolve fake gh on Windows On Windows, Command::new("gh") only resolves gh.exe via CreateProcessW, not gh.cmd scripts. The previous approach of prepending a fake-bin dir to PATH with a gh.cmd wrapper was ineffective because the real gh.exe was always found first. Introduce a gh_program() helper that checks DAGGER_GH_BIN before falling back to "gh". The test now sets this env var on Windows to point directly at the .cmd wrapper, bypassing PATH resolution entirely. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/core/gh.rs | 15 ++++++++++++--- src/core/sync.rs | 5 +++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/core/gh.rs b/src/core/gh.rs index 62e05aa..8a124d2 100644 --- a/src/core/gh.rs +++ b/src/core/gh.rs @@ -1,7 +1,7 @@ use std::io; use std::io::{Read, Write}; use std::process::{Command, ExitStatus, Output, Stdio}; -use std::thread; +use std::{env, thread}; use serde::Deserialize; @@ -352,8 +352,17 @@ fn pull_request_number_from_url(url: &str) -> Option { (!digits.is_empty()).then(|| digits.parse().ok()).flatten() } +/// Returns the program name used to invoke the GitHub CLI. +/// +/// Defaults to `"gh"` but can be overridden via the `DAGGER_GH_BIN` environment +/// variable, which is useful for testing on platforms where `Command::new("gh")` +/// does not resolve non-`.exe` scripts (e.g. `.cmd` wrappers on Windows). +fn gh_program() -> String { + env::var("DAGGER_GH_BIN").unwrap_or_else(|_| "gh".to_string()) +} + fn run_gh_capture_output(args: &[String]) -> io::Result { - let output = Command::new("gh") + let output = Command::new(gh_program()) .args(args) .output() .map_err(normalize_gh_spawn_error)?; @@ -375,7 +384,7 @@ fn run_gh_command(command_name: &str, args: &[String]) -> io::Result<()> { } fn run_gh_with_live_output(args: &[String]) -> io::Result { - let mut child = Command::new("gh") + let mut child = Command::new(gh_program()) .args(args) .stdin(Stdio::inherit()) .stdout(Stdio::piped()) diff --git a/src/core/sync.rs b/src/core/sync.rs index d45d132..90fdfc1 100644 --- a/src/core/sync.rs +++ b/src/core/sync.rs @@ -1853,6 +1853,11 @@ mod tests { install_fake_executable(&bin_dir, "gh", &script); fs::write(&log_path, "").unwrap(); let _path_guard = EnvVarGuard::set("PATH", path_with_prepend(&bin_dir)); + // On Windows, Command::new("gh") only resolves gh.exe, not gh.cmd. + // Point DAGGER_GH_BIN at the .cmd wrapper so gh_program() uses it directly. + #[cfg(windows)] + let _gh_bin_guard = + EnvVarGuard::set("DAGGER_GH_BIN", bin_dir.join("gh.cmd").display().to_string()); let plan = PullRequestUpdatePlan { actions: vec![ From a16fd52f7c9bda777c5e6e4ab38fd87ffc304f18 Mon Sep 17 00:00:00 2001 From: DJ Date: Fri, 3 Apr 2026 11:31:07 -0700 Subject: [PATCH 06/12] style: fix cargo fmt formatting for DAGGER_GH_BIN guard Co-Authored-By: Claude Opus 4.6 (1M context) --- src/core/sync.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/sync.rs b/src/core/sync.rs index 90fdfc1..c53cf73 100644 --- a/src/core/sync.rs +++ b/src/core/sync.rs @@ -1856,8 +1856,10 @@ mod tests { // On Windows, Command::new("gh") only resolves gh.exe, not gh.cmd. // Point DAGGER_GH_BIN at the .cmd wrapper so gh_program() uses it directly. #[cfg(windows)] - let _gh_bin_guard = - EnvVarGuard::set("DAGGER_GH_BIN", bin_dir.join("gh.cmd").display().to_string()); + let _gh_bin_guard = EnvVarGuard::set( + "DAGGER_GH_BIN", + bin_dir.join("gh.cmd").display().to_string(), + ); let plan = PullRequestUpdatePlan { actions: vec![ From 5a2f9fa1b5600d55be06b046555b54cc9ba3ca57 Mon Sep 17 00:00:00 2001 From: DJ Date: Fri, 3 Apr 2026 18:09:48 -0700 Subject: [PATCH 07/12] fix(test): make branch PR test work on Windows CI The init_lineage_shows_tracked_pull_request_numbers test failed on Windows because the fake gh mock was a Unix shell script that Windows cannot execute. - Add Windows .cmd batch script variant for the fake gh in branch tests - install_fake_executable now writes .cmd files on Windows - path_with_prepend uses ; separator on Windows instead of : - git_binary_path uses where instead of which on Windows - Set DAGGER_GH_BIN env var so gh_program() finds the .cmd wrapper Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/branch.rs | 31 ++++++++++++++++++++++++++++--- tests/support/mod.rs | 28 ++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/tests/branch.rs b/tests/branch.rs index 0d7fb8b..40a5126 100644 --- a/tests/branch.rs +++ b/tests/branch.rs @@ -7,8 +7,13 @@ use support::{ load_state_json, path_with_prepend, strip_ansi, with_temp_repo, }; -fn install_fake_gh(repo: &Path, script: &str) -> (PathBuf, String) { +fn install_fake_gh(repo: &Path, unix_script: &str, windows_script: &str) -> (PathBuf, String) { let bin_dir = repo.join("fake-bin"); + let script = if cfg!(windows) { + windows_script + } else { + unix_script + }; install_fake_executable(&bin_dir, "gh", script); let path = path_with_prepend(&bin_dir); @@ -58,7 +63,7 @@ fn init_lineage_shows_tracked_pull_request_numbers() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path) = install_fake_gh( + let (bin_dir, path) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -72,10 +77,30 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/123 + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); - dgr_ok_with_env(repo, &["pr"], &[("PATH", path.as_str())]); + let gh_bin = bin_dir.join(if cfg!(windows) { "gh.cmd" } else { "gh" }); + dgr_ok_with_env( + repo, + &["pr"], + &[ + ("PATH", path.as_str()), + ("DAGGER_GH_BIN", gh_bin.to_str().unwrap()), + ], + ); let output = dgr_ok(repo, &["init"]); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); diff --git a/tests/support/mod.rs b/tests/support/mod.rs index d06eee1..80198c0 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -161,30 +161,46 @@ pub fn git_stdout(repo: &Path, args: &[&str]) -> String { pub fn install_fake_executable(bin_dir: &Path, name: &str, script: &str) { fs::create_dir_all(bin_dir).unwrap(); - let path = bin_dir.join(name); - fs::write(&path, script).unwrap(); + #[cfg(unix)] { + let path = bin_dir.join(name); + fs::write(&path, script).unwrap(); let mut permissions = fs::metadata(&path).unwrap().permissions(); permissions.set_mode(0o755); fs::set_permissions(path, permissions).unwrap(); } + + #[cfg(windows)] + { + let path = bin_dir.join(format!("{name}.cmd")); + fs::write(&path, script).unwrap(); + } } pub fn path_with_prepend(dir: &Path) -> String { let existing_path = std::env::var("PATH").unwrap_or_default(); + let separator = if cfg!(windows) { ";" } else { ":" }; if existing_path.is_empty() { dir.display().to_string() } else { - format!("{}:{existing_path}", dir.display()) + format!("{}{separator}{existing_path}", dir.display()) } } pub fn git_binary_path() -> String { - let output = Command::new("which").arg("git").output().unwrap(); - assert!(output.status.success(), "which git failed"); + let cmd = if cfg!(windows) { "where" } else { "which" }; + let output = Command::new(cmd).arg("git").output().unwrap(); + assert!(output.status.success(), "{cmd} git failed"); - String::from_utf8(output.stdout).unwrap().trim().to_string() + // `where` on Windows may return multiple lines; take the first. + String::from_utf8(output.stdout) + .unwrap() + .lines() + .next() + .unwrap() + .trim() + .to_string() } pub fn load_state_json(repo: &Path) -> Value { From 31cabf198215eb245fea410d48bcf93615aacf01 Mon Sep 17 00:00:00 2001 From: DJ Date: Fri, 3 Apr 2026 20:13:08 -0700 Subject: [PATCH 08/12] fix(test): make all pr.rs tests work on Windows CI Add Windows .cmd equivalents for every fake gh shell script in pr.rs and pass DAGGER_GH_BIN to ensure the correct executable is resolved. This follows the same cross-platform pattern already applied to branch.rs and support/mod.rs. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/pr.rs | 338 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 261 insertions(+), 77 deletions(-) diff --git a/tests/pr.rs b/tests/pr.rs index 74f0ddc..d72292d 100644 --- a/tests/pr.rs +++ b/tests/pr.rs @@ -10,14 +10,27 @@ use support::{ path_with_prepend, strip_ansi, with_temp_repo, }; -fn install_fake_gh(repo: &Path, script: &str) -> (PathBuf, String, String) { +fn install_fake_gh( + repo: &Path, + unix_script: &str, + windows_script: &str, +) -> (PathBuf, String, String, String) { let bin_dir = repo.join("fake-bin"); + let script = if cfg!(windows) { + windows_script + } else { + unix_script + }; install_fake_executable(&bin_dir, "gh", script); let path = path_with_prepend(&bin_dir); let log_path = repo.join("gh.log").display().to_string(); + let gh_bin = bin_dir + .join(if cfg!(windows) { "gh.cmd" } else { "gh" }) + .display() + .to_string(); - (bin_dir, path, log_path) + (bin_dir, path, log_path, gh_bin) } fn clear_log(path: &str) { @@ -51,7 +64,7 @@ fn pr_creates_root_pull_request_tracks_number_and_updates_tree() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -66,6 +79,19 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/123 + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -82,6 +108,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -131,7 +158,7 @@ fn pr_merge_retargets_open_child_pull_request_before_merging_parent() { track_pull_request_number(repo, "feat/auth-ui", 124); git_ok(repo, &["checkout", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -148,6 +175,21 @@ if [ "$1" = "pr" ] && [ "$2" = "merge" ] && [ "$3" = "123" ] && [ "$4" = "--squa fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="view" if "%3"=="124" ( + echo {"number":124,"state":"OPEN","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-ui","headRefOid":"abc123","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/124"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="edit" if "%3"=="124" if "%4"=="--base" if "%5"=="main" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="merge" if "%3"=="123" if "%4"=="--squash" if "%5"=="--delete-branch" ( + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -157,6 +199,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -186,7 +229,7 @@ fn pr_creates_child_pull_request_against_tracked_parent() { dgr_ok(repo, &["branch", "feat/auth"]); dgr_ok(repo, &["branch", "feat/auth-api"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -200,6 +243,18 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then exit 0 fi exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/234 + exit /b 0 +) +exit /b 1 "#, ); @@ -209,6 +264,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -233,7 +289,7 @@ fn pr_defaults_body_to_title_when_body_is_omitted() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -248,6 +304,19 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/321 + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -257,6 +326,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -283,7 +353,7 @@ fn pr_adopts_matching_open_pull_request_without_creating_another() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -294,6 +364,15 @@ if [ "$1" = "pr" ] && [ "$2" = "list" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [{"number":345,"baseRefName":"main","url":"https://github.com/oneirosoft/dagger/pull/345"}] + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -303,6 +382,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -333,7 +413,7 @@ fn pr_is_idempotent_when_branch_already_tracks_pull_request() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (bin_dir, path, log_path) = install_fake_gh( + let (bin_dir, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -347,6 +427,18 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then exit 0 fi exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/456 + exit /b 0 +) +exit /b 1 "#, ); @@ -356,18 +448,18 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); install_fake_executable( &bin_dir, "gh", - r#"#!/bin/sh -set -eu -printf '%s\n' "$*" >> "$DGR_TEST_GH_LOG" -echo "gh should not have been called" >&2 -exit 99 -"#, + if cfg!(windows) { + "@echo off\r\necho %* >> \"%DGR_TEST_GH_LOG%\"\r\necho gh should not have been called 1>&2\r\nexit /b 99\r\n" + } else { + "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"$DGR_TEST_GH_LOG\"\necho \"gh should not have been called\" >&2\nexit 99\n" + }, ); let output = dgr_ok_with_env( @@ -376,6 +468,7 @@ exit 99 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -394,7 +487,7 @@ fn pr_with_view_only_opens_tracked_pull_request_in_browser() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (bin_dir, path, log_path) = install_fake_gh( + let (bin_dir, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -408,6 +501,18 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then exit 0 fi exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/456 + exit /b 0 +) +exit /b 1 "#, ); @@ -417,6 +522,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); @@ -424,15 +530,11 @@ exit 1 install_fake_executable( &bin_dir, "gh", - r#"#!/bin/sh -set -eu -printf '%s\n' "$*" >> "$DGR_TEST_GH_LOG" -if [ "$1" = "pr" ] && [ "$2" = "view" ] && [ "$3" = "456" ] && [ "$4" = "--web" ]; then - exit 0 -fi -echo "unexpected gh args: $*" >&2 -exit 1 -"#, + if cfg!(windows) { + "@echo off\r\necho %* >> \"%DGR_TEST_GH_LOG%\"\r\nif \"%1\"==\"pr\" if \"%2\"==\"view\" if \"%3\"==\"456\" if \"%4\"==\"--web\" (\r\n exit /b 0\r\n)\r\necho unexpected gh args: %* 1>&2\r\nexit /b 1\r\n" + } else { + "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"$DGR_TEST_GH_LOG\"\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"view\" ] && [ \"$3\" = \"456\" ] && [ \"$4\" = \"--web\" ]; then\n exit 0\nfi\necho \"unexpected gh args: $*\" >&2\nexit 1\n" + }, ); let output = dgr_ok_with_env( @@ -441,6 +543,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); @@ -457,7 +560,7 @@ fn pr_with_create_and_view_opens_browser_after_tracking() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -475,6 +578,22 @@ if [ "$1" = "pr" ] && [ "$2" = "view" ] && [ "$3" = "123" ] && [ "$4" = "--web" fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/123 + exit /b 0 +) +if "%1"=="pr" if "%2"=="view" if "%3"=="123" if "%4"=="--web" ( + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -484,6 +603,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -517,7 +637,7 @@ fn pr_prompts_to_push_branch_before_creating_pull_request() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -532,6 +652,19 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/777 + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -542,6 +675,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); assert!(output.status.success()); @@ -582,7 +716,7 @@ fn pr_declining_push_skips_pull_request_creation() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -593,6 +727,15 @@ if [ "$1" = "pr" ] && [ "$2" = "list" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -603,6 +746,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); assert!(output.status.success()); @@ -635,7 +779,7 @@ fn pr_list_renders_open_tracked_pull_requests_in_lineage_order() { initialize_main_repo(repo); dgr_ok(repo, &["init"]); - let (bin_dir, path, log_path) = install_fake_gh( + let (bin_dir, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -657,6 +801,26 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + for /f "delims=" %%b in ('git branch --show-current') do set "CURRENT_BRANCH=%%b" + if "%CURRENT_BRANCH%"=="feat/auth" ( + echo https://github.com/oneirosoft/dagger/pull/101 + exit /b 0 + ) + if "%CURRENT_BRANCH%"=="feat/auth-ui" ( + echo https://github.com/oneirosoft/dagger/pull/102 + exit /b 0 + ) +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -667,6 +831,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); dgr_ok(repo, &["branch", "feat/auth-ui"]); @@ -676,6 +841,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); @@ -683,16 +849,11 @@ exit 1 install_fake_executable( &bin_dir, "gh", - r#"#!/bin/sh -set -eu -printf '%s\n' "$*" >> "$DGR_TEST_GH_LOG" -if [ "$1" = "pr" ] && [ "$2" = "list" ] && [ "$3" = "--state" ] && [ "$4" = "open" ]; then - printf '[{"number":101,"title":"Auth PR","url":"https://github.com/oneirosoft/dagger/pull/101"},{"number":102,"title":"Auth UI PR","url":"https://github.com/oneirosoft/dagger/pull/102"},{"number":999,"title":"External PR","url":"https://github.com/oneirosoft/dagger/pull/999"}]\n' - exit 0 -fi -echo "unexpected gh args: $*" >&2 -exit 1 -"#, + if cfg!(windows) { + "@echo off\r\necho %* >> \"%DGR_TEST_GH_LOG%\"\r\nif \"%1\"==\"pr\" if \"%2\"==\"list\" if \"%3\"==\"--state\" if \"%4\"==\"open\" (\r\n echo [{\"number\":101,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/101\"},{\"number\":102,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/102\"},{\"number\":999,\"title\":\"External PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/999\"}]\r\n exit /b 0\r\n)\r\necho unexpected gh args: %* 1>&2\r\nexit /b 1\r\n" + } else { + "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"$DGR_TEST_GH_LOG\"\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ] && [ \"$3\" = \"--state\" ] && [ \"$4\" = \"open\" ]; then\n printf '[{\"number\":101,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/101\"},{\"number\":102,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/102\"},{\"number\":999,\"title\":\"External PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/999\"}]\\n'\n exit 0\nfi\necho \"unexpected gh args: $*\" >&2\nexit 1\n" + }, ); let output = dgr_ok_with_env( @@ -701,6 +862,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -721,7 +883,7 @@ fn pr_list_with_view_opens_each_listed_pull_request() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (bin_dir, path, log_path) = install_fake_gh( + let (bin_dir, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -735,6 +897,18 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then exit 0 fi exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo https://github.com/oneirosoft/dagger/pull/301 + exit /b 0 +) +exit /b 1 "#, ); @@ -744,34 +918,18 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); dgr_ok(repo, &["branch", "feat/auth-ui"]); install_fake_executable( &bin_dir, "gh", - r#"#!/bin/sh -set -eu -printf '%s\n' "$*" >> "$DGR_TEST_GH_LOG" -if [ "$1" = "pr" ] && [ "$2" = "list" ]; then - current_branch="$(git branch --show-current)" - if [ "$current_branch" = "feat/auth-ui" ] && [ "$3" = "--head" ]; then - printf '[]\n' - exit 0 - fi - printf '[{"number":301,"title":"Auth PR","url":"https://github.com/oneirosoft/dagger/pull/301"},{"number":302,"title":"Auth UI PR","url":"https://github.com/oneirosoft/dagger/pull/302"}]\n' - exit 0 -fi -if [ "$1" = "pr" ] && [ "$2" = "create" ]; then - printf 'https://github.com/oneirosoft/dagger/pull/302\n' - exit 0 -fi -if [ "$1" = "pr" ] && [ "$2" = "view" ] && [ "$4" = "--web" ]; then - exit 0 -fi -echo "unexpected gh args: $*" >&2 -exit 1 -"#, + if cfg!(windows) { + "@echo off\r\necho %* >> \"%DGR_TEST_GH_LOG%\"\r\nif \"%1\"==\"pr\" if \"%2\"==\"list\" (\r\n for /f \"delims=\" %%b in ('git branch --show-current') do set \"CURRENT_BRANCH=%%b\"\r\n if \"%CURRENT_BRANCH%\"==\"feat/auth-ui\" if \"%3\"==\"--head\" (\r\n echo []\r\n exit /b 0\r\n )\r\n echo [{\"number\":301,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/301\"},{\"number\":302,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/302\"}]\r\n exit /b 0\r\n)\r\nif \"%1\"==\"pr\" if \"%2\"==\"create\" (\r\n echo https://github.com/oneirosoft/dagger/pull/302\r\n exit /b 0\r\n)\r\nif \"%1\"==\"pr\" if \"%2\"==\"view\" if \"%4\"==\"--web\" (\r\n exit /b 0\r\n)\r\necho unexpected gh args: %* 1>&2\r\nexit /b 1\r\n" + } else { + "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"$DGR_TEST_GH_LOG\"\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ]; then\n current_branch=\"$(git branch --show-current)\"\n if [ \"$current_branch\" = \"feat/auth-ui\" ] && [ \"$3\" = \"--head\" ]; then\n printf '[]\\n'\n exit 0\n fi\n printf '[{\"number\":301,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/301\"},{\"number\":302,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/302\"}]\\n'\n exit 0\nfi\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"create\" ]; then\n printf 'https://github.com/oneirosoft/dagger/pull/302\\n'\n exit 0\nfi\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"view\" ] && [ \"$4\" = \"--web\" ]; then\n exit 0\nfi\necho \"unexpected gh args: $*\" >&2\nexit 1\n" + }, ); dgr_ok_with_env( repo, @@ -779,6 +937,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); @@ -786,19 +945,11 @@ exit 1 install_fake_executable( &bin_dir, "gh", - r#"#!/bin/sh -set -eu -printf '%s\n' "$*" >> "$DGR_TEST_GH_LOG" -if [ "$1" = "pr" ] && [ "$2" = "list" ] && [ "$3" = "--state" ] && [ "$4" = "open" ]; then - printf '[{"number":301,"title":"Auth PR","url":"https://github.com/oneirosoft/dagger/pull/301"},{"number":302,"title":"Auth UI PR","url":"https://github.com/oneirosoft/dagger/pull/302"}]\n' - exit 0 -fi -if [ "$1" = "pr" ] && [ "$2" = "view" ] && [ "$4" = "--web" ]; then - exit 0 -fi -echo "unexpected gh args: $*" >&2 -exit 1 -"#, + if cfg!(windows) { + "@echo off\r\necho %* >> \"%DGR_TEST_GH_LOG%\"\r\nif \"%1\"==\"pr\" if \"%2\"==\"list\" if \"%3\"==\"--state\" if \"%4\"==\"open\" (\r\n echo [{\"number\":301,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/301\"},{\"number\":302,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/302\"}]\r\n exit /b 0\r\n)\r\nif \"%1\"==\"pr\" if \"%2\"==\"view\" if \"%4\"==\"--web\" (\r\n exit /b 0\r\n)\r\necho unexpected gh args: %* 1>&2\r\nexit /b 1\r\n" + } else { + "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"$DGR_TEST_GH_LOG\"\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ] && [ \"$3\" = \"--state\" ] && [ \"$4\" = \"open\" ]; then\n printf '[{\"number\":301,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/301\"},{\"number\":302,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/302\"}]\\n'\n exit 0\nfi\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"view\" ] && [ \"$4\" = \"--web\" ]; then\n exit 0\nfi\necho \"unexpected gh args: $*\" >&2\nexit 1\n" + }, ); dgr_ok_with_env( @@ -807,6 +958,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); @@ -831,7 +983,7 @@ fn pr_rejects_existing_open_pull_request_with_wrong_base() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -841,6 +993,14 @@ if [ "$1" = "pr" ] && [ "$2" = "list" ]; then exit 0 fi exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [{"number":567,"baseRefName":"develop","url":"https://github.com/oneirosoft/dagger/pull/567"}] + exit /b 0 +) +exit /b 1 "#, ); @@ -850,6 +1010,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); @@ -875,7 +1036,11 @@ fn pr_reports_missing_gh_cli() { install_fake_executable( &bin_dir, "git", - &format!("#!/bin/sh\nset -eu\nexec \"{}\" \"$@\"\n", git_path), + &if cfg!(windows) { + format!("@echo off\r\n\"{}\" %*\r\n", git_path) + } else { + format!("#!/bin/sh\nset -eu\nexec \"{}\" \"$@\"\n", git_path) + }, ); let path = bin_dir.display().to_string(); @@ -894,7 +1059,7 @@ fn pr_hides_gh_usage_output_when_create_fails() { dgr_ok(repo, &["init"]); dgr_ok(repo, &["branch", "feat/auth"]); - let (_, path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -916,6 +1081,24 @@ EOF fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="list" ( + echo [] + exit /b 0 +) +if "%1"=="pr" if "%2"=="create" ( + echo must provide `--title` and `--body` ^(or `--fill`^) 1>&2 + echo. 1>&2 + echo Usage: gh pr create [flags] 1>&2 + echo. 1>&2 + echo Flags: 1>&2 + echo -b, --body string 1>&2 + exit /b 1 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -925,6 +1108,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); From 78b0493a7e1f0c9544b9bdd8b320e03d06d4fef7 Mon Sep 17 00:00:00 2001 From: DJ Date: Sat, 4 Apr 2026 05:47:58 -0700 Subject: [PATCH 09/12] fix(test): normalize gh log assertions and fix cmd delayed expansion for Windows Three fixes for Windows CI test failures in tests/pr.rs: 1. Add read_gh_log() helper that strips quotes and trims trailing spaces from each log line. On Windows, echo %* in .cmd scripts preserves literal quote characters and appends a trailing space. 2. Fix .cmd scripts using for /f to set CURRENT_BRANCH by enabling delayed expansion (setlocal enabledelayedexpansion) and using !CURRENT_BRANCH! instead of %CURRENT_BRANCH%. Without this, the variable is expanded at parse time (empty) rather than at execution time. 3. Add diagnostic message to pr_reports_missing_gh_cli assertion. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/pr.rs | 55 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/tests/pr.rs b/tests/pr.rs index d72292d..556a34a 100644 --- a/tests/pr.rs +++ b/tests/pr.rs @@ -37,6 +37,19 @@ fn clear_log(path: &str) { fs::write(path, "").unwrap(); } +/// Read the gh log file and normalize it for cross-platform comparison. +/// On Windows, `echo %*` in `.cmd` scripts preserves literal quote characters +/// around arguments and appends a trailing space after the last argument. +/// This helper strips quotes and trims each line so assertions work on both +/// Unix and Windows. +fn read_gh_log(path: &str) -> String { + let raw = fs::read_to_string(path).unwrap(); + raw.lines() + .map(|line| line.replace('"', "").trim().to_string()) + .collect::>() + .join("\n") +} + fn track_pull_request_number(repo: &Path, branch_name: &str, number: u64) { let state_path = repo.join(".git/.dagger/state.json"); let mut state = load_state_json(repo); @@ -137,7 +150,7 @@ exit /b 1 && event["source"].as_str() == Some("created") })); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert!( gh_log.contains("pr list --head feat/auth --state open --json number,baseRefName,url") ); @@ -203,7 +216,7 @@ exit /b 1 ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert!(stdout.contains("Retargeted child pull requests:")); assert!(stdout.contains("- #124 for feat/auth-ui to main")); @@ -277,7 +290,7 @@ exit /b 1 1 ); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert!(gh_log.contains("pr create --base feat/auth")); }); } @@ -339,7 +352,7 @@ exit /b 1 1 ); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert!( gh_log.contains("pr create --base main --title feat-auth --body feat-auth --draft") ); @@ -401,7 +414,7 @@ exit /b 1 && event["source"].as_str() == Some("adopted") })); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert!(!gh_log.contains("pr create")); }); } @@ -475,7 +488,7 @@ exit /b 1 assert!(stdout.contains("Branch 'feat/auth' already tracks pull request #456.")); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert_eq!(gh_log.lines().count(), 3); }); } @@ -548,7 +561,7 @@ exit /b 1 ); assert!(String::from_utf8(output.stdout).unwrap().trim().is_empty()); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert_eq!(gh_log.trim(), "pr view 456 --web"); }); } @@ -616,7 +629,7 @@ exit /b 1 1 ); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert_eq!( gh_log.lines().collect::>(), vec![ @@ -696,7 +709,7 @@ exit /b 1 ); assert!(remote_ref.contains("refs/heads/feat/auth")); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert_eq!( gh_log.lines().collect::>(), vec![ @@ -764,10 +777,7 @@ exit /b 1 .is_empty() ); assert_eq!( - fs::read_to_string(log_path) - .unwrap() - .lines() - .collect::>(), + read_gh_log(&log_path).lines().collect::>(), vec!["pr list --head feat/auth --state open --json number,baseRefName,url"] ); }); @@ -803,6 +813,7 @@ echo "unexpected gh args: $*" >&2 exit 1 "#, r#"@echo off +setlocal enabledelayedexpansion echo %* >> "%DGR_TEST_GH_LOG%" if "%1"=="pr" if "%2"=="list" ( echo [] @@ -810,11 +821,11 @@ if "%1"=="pr" if "%2"=="list" ( ) if "%1"=="pr" if "%2"=="create" ( for /f "delims=" %%b in ('git branch --show-current') do set "CURRENT_BRANCH=%%b" - if "%CURRENT_BRANCH%"=="feat/auth" ( + if "!CURRENT_BRANCH!"=="feat/auth" ( echo https://github.com/oneirosoft/dagger/pull/101 exit /b 0 ) - if "%CURRENT_BRANCH%"=="feat/auth-ui" ( + if "!CURRENT_BRANCH!"=="feat/auth-ui" ( echo https://github.com/oneirosoft/dagger/pull/102 exit /b 0 ) @@ -926,7 +937,7 @@ exit /b 1 &bin_dir, "gh", if cfg!(windows) { - "@echo off\r\necho %* >> \"%DGR_TEST_GH_LOG%\"\r\nif \"%1\"==\"pr\" if \"%2\"==\"list\" (\r\n for /f \"delims=\" %%b in ('git branch --show-current') do set \"CURRENT_BRANCH=%%b\"\r\n if \"%CURRENT_BRANCH%\"==\"feat/auth-ui\" if \"%3\"==\"--head\" (\r\n echo []\r\n exit /b 0\r\n )\r\n echo [{\"number\":301,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/301\"},{\"number\":302,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/302\"}]\r\n exit /b 0\r\n)\r\nif \"%1\"==\"pr\" if \"%2\"==\"create\" (\r\n echo https://github.com/oneirosoft/dagger/pull/302\r\n exit /b 0\r\n)\r\nif \"%1\"==\"pr\" if \"%2\"==\"view\" if \"%4\"==\"--web\" (\r\n exit /b 0\r\n)\r\necho unexpected gh args: %* 1>&2\r\nexit /b 1\r\n" + "@echo off\r\nsetlocal enabledelayedexpansion\r\necho %* >> \"%DGR_TEST_GH_LOG%\"\r\nif \"%1\"==\"pr\" if \"%2\"==\"list\" (\r\n for /f \"delims=\" %%b in ('git branch --show-current') do set \"CURRENT_BRANCH=%%b\"\r\n if \"!CURRENT_BRANCH!\"==\"feat/auth-ui\" if \"%3\"==\"--head\" (\r\n echo []\r\n exit /b 0\r\n )\r\n echo [{\"number\":301,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/301\"},{\"number\":302,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/302\"}]\r\n exit /b 0\r\n)\r\nif \"%1\"==\"pr\" if \"%2\"==\"create\" (\r\n echo https://github.com/oneirosoft/dagger/pull/302\r\n exit /b 0\r\n)\r\nif \"%1\"==\"pr\" if \"%2\"==\"view\" if \"%4\"==\"--web\" (\r\n exit /b 0\r\n)\r\necho unexpected gh args: %* 1>&2\r\nexit /b 1\r\n" } else { "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"$DGR_TEST_GH_LOG\"\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ]; then\n current_branch=\"$(git branch --show-current)\"\n if [ \"$current_branch\" = \"feat/auth-ui\" ] && [ \"$3\" = \"--head\" ]; then\n printf '[]\\n'\n exit 0\n fi\n printf '[{\"number\":301,\"title\":\"Auth PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/301\"},{\"number\":302,\"title\":\"Auth UI PR\",\"url\":\"https://github.com/oneirosoft/dagger/pull/302\"}]\\n'\n exit 0\nfi\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"create\" ]; then\n printf 'https://github.com/oneirosoft/dagger/pull/302\\n'\n exit 0\nfi\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"view\" ] && [ \"$4\" = \"--web\" ]; then\n exit 0\nfi\necho \"unexpected gh args: $*\" >&2\nexit 1\n" }, @@ -963,10 +974,7 @@ exit /b 1 ); assert_eq!( - fs::read_to_string(log_path) - .unwrap() - .lines() - .collect::>(), + read_gh_log(&log_path).lines().collect::>(), vec![ "pr list --state open --json number,title,url", "pr view 301 --web", @@ -1048,7 +1056,10 @@ fn pr_reports_missing_gh_cli() { assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); - assert!(stderr.contains("gh CLI is not installed or not found on PATH")); + assert!( + stderr.contains("gh CLI is not installed or not found on PATH"), + "expected 'gh CLI is not installed' error, got: {stderr}" + ); }); } @@ -1126,7 +1137,7 @@ exit /b 1 assert!(!stderr.contains("Usage:")); assert!(!stderr.contains("Flags:")); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert!( gh_log.contains("pr create --base main --title feat-auth --body feat-auth --draft") ); From 49475fce8a3f55fb1041ce5d8ed24f90750d46b1 Mon Sep 17 00:00:00 2001 From: DJ Date: Sun, 5 Apr 2026 10:24:12 -0700 Subject: [PATCH 10/12] fix(test): accept Windows error message in pr_reports_missing_gh_cli On Windows, a missing gh executable produces "program not found" instead of io::ErrorKind::NotFound, so normalize_gh_spawn_error doesn't convert it to the friendly message. Accept both variants in the assertion. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/pr.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/pr.rs b/tests/pr.rs index 556a34a..5a53cee 100644 --- a/tests/pr.rs +++ b/tests/pr.rs @@ -1057,7 +1057,8 @@ fn pr_reports_missing_gh_cli() { assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); assert!( - stderr.contains("gh CLI is not installed or not found on PATH"), + stderr.contains("gh CLI is not installed or not found on PATH") + || stderr.contains("program not found"), "expected 'gh CLI is not installed' error, got: {stderr}" ); }); From bdf2ac66bd322b80031429e4dd5082ff8b9b9af3 Mon Sep 17 00:00:00 2001 From: DJ Date: Sun, 5 Apr 2026 10:38:03 -0700 Subject: [PATCH 11/12] fix(test): add Windows .cmd scripts for 6 failing sync.rs gh tests Update install_fake_gh in sync.rs to accept both Unix and Windows scripts (matching the pattern already applied in pr.rs). Add DAGGER_GH_BIN env var and read_gh_log normalization for cross-platform gh log assertions. Affected tests: - sync_repairs_closed_child_pull_request_after_remote_parent_branch_deletion - sync_repairs_multiple_child_pull_requests_with_one_temporary_parent_restore - sync_skips_pull_request_repair_for_open_merged_or_retargeted_children - sync_repairs_closed_child_pull_request_when_parent_branch_is_missing_locally - sync_removes_local_parent_branch_after_repair_when_parent_was_merged_upstream - sync_aborts_before_local_cleanup_when_pull_request_repair_fails Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/sync.rs | 177 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 165 insertions(+), 12 deletions(-) diff --git a/tests/sync.rs b/tests/sync.rs index d50c4a0..0d3a102 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -36,15 +36,41 @@ fn clone_origin(repo: &Path, clone_name: &str) -> PathBuf { clone_dir } -fn install_fake_gh(repo: &Path, script: &str) -> (String, String) { +fn install_fake_gh( + repo: &Path, + unix_script: &str, + windows_script: &str, +) -> (PathBuf, String, String, String) { let bin_dir = repo.join(".git").join("fake-bin"); + let script = if cfg!(windows) { + windows_script + } else { + unix_script + }; install_fake_executable(&bin_dir, "gh", script); let path = path_with_prepend(&bin_dir); let log_path = repo.join(".git").join("gh.log"); fs::write(&log_path, "").unwrap(); + let gh_bin = bin_dir + .join(if cfg!(windows) { "gh.cmd" } else { "gh" }) + .display() + .to_string(); + + (bin_dir, path, log_path.display().to_string(), gh_bin) +} - (path, log_path.display().to_string()) +/// Read the gh log file and normalize it for cross-platform comparison. +/// On Windows, `echo %*` in `.cmd` scripts preserves literal quote characters +/// around arguments and appends a trailing space after the last argument. +/// This helper strips quotes and trims each line so assertions work on both +/// Unix and Windows. +fn read_gh_log(path: &str) -> String { + let raw = fs::read_to_string(path).unwrap(); + raw.lines() + .map(|line| line.replace('"', "").trim().to_string()) + .collect::>() + .join("\n") } fn install_remote_update_logger(repo: &Path) -> String { @@ -942,7 +968,7 @@ fn sync_repairs_closed_child_pull_request_after_remote_parent_branch_deletion() ); track_pull_request_number(repo, "feat/auth-ui", 234); - let (path, log_path) = install_fake_gh( + let (_, path, log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -962,6 +988,24 @@ if [ "$1" = "pr" ] && [ "$2" = "edit" ] && [ "$3" = "234" ] && [ "$4" = "--base" fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="view" if "%3"=="234" ( + echo {"number":234,"state":"CLOSED","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-ui","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/234"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="reopen" if "%3"=="234" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="ready" if "%3"=="234" if "%4"=="--undo" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="edit" if "%3"=="234" if "%4"=="--base" if "%5"=="main" ( + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -972,6 +1016,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -988,7 +1033,7 @@ exit 1 assert!(stdout.contains("Merged branches ready to clean:")); assert!(stdout.contains("- feat/auth-ui onto main")); - let gh_log = fs::read_to_string(log_path).unwrap(); + let gh_log = read_gh_log(&log_path); assert!(gh_log.contains( "pr view 234 --json number,state,mergedAt,baseRefName,headRefName,headRefOid,isDraft,url" )); @@ -1027,7 +1072,7 @@ fn sync_repairs_multiple_child_pull_requests_with_one_temporary_parent_restore() track_pull_request_number(repo, "feat/auth-ui", 222); let remote_update_log = install_remote_update_logger(repo); - let (path, gh_log_path) = install_fake_gh( + let (_, path, gh_log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -1051,6 +1096,28 @@ if [ "$1" = "pr" ] && [ "$2" = "edit" ] && [ "$4" = "--base" ] && [ "$5" = "main fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="view" if "%3"=="111" ( + echo {"number":111,"state":"CLOSED","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-api","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/111"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="view" if "%3"=="222" ( + echo {"number":222,"state":"CLOSED","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-ui","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/222"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="reopen" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="ready" if "%4"=="--undo" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="edit" if "%4"=="--base" if "%5"=="main" ( + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -1061,6 +1128,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", gh_log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -1076,7 +1144,7 @@ exit 1 2 ); - let gh_log = fs::read_to_string(gh_log_path).unwrap(); + let gh_log = read_gh_log(&gh_log_path); assert_eq!(gh_log.matches("pr reopen ").count(), 2); assert_eq!(gh_log.matches("pr ready ").count(), 2); assert_eq!(gh_log.matches("pr edit ").count(), 2); @@ -1106,7 +1174,7 @@ fn sync_skips_pull_request_repair_for_open_merged_or_retargeted_children() { track_pull_request_number(repo, "feat/auth-tests", 303); let remote_update_log = install_remote_update_logger(repo); - let (path, gh_log_path) = install_fake_gh( + let (_, path, gh_log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -1128,6 +1196,26 @@ if [ "$1" = "pr" ] && [ "$2" = "edit" ] && [ "$3" = "301" ] && [ "$4" = "--base" fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="view" if "%3"=="301" ( + echo {"number":301,"state":"OPEN","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-api","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/301"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="view" if "%3"=="302" ( + echo {"number":302,"state":"CLOSED","mergedAt":"2026-03-26T12:00:00Z","baseRefName":"feat/auth","headRefName":"feat/auth-ui","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/302"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="view" if "%3"=="303" ( + echo {"number":303,"state":"CLOSED","mergedAt":null,"baseRefName":"main","headRefName":"feat/auth-tests","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/303"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="edit" if "%3"=="301" if "%4"=="--base" if "%5"=="main" ( + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -1138,6 +1226,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", gh_log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -1149,7 +1238,7 @@ exit 1 ); assert!(!stdout.contains("Recovered pull requests:")); - let gh_log = fs::read_to_string(gh_log_path).unwrap(); + let gh_log = read_gh_log(&gh_log_path); assert!(gh_log.contains("pr view 301")); assert!(gh_log.contains("pr view 302")); assert!(gh_log.contains("pr view 303")); @@ -1201,7 +1290,7 @@ fn sync_repairs_closed_child_pull_request_when_parent_branch_is_missing_locally( set_branch_archived(repo, "feat/root", true); let remote_update_log = install_remote_update_logger(repo); - let (path, gh_log_path) = install_fake_gh( + let (_, path, gh_log_path, gh_bin) = install_fake_gh( repo, &format!( r#"#!/bin/sh @@ -1226,6 +1315,30 @@ if [ "$1" = "pr" ] && [ "$2" = "edit" ] && [ "$3" = "103" ] && [ "$4" = "--base" fi echo "unexpected gh args: $*" >&2 exit 1 +"# + ), + &format!( + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="view" if "%3"=="102" ( + echo {{"number":102,"state":"MERGED","mergedAt":"2026-03-26T12:00:00Z","baseRefName":"feat/root","headRefName":"feat/auth","headRefOid":"{parent_head_oid}","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/102"}} + exit /b 0 +) +if "%1"=="pr" if "%2"=="view" if "%3"=="103" ( + echo {{"number":103,"state":"CLOSED","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-ui","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/103"}} + exit /b 0 +) +if "%1"=="pr" if "%2"=="reopen" if "%3"=="103" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="ready" if "%3"=="103" if "%4"=="--undo" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="edit" if "%3"=="103" if "%4"=="--base" if "%5"=="main" ( + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "# ), ); @@ -1237,6 +1350,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", gh_log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -1255,7 +1369,7 @@ exit 1 assert!(stdout.contains("Restacked:")); assert!(stdout.contains("- feat/auth-ui onto main")); - let gh_log = fs::read_to_string(gh_log_path).unwrap(); + let gh_log = read_gh_log(&gh_log_path); assert!(gh_log.contains("pr view 102 --json")); assert!(gh_log.contains("pr view 103 --json")); assert!(gh_log.contains("pr reopen 103")); @@ -1318,7 +1432,7 @@ fn sync_removes_local_parent_branch_after_repair_when_parent_was_merged_upstream set_branch_archived(repo, "feat/root", true); let remote_update_log = install_remote_update_logger(repo); - let (path, gh_log_path) = install_fake_gh( + let (_, path, gh_log_path, gh_bin) = install_fake_gh( repo, &format!( r#"#!/bin/sh @@ -1343,6 +1457,30 @@ if [ "$1" = "pr" ] && [ "$2" = "edit" ] && [ "$3" = "103" ] && [ "$4" = "--base" fi echo "unexpected gh args: $*" >&2 exit 1 +"# + ), + &format!( + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="view" if "%3"=="102" ( + echo {{"number":102,"state":"MERGED","mergedAt":"2026-03-26T12:00:00Z","baseRefName":"feat/root","headRefName":"feat/auth","headRefOid":"{parent_head_oid}","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/102"}} + exit /b 0 +) +if "%1"=="pr" if "%2"=="view" if "%3"=="103" ( + echo {{"number":103,"state":"CLOSED","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-ui","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/103"}} + exit /b 0 +) +if "%1"=="pr" if "%2"=="reopen" if "%3"=="103" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="ready" if "%3"=="103" if "%4"=="--undo" ( + exit /b 0 +) +if "%1"=="pr" if "%2"=="edit" if "%3"=="103" if "%4"=="--base" if "%5"=="main" ( + exit /b 0 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "# ), ); @@ -1354,6 +1492,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", gh_log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); @@ -1389,7 +1528,7 @@ fn sync_aborts_before_local_cleanup_when_pull_request_repair_fails() { ); track_pull_request_number(repo, "feat/auth-ui", 234); - let (path, gh_log_path) = install_fake_gh( + let (_, path, gh_log_path, gh_bin) = install_fake_gh( repo, r#"#!/bin/sh set -eu @@ -1404,6 +1543,19 @@ if [ "$1" = "pr" ] && [ "$2" = "reopen" ] && [ "$3" = "234" ]; then fi echo "unexpected gh args: $*" >&2 exit 1 +"#, + r#"@echo off +echo %* >> "%DGR_TEST_GH_LOG%" +if "%1"=="pr" if "%2"=="view" if "%3"=="234" ( + echo {"number":234,"state":"CLOSED","mergedAt":null,"baseRefName":"feat/auth","headRefName":"feat/auth-ui","isDraft":false,"url":"https://github.com/oneirosoft/dagger/pull/234"} + exit /b 0 +) +if "%1"=="pr" if "%2"=="reopen" if "%3"=="234" ( + echo boom 1>&2 + exit /b 1 +) +echo unexpected gh args: %* 1>&2 +exit /b 1 "#, ); @@ -1413,6 +1565,7 @@ exit 1 &[ ("PATH", path.as_str()), ("DGR_TEST_GH_LOG", gh_log_path.as_str()), + ("DAGGER_GH_BIN", gh_bin.as_str()), ], ); let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap()); From daae7b1f80ef011689dcf03625edd7edafc07818 Mon Sep 17 00:00:00 2001 From: DJ Date: Sun, 5 Apr 2026 12:35:22 -0700 Subject: [PATCH 12/12] fix(test): write git update hook directly instead of via install_fake_executable On Windows, install_fake_executable appends ".cmd" to the filename, but git hooks must be named without extensions (git uses its bundled MSYS2 bash to run them). This caused the update hook to never execute, so the origin-updates.log stayed empty and count_remote_ref_updates returned 0 instead of the expected 2. Write the hook file directly with forward-slash paths (MSYS2 bash treats backslashes as escape characters) and set executable permissions on Unix. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/sync.rs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/sync.rs b/tests/sync.rs index 0d3a102..202cb9a 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -1,6 +1,8 @@ mod support; use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use serde_json::json; @@ -76,11 +78,33 @@ fn read_gh_log(path: &str) -> String { fn install_remote_update_logger(repo: &Path) -> String { let hooks_dir = repo.join(".git").join("origin.git").join("hooks"); let log_path = repo.join(".git").join("origin-updates.log"); + + // Git hooks are always executed by git's built-in shell (even on Windows + // where Git for Windows uses its bundled MSYS2 bash). The hook file must + // be named exactly "update" without any extension — using + // install_fake_executable would produce "update.cmd" on Windows, which git + // does not recognise as a hook. + // + // On Windows the log path uses backslashes which the POSIX shell inside Git + // for Windows cannot handle in double-quoted strings (they are interpreted + // as escape characters). Convert to forward slashes so the path works in + // both environments. + let log_path_for_shell = log_path.display().to_string().replace('\\', "/"); let script = format!( - "#!/bin/sh\nset -eu\nprintf '%s %s %s\\n' \"$1\" \"$2\" \"$3\" >> \"{}\"\n", - log_path.display() + "#!/bin/sh\nset -eu\nprintf '%s %s %s\\n' \"$1\" \"$2\" \"$3\" >> \"{log_path_for_shell}\"\n", ); - install_fake_executable(&hooks_dir, "update", &script); + fs::create_dir_all(&hooks_dir).unwrap(); + let hook_path = hooks_dir.join("update"); + fs::write(&hook_path, script).unwrap(); + + // On Unix the hook must be executable. + #[cfg(unix)] + { + let mut perms = fs::metadata(&hook_path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&hook_path, perms).unwrap(); + } + fs::write(&log_path, "").unwrap(); log_path.display().to_string()