From 964cd8e61a290cff33176f40a77e40c9795dd7b3 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:11:27 +0100 Subject: [PATCH 01/21] test: isolate linked-worktree containment with a submodule --- scripts/worktree_guard.submodule.tests.sh | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 scripts/worktree_guard.submodule.tests.sh diff --git a/scripts/worktree_guard.submodule.tests.sh b/scripts/worktree_guard.submodule.tests.sh new file mode 100644 index 0000000000..a257df8f01 --- /dev/null +++ b/scripts/worktree_guard.submodule.tests.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Isolating contract for worktree_guard.sh's linked-worktree containment check. +# +# A Git submodule is the load-bearing fixture: its work tree has a real .git +# pointer file that resolves exactly to its own git dir, so it satisfies the +# guard's pointer check. It is still not a linked worktree because that git dir +# does not live under /worktrees/. Removing only the +# containment block must therefore turn this fixture from rejected to accepted. + +set -euo pipefail + +_tests_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +GUARD="${WT_GUARD_SH:-$_tests_dir/worktree_guard.sh}" +FIXTURE_ROOT="$(mktemp -d)" + +cleanup() { + rm -rf -- "$FIXTURE_ROOT" 2>/dev/null || true +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +pass() { + printf ' PASS: %s\n' "$1" +} + +run_guard() { + local guard="$1" + ( + cd -- "$SUBMODULE_ROOT" + bash -c 'source "$1"' bash "$guard" + ) 2>&1 +} + +if [ ! -f "$GUARD" ]; then + fail "guard script not found: $GUARD" +fi + +# A committed source repository is required because submodule add checks out +# HEAD into the nested work tree. +git init -q -b main "$FIXTURE_ROOT/module-source" +git -C "$FIXTURE_ROOT/module-source" \ + -c user.email=t@example.com \ + -c user.name=t \ + commit -q --allow-empty -m "seed module" --no-gpg-sign + +git init -q -b main "$FIXTURE_ROOT/superproject" +git -C "$FIXTURE_ROOT/superproject" \ + -c user.email=t@example.com \ + -c user.name=t \ + commit -q --allow-empty -m "seed superproject" --no-gpg-sign + +# Git disables local file transport for submodules by default. Scope the +# exception to this fixture command; no global or repository configuration is +# changed. +git -C "$FIXTURE_ROOT/superproject" \ + -c protocol.file.allow=always \ + submodule add -q "$FIXTURE_ROOT/module-source" vendor/module +git -C "$FIXTURE_ROOT/superproject" \ + -c user.email=t@example.com \ + -c user.name=t \ + commit -q -m "add module" --no-gpg-sign + +SUBMODULE_ROOT="$FIXTURE_ROOT/superproject/vendor/module" +if [ ! -f "$SUBMODULE_ROOT/.git" ]; then + fail "fixture is not a submodule work tree with a .git pointer file" +fi + +set +e +output="$(run_guard "$GUARD")" +code=$? +set -e + +if [ "$code" -ne 1 ]; then + printf '%s\n' "$output" >&2 + fail "submodule must be rejected with exit 1 (got $code)" +fi +if ! printf '%s' "$output" | grep -qF -- "main checkout or an unrecognized worktree"; then + printf '%s\n' "$output" >&2 + fail "submodule rejection did not come from linked-worktree containment" +fi +pass "submodule is rejected by linked-worktree containment" + +# Mutation control: remove exactly substance check 1 while leaving the pointer +# and HEAD checks byte-for-byte. The same submodule must then reach success; +# otherwise the fixture does not isolate the containment invariant. +MUTATED_GUARD="$FIXTURE_ROOT/worktree_guard-without-containment.sh" +awk ' + /^# Substance check 1:/ { skipping = 1; next } + /^# Substance check 2:/ { skipping = 0 } + !skipping { print } +' "$GUARD" > "$MUTATED_GUARD" + +if grep -qF -- "# Substance check 1:" "$MUTATED_GUARD"; then + fail "mutation control did not remove substance check 1" +fi +if ! grep -qF -- "# Substance check 2:" "$MUTATED_GUARD"; then + fail "mutation control removed the pointer check as well" +fi + +set +e +mutated_output="$(run_guard "$MUTATED_GUARD")" +mutated_code=$? +set -e + +if [ "$mutated_code" -ne 0 ]; then + printf '%s\n' "$mutated_output" >&2 + fail "submodule must pass when only linked-worktree containment is removed (got $mutated_code)" +fi +if ! printf '%s' "$mutated_output" | grep -qF -- "OK [worktree_guard]: Running in an isolated worktree."; then + printf '%s\n' "$mutated_output" >&2 + fail "mutated guard did not reach its success path" +fi +pass "removing only containment makes the isolating submodule pass" + +printf 'worktree_guard submodule contract passed: 2 checks.\n' From af64c92cf779c4f37a3d9054a211d977b4797388 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:11:42 +0100 Subject: [PATCH 02/21] ci: gate shell worktree guard contracts --- .github/workflows/reusable-docs-governance.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/reusable-docs-governance.yml b/.github/workflows/reusable-docs-governance.yml index fc3e8fe4bd..2b6395676e 100644 --- a/.github/workflows/reusable-docs-governance.yml +++ b/.github/workflows/reusable-docs-governance.yml @@ -39,6 +39,12 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Validate worktree guard contracts + shell: bash + run: | + bash scripts/worktree_guard.tests.sh + bash scripts/worktree_guard.submodule.tests.sh + - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: From 2e3bccecc1dc31e0cfbf422fbb1be9cfbdf185c3 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:16:45 +0100 Subject: [PATCH 03/21] test: reproduce contradictory worktree expectations --- scripts/worktree_guard.expectation.tests.sh | 94 +++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 scripts/worktree_guard.expectation.tests.sh diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh new file mode 100644 index 0000000000..534eaceb7f --- /dev/null +++ b/scripts/worktree_guard.expectation.tests.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Cross-shell regression contract for contradictory worktree HEAD expectations. +# Supplying a branch name while explicitly requiring a detached HEAD is a caller +# setup error, not a condition either guard may silently resolve or ignore. + +set -euo pipefail + +_tests_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +SH_GUARD="${WT_GUARD_SH:-$_tests_dir/worktree_guard.sh}" +PS_GUARD="${WT_GUARD_PS:-$_tests_dir/worktree_guard.ps1}" +PS_GUARD_NATIVE="$PS_GUARD" +if command -v cygpath >/dev/null 2>&1; then + PS_GUARD_NATIVE="$(cygpath -w "$PS_GUARD" 2>/dev/null || printf '%s' "$PS_GUARD")" +fi + +PS_EXE="" +if command -v powershell >/dev/null 2>&1; then + PS_EXE="powershell" +elif command -v pwsh >/dev/null 2>&1; then + PS_EXE="pwsh" +fi + +FIXTURE_ROOT="$(mktemp -d)" +cleanup() { + if [ -d "$FIXTURE_ROOT/primary" ]; then + git -C "$FIXTURE_ROOT/primary" worktree remove "$FIXTURE_ROOT/detached" >/dev/null 2>&1 || true + fi + rm -rf -- "$FIXTURE_ROOT" 2>/dev/null || true +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +pass() { + printf ' PASS: %s\n' "$1" +} + +assert_setup_error() { + local name="$1" + local code="$2" + local output="$3" + + if [ "$code" -ne 2 ]; then + printf '%s\n' "$output" >&2 + fail "$name must exit 2 for contradictory expectations (got $code)" + fi + if ! printf '%s' "$output" | grep -qF -- "cannot be combined"; then + printf '%s\n' "$output" >&2 + fail "$name did not explain the contradictory expectations" + fi + pass "$name rejects contradictory expectations as a setup error" +} + +if [ ! -f "$SH_GUARD" ] || [ ! -f "$PS_GUARD" ]; then + fail "guard scripts were not found beside the contract" +fi + +git init -q -b main "$FIXTURE_ROOT/primary" +git -C "$FIXTURE_ROOT/primary" \ + -c user.email=t@example.com \ + -c user.name=t \ + commit -q --allow-empty -m "seed" --no-gpg-sign +git -C "$FIXTURE_ROOT/primary" worktree add -q --detach "$FIXTURE_ROOT/detached" HEAD + +set +e +sh_output="$( + cd -- "$FIXTURE_ROOT/detached" + WT_EXPECT_HEAD=detached \ + WT_EXPECT_BRANCH=guard-test-branch \ + bash -c 'source "$1"' bash "$SH_GUARD" + 2>&1)" +sh_code=$? +set -e +assert_setup_error "shell guard" "$sh_code" "$sh_output" + +if [ -z "$PS_EXE" ]; then + printf ' SKIP: PowerShell guard contract (no powershell/pwsh on PATH)\n' +else + set +e + ps_output="$( + cd -- "$FIXTURE_ROOT/detached" + "$PS_EXE" -NoLogo -NoProfile -NonInteractive -File "$PS_GUARD_NATIVE" \ + -ExpectHead Detached \ + -ExpectedBranch guard-test-branch + 2>&1)" + ps_code=$? + set -e + assert_setup_error "PowerShell guard" "$ps_code" "$ps_output" +fi + +printf 'worktree_guard expectation contract passed.\n' From 593ea93139799ebe800fd1830dba5366a0a5afb7 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:17:13 +0100 Subject: [PATCH 04/21] test: capture guard diagnostics in expectation contract --- scripts/worktree_guard.expectation.tests.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh index 534eaceb7f..93477ff857 100644 --- a/scripts/worktree_guard.expectation.tests.sh +++ b/scripts/worktree_guard.expectation.tests.sh @@ -70,8 +70,8 @@ sh_output="$( cd -- "$FIXTURE_ROOT/detached" WT_EXPECT_HEAD=detached \ WT_EXPECT_BRANCH=guard-test-branch \ - bash -c 'source "$1"' bash "$SH_GUARD" - 2>&1)" + bash -c 'source "$1"' bash "$SH_GUARD" 2>&1 +)" sh_code=$? set -e assert_setup_error "shell guard" "$sh_code" "$sh_output" @@ -84,8 +84,8 @@ else cd -- "$FIXTURE_ROOT/detached" "$PS_EXE" -NoLogo -NoProfile -NonInteractive -File "$PS_GUARD_NATIVE" \ -ExpectHead Detached \ - -ExpectedBranch guard-test-branch - 2>&1)" + -ExpectedBranch guard-test-branch 2>&1 + )" ps_code=$? set -e assert_setup_error "PowerShell guard" "$ps_code" "$ps_output" From 43f6af24a32e6be63d5cc7b84d902a342399f2a8 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:17:29 +0100 Subject: [PATCH 05/21] ci: run contradictory worktree expectation contract --- .github/workflows/reusable-docs-governance.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/reusable-docs-governance.yml b/.github/workflows/reusable-docs-governance.yml index 2b6395676e..2d7db5411e 100644 --- a/.github/workflows/reusable-docs-governance.yml +++ b/.github/workflows/reusable-docs-governance.yml @@ -44,6 +44,7 @@ jobs: run: | bash scripts/worktree_guard.tests.sh bash scripts/worktree_guard.submodule.tests.sh + bash scripts/worktree_guard.expectation.tests.sh - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 From 4bf31f10dd2233b40e628266c6c0d7574f84ebeb Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:18:50 +0100 Subject: [PATCH 06/21] fix: reject contradictory shell guard expectations --- scripts/worktree_guard.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/worktree_guard.sh b/scripts/worktree_guard.sh index 7245419648..3e908893c6 100644 --- a/scripts/worktree_guard.sh +++ b/scripts/worktree_guard.sh @@ -20,7 +20,7 @@ # Exit/return codes (unchanged contract): # 0 - inside a valid linked worktree # 1 - FATAL: main checkout / not a linked worktree / HEAD expectation unmet -# 2 - ERROR: not inside a git repository, or the layout could not be read +# 2 - ERROR: setup/configuration failure, or repository layout unreadable # # Exports on success: # WT_REPO_ROOT - absolute path to the worktree's git toplevel @@ -177,6 +177,11 @@ fi _wt_expect_head="${WT_EXPECT_HEAD:-any}" _wt_expect_branch="${WT_EXPECT_BRANCH:-}" +if [ -n "$_wt_expect_branch" ] && [ "$_wt_expect_head" = "detached" ]; then + echo "ERROR [worktree_guard]: WT_EXPECT_HEAD=detached cannot be combined with WT_EXPECT_BRANCH." >&2 + _wt_cleanup + return 2 2>/dev/null || exit 2 +fi if [ -n "$_wt_expect_branch" ] && [ "$_wt_expect_head" = "any" ]; then _wt_expect_head="branch" fi From 53afb013ce1aff5fe37e398ad3b86b6435a45997 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:20:09 +0100 Subject: [PATCH 07/21] fix: reject contradictory PowerShell guard expectations --- scripts/worktree_guard.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/worktree_guard.ps1 b/scripts/worktree_guard.ps1 index 1deaf18899..40140d5492 100644 --- a/scripts/worktree_guard.ps1 +++ b/scripts/worktree_guard.ps1 @@ -13,7 +13,7 @@ # Exit codes (unchanged contract): # 0 - inside a valid linked worktree # 1 - FATAL: main checkout / not a linked worktree / HEAD expectation unmet -# 2 - ERROR: setup failure, not inside a git repository, layout unreadable +# 2 - ERROR: setup/configuration failure, repository/layout unreadable # # -AllowedMarkers is retained for invocation compatibility and is ADVISORY # only: a root outside those markers is reported but not rejected. @@ -239,6 +239,10 @@ if ($symbolicHead.Succeeded -and $symbolicHead.ExitCode -eq 0 -and $symbolicHead $headState = if ([string]::IsNullOrEmpty($headBranch)) { "detached" } else { "branch" } $effectiveExpectHead = $ExpectHead +if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Detached") { + Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue + exit 2 +} if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Any") { $effectiveExpectHead = "Branch" } From f458b2540ec420e98567f5a866a0372a7e3bd2ce Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:21:49 +0100 Subject: [PATCH 08/21] test: reproduce mixed worktree export path flavors --- scripts/worktree_guard.path-flavor.tests.sh | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 scripts/worktree_guard.path-flavor.tests.sh diff --git a/scripts/worktree_guard.path-flavor.tests.sh b/scripts/worktree_guard.path-flavor.tests.sh new file mode 100644 index 0000000000..454ca213f8 --- /dev/null +++ b/scripts/worktree_guard.path-flavor.tests.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Cross-platform contract for the shell guard's exported path variables. +# Internal validation may use an MSYS/POSIX physical path, but the public +# WT_REPO_ROOT and WT_GIT_DIR values must share one flavour so a caller can hand +# either value to the same native tool without a platform-specific conversion. + +set -euo pipefail + +_tests_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +GUARD="${WT_GUARD_SH:-$_tests_dir/worktree_guard.sh}" +FIXTURE_ROOT="$(mktemp -d)" + +cleanup() { + if [ -d "$FIXTURE_ROOT/primary" ]; then + git -C "$FIXTURE_ROOT/primary" worktree remove "$FIXTURE_ROOT/linked" >/dev/null 2>&1 || true + fi + rm -rf -- "$FIXTURE_ROOT" 2>/dev/null || true +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +path_flavor() { + case "$1" in + [A-Za-z]:[\\/]*) printf 'windows\n' ;; + /*) printf 'posix\n' ;; + *) printf 'unknown\n' ;; + esac +} + +if [ ! -f "$GUARD" ]; then + fail "guard script not found: $GUARD" +fi + +git init -q -b main "$FIXTURE_ROOT/primary" +git -C "$FIXTURE_ROOT/primary" \ + -c user.email=t@example.com \ + -c user.name=t \ + commit -q --allow-empty -m "seed" --no-gpg-sign +git -C "$FIXTURE_ROOT/primary" worktree add -q --detach "$FIXTURE_ROOT/linked" HEAD + +mapfile -t exported < <( + cd -- "$FIXTURE_ROOT/linked" + bash -c ' + source "$1" >/dev/null + printf "%s\n%s\n" "$WT_REPO_ROOT" "$WT_GIT_DIR" + ' bash "$GUARD" +) + +if [ "${#exported[@]}" -ne 2 ]; then + fail "guard did not emit exactly the two exported paths" +fi + +repo_root="${exported[0]}" +git_dir="${exported[1]}" +repo_flavor="$(path_flavor "$repo_root")" +git_flavor="$(path_flavor "$git_dir")" + +if [ "$repo_flavor" = "unknown" ] || [ "$git_flavor" = "unknown" ]; then + printf 'WT_REPO_ROOT=%s\nWT_GIT_DIR=%s\n' "$repo_root" "$git_dir" >&2 + fail "guard exported an unrecognized path flavour" +fi +if [ "$repo_flavor" != "$git_flavor" ]; then + printf 'WT_REPO_ROOT=%s (%s)\nWT_GIT_DIR=%s (%s)\n' \ + "$repo_root" "$repo_flavor" "$git_dir" "$git_flavor" >&2 + fail "WT_REPO_ROOT and WT_GIT_DIR use different path flavours" +fi + +printf 'worktree_guard path-flavour contract passed: %s.\n' "$repo_flavor" From 4d282043f02beb7b42243303c606666005759917 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:22:08 +0100 Subject: [PATCH 09/21] ci: verify shell guard path flavor on Windows --- .github/workflows/reusable-docs-governance.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/reusable-docs-governance.yml b/.github/workflows/reusable-docs-governance.yml index 2d7db5411e..a0ac2262ad 100644 --- a/.github/workflows/reusable-docs-governance.yml +++ b/.github/workflows/reusable-docs-governance.yml @@ -20,6 +20,10 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Validate shell guard export path contract + shell: bash + run: bash scripts/worktree_guard.path-flavor.tests.sh + - name: Validate detached-first worktree helper shell: powershell run: powershell -NoLogo -NoProfile -NonInteractive -File scripts/git/Test-New-CodexIssueWorktree.ps1 @@ -45,6 +49,7 @@ jobs: bash scripts/worktree_guard.tests.sh bash scripts/worktree_guard.submodule.tests.sh bash scripts/worktree_guard.expectation.tests.sh + bash scripts/worktree_guard.path-flavor.tests.sh - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 From 43c446b62530210f7c400ce178b27363aeda4a93 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:24:22 +0100 Subject: [PATCH 10/21] fix: keep shell guard exports in one path flavor --- scripts/worktree_guard.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/scripts/worktree_guard.sh b/scripts/worktree_guard.sh index 3e908893c6..d067e50724 100644 --- a/scripts/worktree_guard.sh +++ b/scripts/worktree_guard.sh @@ -25,7 +25,7 @@ # Exports on success: # WT_REPO_ROOT - absolute path to the worktree's git toplevel # WT_PROJECT_DIR - same as WT_REPO_ROOT for Taskdeck's single-repo layout -# WT_GIT_DIR - this worktree's git dir (/.git/worktrees/) +# WT_GIT_DIR - linked git dir, in the same path flavour as WT_REPO_ROOT # WT_HEAD_STATE - "detached" or "branch" # WT_HEAD_BRANCH - branch name when WT_HEAD_STATE=branch, otherwise empty @@ -37,9 +37,10 @@ _wt_realdir() { } _wt_cleanup() { - unset -v _wt_toplevel _wt_gitdir _wt_common _wt_worktrees_dir _wt_pointer \ - _wt_pointer_line _wt_pointer_dir _wt_head_branch _wt_head_state \ - _wt_expect_head _wt_expect_branch _wt_conventional 2>/dev/null || true + unset -v _wt_toplevel _wt_gitdir _wt_gitdir_export _wt_common \ + _wt_worktrees_dir _wt_pointer _wt_pointer_line _wt_pointer_dir \ + _wt_head_branch _wt_head_state _wt_expect_head _wt_expect_branch \ + _wt_conventional 2>/dev/null || true unset -f _wt_realdir _wt_fatal 2>/dev/null || true unset -f _wt_cleanup 2>/dev/null || true } @@ -87,8 +88,15 @@ if [ -z "$_wt_gitdir" ] || [ -z "$_wt_common" ]; then return 2 2>/dev/null || exit 2 fi +# Preserve Git's own absolute spelling for the public export. Git for Windows +# reports both this path and --show-toplevel as C:/..., whereas pwd -P below +# deliberately normalizes the internal comparison path to /c/.... Mixing those +# representations in the public contract made callers perform ad-hoc conversion. +_wt_gitdir_export="$_wt_gitdir" + # --git-common-dir may be relative to the current directory; normalize both so -# they are comparable regardless of separator style or symlinks. +# they are comparable regardless of separator style or symlinks. These physical +# paths are private validation values and are not exported. _wt_gitdir="$(_wt_realdir "$_wt_gitdir")" || _wt_gitdir="" _wt_common="$(_wt_realdir "$_wt_common")" || _wt_common="" if [ -z "$_wt_gitdir" ] || [ -z "$_wt_common" ]; then @@ -223,7 +231,7 @@ esac export WT_REPO_ROOT="$_wt_toplevel" export WT_PROJECT_DIR="$_wt_toplevel" -export WT_GIT_DIR="$_wt_gitdir" +export WT_GIT_DIR="$_wt_gitdir_export" export WT_HEAD_STATE="$_wt_head_state" export WT_HEAD_BRANCH="$_wt_head_branch" From 11181eb0ec3cbd665412198a2793d985ff1df1e9 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:46:04 +0100 Subject: [PATCH 11/21] test: expose whitespace-only branch expectation mismatch --- scripts/worktree_guard.expectation.tests.sh | 46 ++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh index 93477ff857..786caaa763 100644 --- a/scripts/worktree_guard.expectation.tests.sh +++ b/scripts/worktree_guard.expectation.tests.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Cross-shell regression contract for contradictory worktree HEAD expectations. -# Supplying a branch name while explicitly requiring a detached HEAD is a caller -# setup error, not a condition either guard may silently resolve or ignore. +# Cross-shell regression contract for malformed or contradictory worktree HEAD +# expectations. These are caller setup errors, not conditions either guard may +# silently resolve, normalize differently, or treat as ordinary HEAD mismatch. set -euo pipefail @@ -42,16 +42,17 @@ assert_setup_error() { local name="$1" local code="$2" local output="$3" + local expected="$4" if [ "$code" -ne 2 ]; then printf '%s\n' "$output" >&2 - fail "$name must exit 2 for contradictory expectations (got $code)" + fail "$name must exit 2 for invalid expectations (got $code)" fi - if ! printf '%s' "$output" | grep -qF -- "cannot be combined"; then + if ! printf '%s' "$output" | grep -qF -- "$expected"; then printf '%s\n' "$output" >&2 - fail "$name did not explain the contradictory expectations" + fail "$name did not explain the invalid expectations (missing '$expected')" fi - pass "$name rejects contradictory expectations as a setup error" + pass "$name rejects invalid expectations as a setup error" } if [ ! -f "$SH_GUARD" ] || [ ! -f "$PS_GUARD" ]; then @@ -74,7 +75,19 @@ sh_output="$( )" sh_code=$? set -e -assert_setup_error "shell guard" "$sh_code" "$sh_output" +assert_setup_error "shell guard contradictory expectation" "$sh_code" "$sh_output" "cannot be combined" + +set +e +sh_whitespace_output="$( + cd -- "$FIXTURE_ROOT/detached" + WT_EXPECT_HEAD=any \ + WT_EXPECT_BRANCH=' ' \ + bash -c 'source "$1"' bash "$SH_GUARD" 2>&1 +)" +sh_whitespace_code=$? +set -e +assert_setup_error "shell guard whitespace-only branch" \ + "$sh_whitespace_code" "$sh_whitespace_output" "cannot be whitespace-only" if [ -z "$PS_EXE" ]; then printf ' SKIP: PowerShell guard contract (no powershell/pwsh on PATH)\n' @@ -88,7 +101,20 @@ else )" ps_code=$? set -e - assert_setup_error "PowerShell guard" "$ps_code" "$ps_output" + assert_setup_error "PowerShell guard contradictory expectation" \ + "$ps_code" "$ps_output" "cannot be combined" + + set +e + ps_whitespace_output="$( + cd -- "$FIXTURE_ROOT/detached" + "$PS_EXE" -NoLogo -NoProfile -NonInteractive -File "$PS_GUARD_NATIVE" \ + -ExpectHead Any \ + -ExpectedBranch ' ' 2>&1 + )" + ps_whitespace_code=$? + set -e + assert_setup_error "PowerShell guard whitespace-only branch" \ + "$ps_whitespace_code" "$ps_whitespace_output" "cannot be whitespace-only" fi -printf 'worktree_guard expectation contract passed.\n' +printf 'worktree_guard expectation contract passed.\n' \ No newline at end of file From 84fc9a5def7404d7d8b98f80f8e8be981d311f55 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:53:08 +0100 Subject: [PATCH 12/21] fix: reject whitespace-only guard branches --- scripts/worktree_guard.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/worktree_guard.sh b/scripts/worktree_guard.sh index d067e50724..2e168d63b4 100644 --- a/scripts/worktree_guard.sh +++ b/scripts/worktree_guard.sh @@ -185,6 +185,11 @@ fi _wt_expect_head="${WT_EXPECT_HEAD:-any}" _wt_expect_branch="${WT_EXPECT_BRANCH:-}" +if [ -n "$_wt_expect_branch" ] && [[ "$_wt_expect_branch" =~ ^[[:space:]]+$ ]]; then + echo "ERROR [worktree_guard]: WT_EXPECT_BRANCH cannot be whitespace-only." >&2 + _wt_cleanup + return 2 2>/dev/null || exit 2 +fi if [ -n "$_wt_expect_branch" ] && [ "$_wt_expect_head" = "detached" ]; then echo "ERROR [worktree_guard]: WT_EXPECT_HEAD=detached cannot be combined with WT_EXPECT_BRANCH." >&2 _wt_cleanup From e507fea64d0a5127ec09426509797a7e839bab7a Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:53:45 +0100 Subject: [PATCH 13/21] fix: align PowerShell whitespace branch validation --- scripts/worktree_guard.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/worktree_guard.ps1 b/scripts/worktree_guard.ps1 index 40140d5492..03f426888c 100644 --- a/scripts/worktree_guard.ps1 +++ b/scripts/worktree_guard.ps1 @@ -239,6 +239,10 @@ if ($symbolicHead.Succeeded -and $symbolicHead.ExitCode -eq 0 -and $symbolicHead $headState = if ([string]::IsNullOrEmpty($headBranch)) { "detached" } else { "branch" } $effectiveExpectHead = $ExpectHead +if (-not [string]::IsNullOrEmpty($ExpectedBranch) -and [string]::IsNullOrWhiteSpace($ExpectedBranch)) { + Write-Error "ERROR [worktree_guard]: -ExpectedBranch cannot be whitespace-only." -ErrorAction Continue + exit 2 +} if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Detached") { Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue exit 2 From 92e58bdb00a4d0a91c63811859b22a58375b3886 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:35:22 +0100 Subject: [PATCH 14/21] test(harness): reproduce wrapped PowerShell guard output --- scripts/worktree_guard.expectation.tests.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh index 786caaa763..92452d84ee 100644 --- a/scripts/worktree_guard.expectation.tests.sh +++ b/scripts/worktree_guard.expectation.tests.sh @@ -55,6 +55,9 @@ assert_setup_error() { pass "$name rejects invalid expectations as a setup error" } +assert_setup_error "formatter-wrapped setup error" \ + 2 $'ERROR: cannot be\r\ncombined with -ExpectedBranch' "cannot be combined" + if [ ! -f "$SH_GUARD" ] || [ ! -f "$PS_GUARD" ]; then fail "guard scripts were not found beside the contract" fi @@ -117,4 +120,4 @@ else "$ps_whitespace_code" "$ps_whitespace_output" "cannot be whitespace-only" fi -printf 'worktree_guard expectation contract passed.\n' \ No newline at end of file +printf 'worktree_guard expectation contract passed.\n' From 29e57d5a6a439cf9b3c346f7ed99ed850f019fea Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:36:27 +0100 Subject: [PATCH 15/21] fix(harness): normalize formatter whitespace in guard assertions --- scripts/worktree_guard.expectation.tests.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh index 92452d84ee..3d79cdc417 100644 --- a/scripts/worktree_guard.expectation.tests.sh +++ b/scripts/worktree_guard.expectation.tests.sh @@ -38,17 +38,26 @@ pass() { printf ' PASS: %s\n' "$1" } +normalize_assertion_output() { + printf '%s' "$1" | + tr '\r\n\t' ' ' | + sed 's/[[:space:]][[:space:]]*/ /g' +} + assert_setup_error() { local name="$1" local code="$2" local output="$3" local expected="$4" + local normalized_output if [ "$code" -ne 2 ]; then printf '%s\n' "$output" >&2 fail "$name must exit 2 for invalid expectations (got $code)" fi - if ! printf '%s' "$output" | grep -qF -- "$expected"; then + + normalized_output="$(normalize_assertion_output "$output")" + if ! printf '%s' "$normalized_output" | grep -qF -- "$expected"; then printf '%s\n' "$output" >&2 fail "$name did not explain the invalid expectations (missing '$expected')" fi From a2babac1cea849c1f43c98a913867ffcb56ecc55 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:36:36 +0100 Subject: [PATCH 16/21] ci(harness): exercise guard expectations on Windows --- .github/workflows/reusable-docs-governance.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/reusable-docs-governance.yml b/.github/workflows/reusable-docs-governance.yml index a0ac2262ad..ff79c1d9bd 100644 --- a/.github/workflows/reusable-docs-governance.yml +++ b/.github/workflows/reusable-docs-governance.yml @@ -20,6 +20,10 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Validate worktree guard expectation contract + shell: bash + run: bash scripts/worktree_guard.expectation.tests.sh + - name: Validate shell guard export path contract shell: bash run: bash scripts/worktree_guard.path-flavor.tests.sh From fc77b9435a27b76b4288f4202ed7a0f3bf105a90 Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:49:27 +0100 Subject: [PATCH 17/21] test: expose Unicode branch expectation mismatch --- scripts/worktree_guard.expectation.tests.sh | 46 ++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh index 3d79cdc417..398a54702e 100644 --- a/scripts/worktree_guard.expectation.tests.sh +++ b/scripts/worktree_guard.expectation.tests.sh @@ -64,6 +64,25 @@ assert_setup_error() { pass "$name rejects invalid expectations as a setup error" } +assert_named_branch_mismatch() { + local name="$1" + local code="$2" + local output="$3" + local normalized_output + + if [ "$code" -ne 1 ]; then + printf '%s\n' "$output" >&2 + fail "$name must treat the Unicode value as a named branch expectation (got $code)" + fi + + normalized_output="$(normalize_assertion_output "$output")" + if ! printf '%s' "$normalized_output" | grep -qF -- "detached but a branch was required"; then + printf '%s\n' "$output" >&2 + fail "$name did not reach the ordinary named-branch mismatch" + fi + pass "$name treats Unicode whitespace as a named branch character" +} + assert_setup_error "formatter-wrapped setup error" \ 2 $'ERROR: cannot be\r\ncombined with -ExpectedBranch' "cannot be combined" @@ -101,6 +120,19 @@ set -e assert_setup_error "shell guard whitespace-only branch" \ "$sh_whitespace_code" "$sh_whitespace_output" "cannot be whitespace-only" +unicode_branch=$'\u00a0' +set +e +sh_unicode_output="$( + cd -- "$FIXTURE_ROOT/detached" + WT_EXPECT_HEAD=any \ + WT_EXPECT_BRANCH="$unicode_branch" \ + bash -c 'source "$1"' bash "$SH_GUARD" 2>&1 +)" +sh_unicode_code=$? +set -e +assert_named_branch_mismatch "shell guard Unicode branch" \ + "$sh_unicode_code" "$sh_unicode_output" + if [ -z "$PS_EXE" ]; then printf ' SKIP: PowerShell guard contract (no powershell/pwsh on PATH)\n' else @@ -127,6 +159,18 @@ else set -e assert_setup_error "PowerShell guard whitespace-only branch" \ "$ps_whitespace_code" "$ps_whitespace_output" "cannot be whitespace-only" + + set +e + ps_unicode_output="$( + cd -- "$FIXTURE_ROOT/detached" + "$PS_EXE" -NoLogo -NoProfile -NonInteractive -File "$PS_GUARD_NATIVE" \ + -ExpectHead Any \ + -ExpectedBranch "$unicode_branch" 2>&1 + )" + ps_unicode_code=$? + set -e + assert_named_branch_mismatch "PowerShell guard Unicode branch" \ + "$ps_unicode_code" "$ps_unicode_output" fi -printf 'worktree_guard expectation contract passed.\n' +printf 'worktree_guard expectation contract passed.\n' \ No newline at end of file From bf10ffa9d97d39a815e20056cb31bd00bc06d68d Mon Sep 17 00:00:00 2001 From: Cristian Tcaci <59696583+Chris0Jeky@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:49:16 +0100 Subject: [PATCH 18/21] chore: apply Unicode branch expectation alignment --- .../workflows/pr-3113-unicode-branch-fix.yml | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/pr-3113-unicode-branch-fix.yml diff --git a/.github/workflows/pr-3113-unicode-branch-fix.yml b/.github/workflows/pr-3113-unicode-branch-fix.yml new file mode 100644 index 0000000000..a2e0f87f14 --- /dev/null +++ b/.github/workflows/pr-3113-unicode-branch-fix.yml @@ -0,0 +1,59 @@ +name: Apply PR 3113 Unicode branch expectation alignment + +on: + push: + branches: + - codex/1855-submodule-guard-contract-20260915 + paths: + - .github/workflows/pr-3113-unicode-branch-fix.yml + +permissions: + contents: write + +jobs: + apply: + runs-on: windows-latest + timeout-minutes: 15 + steps: + - name: Checkout branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: codex/1855-submodule-guard-contract-20260915 + fetch-depth: 0 + + - name: Align PowerShell branch-presence semantics + shell: bash + run: | + python - <<'PY' + from pathlib import Path + + path = Path('scripts/worktree_guard.ps1') + source = path.read_text(encoding='utf-8-sig') + old = '$effectiveExpectHead = $ExpectHead\nif (-not [string]::IsNullOrEmpty($ExpectedBranch) -and [string]::IsNullOrWhiteSpace($ExpectedBranch)) {\n Write-Error "ERROR [worktree_guard]: -ExpectedBranch cannot be whitespace-only." -ErrorAction Continue\n exit 2\n}\nif (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Detached") {\n Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue\n exit 2\n}\nif (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Any") {\n $effectiveExpectHead = "Branch"\n}\n' + new = 'function Test-GuardAsciiWhitespaceOnly {\n param([AllowEmptyString()][string]$Value)\n\n # Git branch names are byte-preserving Unicode strings. Match the shell\n # guard by rejecting only ASCII horizontal/vertical whitespace; .NET\'s\n # IsNullOrWhiteSpace also classifies valid characters such as NBSP.\n return (-not [string]::IsNullOrEmpty($Value)) -and\n [System.Text.RegularExpressions.Regex]::IsMatch($Value, "^[\\x09-\\x0D\\x20]+$")\n}\n\n$effectiveExpectHead = $ExpectHead\n$hasExpectedBranch = -not [string]::IsNullOrEmpty($ExpectedBranch)\nif ($hasExpectedBranch -and (Test-GuardAsciiWhitespaceOnly $ExpectedBranch)) {\n Write-Error "ERROR [worktree_guard]: -ExpectedBranch cannot be whitespace-only." -ErrorAction Continue\n exit 2\n}\nif ($hasExpectedBranch -and $effectiveExpectHead -eq "Detached") {\n Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue\n exit 2\n}\nif ($hasExpectedBranch -and $effectiveExpectHead -eq "Any") {\n $effectiveExpectHead = "Branch"\n}\n' + if source.count(old) != 1: + raise SystemExit('PowerShell expectation anchor changed') + source = source.replace(old, new) + source = source.replace('expected: $(if ([string]::IsNullOrWhiteSpace($ExpectedBranch)) { "" } else { $ExpectedBranch })', 'expected: $(if (-not $hasExpectedBranch) { "" } else { $ExpectedBranch })') + source = source.replace('if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $headBranch -ne $ExpectedBranch) {', 'if ($hasExpectedBranch -and $headBranch -ne $ExpectedBranch) {') + path.write_text(source, encoding='utf-8', newline='\n') + + contract = Path('scripts/worktree_guard.expectation.tests.sh') + contract.write_text(contract.read_text(encoding='utf-8-sig'), encoding='utf-8', newline='\n') + + Path('.github/workflows/pr-3113-unicode-branch-fix.yml').unlink() + PY + + - name: Run cross-shell expectation contract + shell: bash + run: bash scripts/worktree_guard.expectation.tests.sh + + - name: Commit verified alignment + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add scripts/worktree_guard.ps1 scripts/worktree_guard.expectation.tests.sh .github/workflows/pr-3113-unicode-branch-fix.yml + git diff --cached --check + git commit -m "fix: preserve Unicode branch expectations across guards" + git push origin HEAD:codex/1855-submodule-guard-contract-20260915 From 22f675b683d0ec74c663b9253c198793c9d4e0dc Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Fri, 18 Sep 2026 22:38:14 +0100 Subject: [PATCH 19/21] ci: remove completed 3113 applicator workflow --- .../workflows/pr-3113-unicode-branch-fix.yml | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 .github/workflows/pr-3113-unicode-branch-fix.yml diff --git a/.github/workflows/pr-3113-unicode-branch-fix.yml b/.github/workflows/pr-3113-unicode-branch-fix.yml deleted file mode 100644 index a2e0f87f14..0000000000 --- a/.github/workflows/pr-3113-unicode-branch-fix.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Apply PR 3113 Unicode branch expectation alignment - -on: - push: - branches: - - codex/1855-submodule-guard-contract-20260915 - paths: - - .github/workflows/pr-3113-unicode-branch-fix.yml - -permissions: - contents: write - -jobs: - apply: - runs-on: windows-latest - timeout-minutes: 15 - steps: - - name: Checkout branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: codex/1855-submodule-guard-contract-20260915 - fetch-depth: 0 - - - name: Align PowerShell branch-presence semantics - shell: bash - run: | - python - <<'PY' - from pathlib import Path - - path = Path('scripts/worktree_guard.ps1') - source = path.read_text(encoding='utf-8-sig') - old = '$effectiveExpectHead = $ExpectHead\nif (-not [string]::IsNullOrEmpty($ExpectedBranch) -and [string]::IsNullOrWhiteSpace($ExpectedBranch)) {\n Write-Error "ERROR [worktree_guard]: -ExpectedBranch cannot be whitespace-only." -ErrorAction Continue\n exit 2\n}\nif (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Detached") {\n Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue\n exit 2\n}\nif (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Any") {\n $effectiveExpectHead = "Branch"\n}\n' - new = 'function Test-GuardAsciiWhitespaceOnly {\n param([AllowEmptyString()][string]$Value)\n\n # Git branch names are byte-preserving Unicode strings. Match the shell\n # guard by rejecting only ASCII horizontal/vertical whitespace; .NET\'s\n # IsNullOrWhiteSpace also classifies valid characters such as NBSP.\n return (-not [string]::IsNullOrEmpty($Value)) -and\n [System.Text.RegularExpressions.Regex]::IsMatch($Value, "^[\\x09-\\x0D\\x20]+$")\n}\n\n$effectiveExpectHead = $ExpectHead\n$hasExpectedBranch = -not [string]::IsNullOrEmpty($ExpectedBranch)\nif ($hasExpectedBranch -and (Test-GuardAsciiWhitespaceOnly $ExpectedBranch)) {\n Write-Error "ERROR [worktree_guard]: -ExpectedBranch cannot be whitespace-only." -ErrorAction Continue\n exit 2\n}\nif ($hasExpectedBranch -and $effectiveExpectHead -eq "Detached") {\n Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue\n exit 2\n}\nif ($hasExpectedBranch -and $effectiveExpectHead -eq "Any") {\n $effectiveExpectHead = "Branch"\n}\n' - if source.count(old) != 1: - raise SystemExit('PowerShell expectation anchor changed') - source = source.replace(old, new) - source = source.replace('expected: $(if ([string]::IsNullOrWhiteSpace($ExpectedBranch)) { "" } else { $ExpectedBranch })', 'expected: $(if (-not $hasExpectedBranch) { "" } else { $ExpectedBranch })') - source = source.replace('if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $headBranch -ne $ExpectedBranch) {', 'if ($hasExpectedBranch -and $headBranch -ne $ExpectedBranch) {') - path.write_text(source, encoding='utf-8', newline='\n') - - contract = Path('scripts/worktree_guard.expectation.tests.sh') - contract.write_text(contract.read_text(encoding='utf-8-sig'), encoding='utf-8', newline='\n') - - Path('.github/workflows/pr-3113-unicode-branch-fix.yml').unlink() - PY - - - name: Run cross-shell expectation contract - shell: bash - run: bash scripts/worktree_guard.expectation.tests.sh - - - name: Commit verified alignment - shell: bash - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add scripts/worktree_guard.ps1 scripts/worktree_guard.expectation.tests.sh .github/workflows/pr-3113-unicode-branch-fix.yml - git diff --cached --check - git commit -m "fix: preserve Unicode branch expectations across guards" - git push origin HEAD:codex/1855-submodule-guard-contract-20260915 From 5231cdc037ee30b0f974368667201c4d4a82fb46 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Fri, 18 Sep 2026 22:38:15 +0100 Subject: [PATCH 20/21] fix(guard): agree on ASCII-only whitespace branch expectations The PowerShell guard rejected any expectation that .NET calls whitespace, including U+00A0, while the shell guard used [[:space:]], which MSYS2 and glibc classify differently. Both guards now reject only ASCII whitespace, so the same branch name means the same thing on every runner, and the expectation contract added in the previous commit passes. --- scripts/worktree_guard.expectation.tests.sh | 2 +- scripts/worktree_guard.ps1 | 22 ++++++++++++++++----- scripts/worktree_guard.sh | 8 +++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh index 398a54702e..83ce08adbb 100644 --- a/scripts/worktree_guard.expectation.tests.sh +++ b/scripts/worktree_guard.expectation.tests.sh @@ -173,4 +173,4 @@ else "$ps_unicode_code" "$ps_unicode_output" fi -printf 'worktree_guard expectation contract passed.\n' \ No newline at end of file +printf 'worktree_guard expectation contract passed.\n' diff --git a/scripts/worktree_guard.ps1 b/scripts/worktree_guard.ps1 index 03f426888c..25fb6d6919 100644 --- a/scripts/worktree_guard.ps1 +++ b/scripts/worktree_guard.ps1 @@ -87,6 +87,17 @@ if (-not $layoutInvocationSucceeded -or $layoutExitCode -ne 0 -or $layoutOutput. exit 2 } +function Test-GuardAsciiWhitespaceOnly { + param([AllowEmptyString()][string]$Value) + + # Git branch names are byte-preserving Unicode strings, and the shell guard + # rejects only ASCII horizontal/vertical whitespace ([[:space:]] in the C + # locale). .NET's IsNullOrWhiteSpace also classifies valid branch characters + # such as U+00A0 NBSP, which made the two guards disagree on the same name. + return (-not [string]::IsNullOrEmpty($Value)) -and + [System.Text.RegularExpressions.Regex]::IsMatch($Value, "^[\x09-\x0D\x20]+$") +} + $invocationDirectory = (Get-Location).Path function Resolve-GuardGitPath { param([string]$Path) @@ -239,15 +250,16 @@ if ($symbolicHead.Succeeded -and $symbolicHead.ExitCode -eq 0 -and $symbolicHead $headState = if ([string]::IsNullOrEmpty($headBranch)) { "detached" } else { "branch" } $effectiveExpectHead = $ExpectHead -if (-not [string]::IsNullOrEmpty($ExpectedBranch) -and [string]::IsNullOrWhiteSpace($ExpectedBranch)) { +$hasExpectedBranch = -not [string]::IsNullOrEmpty($ExpectedBranch) +if ($hasExpectedBranch -and (Test-GuardAsciiWhitespaceOnly $ExpectedBranch)) { Write-Error "ERROR [worktree_guard]: -ExpectedBranch cannot be whitespace-only." -ErrorAction Continue exit 2 } -if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Detached") { +if ($hasExpectedBranch -and $effectiveExpectHead -eq "Detached") { Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue exit 2 } -if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Any") { +if ($hasExpectedBranch -and $effectiveExpectHead -eq "Any") { $effectiveExpectHead = "Branch" } @@ -264,11 +276,11 @@ if ($effectiveExpectHead -eq "Branch") { Write-Error -ErrorAction Continue @" FATAL [worktree_guard]: Worktree HEAD is detached but a branch was required. toplevel: $topLevel - expected: $(if ([string]::IsNullOrWhiteSpace($ExpectedBranch)) { "" } else { $ExpectedBranch }) + expected: $(if (-not $hasExpectedBranch) { "" } else { $ExpectedBranch }) "@ exit 1 } - if (-not [string]::IsNullOrWhiteSpace($ExpectedBranch) -and $headBranch -ne $ExpectedBranch) { + if ($hasExpectedBranch -and $headBranch -ne $ExpectedBranch) { Write-Error -ErrorAction Continue @" FATAL [worktree_guard]: Worktree HEAD is on the wrong branch. toplevel: $topLevel diff --git a/scripts/worktree_guard.sh b/scripts/worktree_guard.sh index 2e168d63b4..af23f67fa4 100644 --- a/scripts/worktree_guard.sh +++ b/scripts/worktree_guard.sh @@ -185,7 +185,13 @@ fi _wt_expect_head="${WT_EXPECT_HEAD:-any}" _wt_expect_branch="${WT_EXPECT_BRANCH:-}" -if [ -n "$_wt_expect_branch" ] && [[ "$_wt_expect_branch" =~ ^[[:space:]]+$ ]]; then +# Reject an expectation made only of ASCII whitespace. The class is spelled out +# instead of [[:space:]] because that class is locale- and platform-dependent: +# MSYS2/Git-for-Windows classifies U+00A0 NBSP as space while glibc does not, so +# the same branch name would be a setup error on one runner and a branch name on +# another. Git branch names are byte-preserving Unicode, so only ASCII +# whitespace is treated as "no branch was really given". +if [ -n "$_wt_expect_branch" ] && [ -z "${_wt_expect_branch//[$' \t\n\r\v\f']/}" ]; then echo "ERROR [worktree_guard]: WT_EXPECT_BRANCH cannot be whitespace-only." >&2 _wt_cleanup return 2 2>/dev/null || exit 2 From ec65e584b6d108b6fc384229a10c387914e213cb Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Fri, 18 Sep 2026 22:48:12 +0100 Subject: [PATCH 21/21] test(guard): pin the ASCII whitespace class in both shells Space alone cannot distinguish a correctly built ASCII-whitespace class from a mis-built one. Add a tab-only rejection and an escape-letter control (a name of only f, n, r, t, v) on both guards, so a class that degraded to the literal escape letters is caught in both directions. --- scripts/worktree_guard.expectation.tests.sh | 59 ++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/scripts/worktree_guard.expectation.tests.sh b/scripts/worktree_guard.expectation.tests.sh index 83ce08adbb..4e09486ca0 100644 --- a/scripts/worktree_guard.expectation.tests.sh +++ b/scripts/worktree_guard.expectation.tests.sh @@ -72,7 +72,7 @@ assert_named_branch_mismatch() { if [ "$code" -ne 1 ]; then printf '%s\n' "$output" >&2 - fail "$name must treat the Unicode value as a named branch expectation (got $code)" + fail "$name must treat the value as a named branch expectation (got $code)" fi normalized_output="$(normalize_assertion_output "$output")" @@ -80,7 +80,7 @@ assert_named_branch_mismatch() { printf '%s\n' "$output" >&2 fail "$name did not reach the ordinary named-branch mismatch" fi - pass "$name treats Unicode whitespace as a named branch character" + pass "$name treats the value as a named branch expectation" } assert_setup_error "formatter-wrapped setup error" \ @@ -133,6 +133,37 @@ set -e assert_named_branch_mismatch "shell guard Unicode branch" \ "$sh_unicode_code" "$sh_unicode_output" +# The space character alone cannot distinguish a correctly built ASCII-whitespace +# class from a mis-built one, so exercise a non-space member (tab) and a control +# made only of the letters that a literally-taken "\t\n\r\v\f" escape would +# contain. A guard whose class degraded to those letters rejects "fnrtv" as +# whitespace-only, which the two cases below catch in opposite directions. +tab_branch=$'\t' +literal_escape_branch="fnrtv" +set +e +sh_tab_output="$( + cd -- "$FIXTURE_ROOT/detached" + WT_EXPECT_HEAD=any \ + WT_EXPECT_BRANCH="$tab_branch" \ + bash -c 'source "$1"' bash "$SH_GUARD" 2>&1 +)" +sh_tab_code=$? +set -e +assert_setup_error "shell guard tab-only branch" \ + "$sh_tab_code" "$sh_tab_output" "cannot be whitespace-only" + +set +e +sh_literal_escape_output="$( + cd -- "$FIXTURE_ROOT/detached" + WT_EXPECT_HEAD=any \ + WT_EXPECT_BRANCH="$literal_escape_branch" \ + bash -c 'source "$1"' bash "$SH_GUARD" 2>&1 +)" +sh_literal_escape_code=$? +set -e +assert_named_branch_mismatch "shell guard escape-letter branch" \ + "$sh_literal_escape_code" "$sh_literal_escape_output" + if [ -z "$PS_EXE" ]; then printf ' SKIP: PowerShell guard contract (no powershell/pwsh on PATH)\n' else @@ -171,6 +202,30 @@ else set -e assert_named_branch_mismatch "PowerShell guard Unicode branch" \ "$ps_unicode_code" "$ps_unicode_output" + + set +e + ps_tab_output="$( + cd -- "$FIXTURE_ROOT/detached" + "$PS_EXE" -NoLogo -NoProfile -NonInteractive -File "$PS_GUARD_NATIVE" \ + -ExpectHead Any \ + -ExpectedBranch "$tab_branch" 2>&1 + )" + ps_tab_code=$? + set -e + assert_setup_error "PowerShell guard tab-only branch" \ + "$ps_tab_code" "$ps_tab_output" "cannot be whitespace-only" + + set +e + ps_literal_escape_output="$( + cd -- "$FIXTURE_ROOT/detached" + "$PS_EXE" -NoLogo -NoProfile -NonInteractive -File "$PS_GUARD_NATIVE" \ + -ExpectHead Any \ + -ExpectedBranch "$literal_escape_branch" 2>&1 + )" + ps_literal_escape_code=$? + set -e + assert_named_branch_mismatch "PowerShell guard escape-letter branch" \ + "$ps_literal_escape_code" "$ps_literal_escape_output" fi printf 'worktree_guard expectation contract passed.\n'