Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
964cd8e
test: isolate linked-worktree containment with a submodule
Chris0Jeky Sep 15, 2026
af64c92
ci: gate shell worktree guard contracts
Chris0Jeky Sep 15, 2026
2e3bcce
test: reproduce contradictory worktree expectations
Chris0Jeky Sep 15, 2026
593ea93
test: capture guard diagnostics in expectation contract
Chris0Jeky Sep 15, 2026
43f6af2
ci: run contradictory worktree expectation contract
Chris0Jeky Sep 15, 2026
4bf31f1
fix: reject contradictory shell guard expectations
Chris0Jeky Sep 15, 2026
53afb01
fix: reject contradictory PowerShell guard expectations
Chris0Jeky Sep 15, 2026
f458b25
test: reproduce mixed worktree export path flavors
Chris0Jeky Sep 15, 2026
4d28204
ci: verify shell guard path flavor on Windows
Chris0Jeky Sep 15, 2026
43c446b
fix: keep shell guard exports in one path flavor
Chris0Jeky Sep 15, 2026
11181eb
test: expose whitespace-only branch expectation mismatch
Chris0Jeky Sep 15, 2026
84fc9a5
fix: reject whitespace-only guard branches
Chris0Jeky Sep 15, 2026
e507fea
fix: align PowerShell whitespace branch validation
Chris0Jeky Sep 15, 2026
d080647
Merge branch 'main' into codex/1855-submodule-guard-contract-20260915
Chris0Jeky Sep 17, 2026
92e58bd
test(harness): reproduce wrapped PowerShell guard output
Chris0Jeky Sep 17, 2026
29e57d5
fix(harness): normalize formatter whitespace in guard assertions
Chris0Jeky Sep 17, 2026
a2babac
ci(harness): exercise guard expectations on Windows
Chris0Jeky Sep 17, 2026
fc77b94
test: expose Unicode branch expectation mismatch
Chris0Jeky Sep 18, 2026
bf10ffa
chore: apply Unicode branch expectation alignment
Chris0Jeky Sep 18, 2026
22f675b
ci: remove completed 3113 applicator workflow
Chris0Jeky Sep 18, 2026
5231cdc
fix(guard): agree on ASCII-only whitespace branch expectations
Chris0Jeky Sep 18, 2026
78abac8
Merge main into codex/1855-submodule-guard-contract-20260915
Chris0Jeky Sep 18, 2026
ec65e58
test(guard): pin the ASCII whitespace class in both shells
Chris0Jeky Sep 18, 2026
ea68213
Merge main into codex/1855-submodule-guard-contract-20260915
Chris0Jeky Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/reusable-docs-governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ 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

- name: Validate detached-first worktree helper
shell: powershell
run: powershell -NoLogo -NoProfile -NonInteractive -File scripts/git/Test-New-CodexIssueWorktree.ps1
Expand Down Expand Up @@ -48,6 +56,14 @@ 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
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
with:
Expand Down
231 changes: 231 additions & 0 deletions scripts/worktree_guard.expectation.tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
#!/usr/bin/env bash
# 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

_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"
}

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

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
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 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 the value as a named branch expectation"
}

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

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 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"

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"

# 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
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 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"

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"

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'
72 changes: 72 additions & 0 deletions scripts/worktree_guard.path-flavor.tests.sh
Original file line number Diff line number Diff line change
@@ -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"
28 changes: 24 additions & 4 deletions scripts/worktree_guard.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -239,7 +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]::IsNullOrWhiteSpace($ExpectedBranch) -and $effectiveExpectHead -eq "Any") {
$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 ($hasExpectedBranch -and $effectiveExpectHead -eq "Detached") {
Write-Error "ERROR [worktree_guard]: -ExpectHead Detached cannot be combined with -ExpectedBranch." -ErrorAction Continue
exit 2
}
if ($hasExpectedBranch -and $effectiveExpectHead -eq "Any") {
$effectiveExpectHead = "Branch"
}

Expand All @@ -256,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)) { "<any branch>" } else { $ExpectedBranch })
expected: $(if (-not $hasExpectedBranch) { "<any branch>" } 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
Expand Down
Loading
Loading