chore(windows): build and pass the test suite natively on Windows (#1501) #1503

Closed
claude wants to merge 1 commit from claude/1501-windows-native-tests into main
Member

Refs #1501

Head 7163298 on base 55a12b9 (live main at push time).

Why

#1485 gave Windows contributors an LF checkout, but a native go test ./... still fails there. cmd/rt and internal/release do not compile (syscall.Flock). rt register-check never matches an allow-list directory against a Windows path. Several tests assume POSIX permissions, an executable bit, glibc regex, or Unix path forms. Release binaries stay linux/amd64, and Linux behaviour does not change.

What's in the PR

Cut lock (internal/release): real fix

  • lock.go keeps the shared lifecycle (acquireLock / release). The OS primitive moves to lock_flock.go (//go:build !windows, flock as before) and lock_windows.go (LockFileEx, LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY, whole-file range). ERROR_LOCK_VIOLATION maps to ErrConcurrentCut.
  • The tag is !windows rather than unix, so the flock file builds for exactly the targets the untagged lock.go built for.
  • LockFileEx is resolved from kernel32 through stdlib syscall, without adding golang.org/x/sys. That is right while Windows is contributor-only. x/sys/windows becomes right if Windows release runners land (Phase 8) or more Win32 calls accumulate.

register.ScanTree: real fix

  • scanOneFile matches allow-list and self-exclusion entries, and reports FileHit.File, in filepath.ToSlash form; it still reads through the native path. ToSlash is the identity on Linux, so register-check output there is byte-identical.

Windows skips, in the existing t.Skip reason style (the tests still run on Linux)

  • TestDiscoverUnreadableIsUngradeable, and TestWritePostCutManifestCustomTagFormat/config_is_UNREADABLE_by_mode_is_could_not_grade. On Windows the latter was green for the wrong reason (see #1501).
  • TestRunPostBumpHooks_theHookDoesNotReceiveTheGitCredential: Windows has no executable bit and cannot exec #!/bin/sh.
  • TestRepinFile_PreservesModeAndSkipsNoMatch: only the mode half is ungraded on Windows (t.Log); the unmatched-file half still runs.
  • TestHeaderEmDashLocaleAndByteClassMutation: the live-pattern subtests run on Windows; only the mutant half skips, because MSYS regex rejects [-—] under C.utf8 too.

cmd/rt test-side fixes, not skips

  • TestChangelogBodyCheckBadThreshold builds rt.exe on Windows.
  • TestFragmentCheckSummaryWordWarnExecutes and TestFragmentCheckLengthWarnCountsRunes now hand the gate a relative dir via a new relFragDir, because a C:\ path correctly takes the safe-char stderr fallback. The expected path uses /, as the gate builds it.
  • TestPrepStagesTheRegeneratedTwinIntoTheIndex uses path.Join for git index keys.

TestPushManifestAuthedRejectsSecondPush: the deadline goes from 2s to 30s. The retry has no backoff, and the "xx" attempt counter stays as the discriminating assertion. Under load, the 2s deadline cancelled attempt 2 before its hook ran, and the error text was the same.

Fragment: changelog.d/1501-windows-native-tests.internal.md (patch lane; nothing a consumer can observe).

Acceptance criteria (#1501)

  • internal/release and cmd/rt build on Windows; the Windows backend returns ErrConcurrentCut in-process and cross-process, and frees the lock when the holder is killed. TestNewCutter_concurrentCut is green on Windows and goes red with tryLockFile neutered. A throwaway cross-process probe (not committed) showed: the second process gets ErrConcurrentCut, the lock is free immediately after the holder is killed, and a double release is a no-op.
  • On Windows, ScanTree matches and reports in / form, and Linux output is unchanged. TestScanTree is red without ToSlash and green with it.
  • Tests needing POSIX mode bits, an executable bit or glibc regex skip on Windows with a reason, and still run on Linux. With -v: SKIP on Windows, PASS on Linux (non-root) for each.
  • The four cmd/rt path and .exe assumptions are fixed in the tests, not skipped.
  • Native Windows go test ./... passes on an LF checkout, and golang:1.25 stays green as non-root (see Test plan).

Test plan

  • Native Windows (Go 1.27.1), fresh core.autocrlf=true clone of 55a12b9 checked out at 7163298, go test -count=1 ./...: 29/29 ok.
  • golang:1.25, same commit, as a non-root user so the existing root skips do not fire: 29/29 ok.
  • go vet ./... for windows, linux and darwin; gofmt -l (Go 1.25) clean; FRAGMENT_CHECK_COVERAGE=content rt fragment-check changelog.d PASS.
  • Reviewer: CI covers the Linux half. The Windows half needs a Windows checkout: go test ./....

Review coordination

  • Author: announce HOLD in the PR thread before requesting or starting an
    approval read if more commits or a PR-body correction is still expected.
  • Reviewer: wait for that explicit HOLD before approving. A
    REQUEST_CHANGES asks the author to fix the PR; it does not freeze the
    author from pushing.
  • Reviewer: immediately before submitting a review, run
    rt review-submit-check --owner OWNER --repo REPO --pr N --reviewed-head SHA
    with the full 40-character server head that was read. rc=0 means only that
    the head still matches; rc=1 requires a fresh read, and rc=2 means the
    check could not grade. This does not check approval eligibility or review
    rows, and it does not submit the review. Omit commit_id when submitting,
    name the exact reviewed SHA in the review body, and read the returned row
    back.
  • Author: after a body-only correction, announce it to the reviewer and obtain
    a fresh read; the head and a human rejection do not move or clear by
    themselves.

What this PR does NOT do

  • Windows release binaries or release runners.
  • Strengthen the post-cut 0o000 arm to assert the could-not-read message (out of scope in #1501; not measured on Linux).
  • A Linux-discriminating test for the ScanTree fix. On Linux a backslash is a filename byte and ToSlash is the identity, so only a Windows run can catch a regression there.
  • Reformat cmd/rt/mirror_drift_check_test.go. Go 1.27's gofmt rewrites its indentation and Go 1.25's does not; this PR does not touch the file.

🤖 Generated with Claude Code

Refs #1501 Head `7163298` on base `55a12b9` (live `main` at push time). ## Why #1485 gave Windows contributors an LF checkout, but a native `go test ./...` still fails there. `cmd/rt` and `internal/release` do not compile (`syscall.Flock`). `rt register-check` never matches an allow-list directory against a Windows path. Several tests assume POSIX permissions, an executable bit, glibc regex, or Unix path forms. Release binaries stay linux/amd64, and Linux behaviour does not change. ## What's in the PR **Cut lock (`internal/release`): real fix** - `lock.go` keeps the shared lifecycle (`acquireLock` / `release`). The OS primitive moves to `lock_flock.go` (`//go:build !windows`, flock as before) and `lock_windows.go` (`LockFileEx`, `LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY`, whole-file range). `ERROR_LOCK_VIOLATION` maps to `ErrConcurrentCut`. - The tag is `!windows` rather than `unix`, so the flock file builds for exactly the targets the untagged `lock.go` built for. - `LockFileEx` is resolved from kernel32 through stdlib `syscall`, without adding `golang.org/x/sys`. That is right while Windows is contributor-only. `x/sys/windows` becomes right if Windows release runners land (Phase 8) or more Win32 calls accumulate. **`register.ScanTree`: real fix** - `scanOneFile` matches allow-list and self-exclusion entries, and reports `FileHit.File`, in `filepath.ToSlash` form; it still reads through the native path. `ToSlash` is the identity on Linux, so register-check output there is byte-identical. **Windows skips, in the existing `t.Skip` reason style (the tests still run on Linux)** - `TestDiscoverUnreadableIsUngradeable`, and `TestWritePostCutManifestCustomTagFormat/config_is_UNREADABLE_by_mode_is_could_not_grade`. On Windows the latter was green for the wrong reason (see #1501). - `TestRunPostBumpHooks_theHookDoesNotReceiveTheGitCredential`: Windows has no executable bit and cannot exec `#!/bin/sh`. - `TestRepinFile_PreservesModeAndSkipsNoMatch`: only the mode half is ungraded on Windows (`t.Log`); the unmatched-file half still runs. - `TestHeaderEmDashLocaleAndByteClassMutation`: the live-pattern subtests run on Windows; only the mutant half skips, because MSYS regex rejects `[-—]` under `C.utf8` too. **`cmd/rt` test-side fixes, not skips** - `TestChangelogBodyCheckBadThreshold` builds `rt.exe` on Windows. - `TestFragmentCheckSummaryWordWarnExecutes` and `TestFragmentCheckLengthWarnCountsRunes` now hand the gate a relative dir via a new `relFragDir`, because a `C:\` path correctly takes the safe-char stderr fallback. The expected path uses `/`, as the gate builds it. - `TestPrepStagesTheRegeneratedTwinIntoTheIndex` uses `path.Join` for git index keys. **`TestPushManifestAuthedRejectsSecondPush`**: the deadline goes from 2s to 30s. The retry has no backoff, and the `"xx"` attempt counter stays as the discriminating assertion. Under load, the 2s deadline cancelled attempt 2 before its hook ran, and the error text was the same. **Fragment**: `changelog.d/1501-windows-native-tests.internal.md` (patch lane; nothing a consumer can observe). ## Acceptance criteria (#1501) - [x] `internal/release` and `cmd/rt` build on Windows; the Windows backend returns `ErrConcurrentCut` in-process and cross-process, and frees the lock when the holder is killed. `TestNewCutter_concurrentCut` is green on Windows and goes red with `tryLockFile` neutered. A throwaway cross-process probe (not committed) showed: the second process gets `ErrConcurrentCut`, the lock is free immediately after the holder is killed, and a double `release` is a no-op. - [x] On Windows, `ScanTree` matches and reports in `/` form, and Linux output is unchanged. `TestScanTree` is red without `ToSlash` and green with it. - [x] Tests needing POSIX mode bits, an executable bit or glibc regex skip on Windows with a reason, and still run on Linux. With `-v`: SKIP on Windows, PASS on Linux (non-root) for each. - [x] The four `cmd/rt` path and `.exe` assumptions are fixed in the tests, not skipped. - [x] Native Windows `go test ./...` passes on an LF checkout, and `golang:1.25` stays green as non-root (see Test plan). ## Test plan - [x] Native Windows (Go 1.27.1), fresh `core.autocrlf=true` clone of `55a12b9` checked out at `7163298`, `go test -count=1 ./...`: 29/29 ok. - [x] `golang:1.25`, same commit, as a non-root user so the existing root skips do not fire: 29/29 ok. - [x] `go vet ./...` for windows, linux and darwin; `gofmt -l` (Go 1.25) clean; `FRAGMENT_CHECK_COVERAGE=content rt fragment-check changelog.d` PASS. - [ ] Reviewer: CI covers the Linux half. The Windows half needs a Windows checkout: `go test ./...`. ## Review coordination - Author: announce `HOLD` in the PR thread before requesting or starting an approval read if more commits or a PR-body correction is still expected. - Reviewer: wait for that explicit `HOLD` before approving. A `REQUEST_CHANGES` asks the author to fix the PR; it does not freeze the author from pushing. - Reviewer: immediately before submitting a review, run `rt review-submit-check --owner OWNER --repo REPO --pr N --reviewed-head SHA` with the full 40-character server head that was read. `rc=0` means only that the head still matches; `rc=1` requires a fresh read, and `rc=2` means the check could not grade. This does not check approval eligibility or review rows, and it does not submit the review. Omit `commit_id` when submitting, name the exact reviewed SHA in the review body, and read the returned row back. - Author: after a body-only correction, announce it to the reviewer and obtain a fresh read; the head and a human rejection do not move or clear by themselves. ## What this PR does NOT do - Windows release binaries or release runners. - Strengthen the post-cut `0o000` arm to assert the could-not-read message (out of scope in #1501; not measured on Linux). - A Linux-discriminating test for the `ScanTree` fix. On Linux a backslash is a filename byte and `ToSlash` is the identity, so only a Windows run can catch a regression there. - Reformat `cmd/rt/mirror_drift_check_test.go`. Go 1.27's gofmt rewrites its indentation and Go 1.25's does not; this PR does not touch the file. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chore(windows): build and pass the test suite natively on Windows (#1501)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
base-divergence-check / check (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 7s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 6s
go-ci / lint + build + test (pull_request) Successful in 31s
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 7s
readme-pin-check / mirror (pull_request) Successful in 8s
readme-pin-check / digest (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 32s
tests / dated-examples (pull_request) Successful in 5s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
7163298262
The cut lock moves its OS primitive behind a build tag: lock_flock.go
(!windows, unchanged flock) and lock_windows.go (LockFileEx with
FAIL_IMMEDIATELY). Contention still surfaces as ErrConcurrentCut,
in-process and cross-process, and the OS frees the lock when the holder
dies.

register.ScanTree matches allow-list and self-exclusion entries, and
reports hits, in slash form. On Windows the allow-list directory never
matched a WalkDir path; on Linux ToSlash is the identity.

Tests that need POSIX mode bits, an executable bit, or glibc regex skip
on Windows with a reason, and still run on Linux. The four cmd/rt tests
that assumed a bare "rt" binary, an absolute path inside the annotation
safe-char set, or backslash git index keys are fixed instead. The
second-push test's 2s deadline becomes a 30s hang guard: under load it
cancelled attempt 2 before the hook ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Member

Superseded by #1505, which landed as 96d2646 and covers this PR's scope and more (committed cross-process and killed-holder lock tests, graded 0o000 arms on Windows, a by-name post_bump_hooks refusal). #1501 is ticked against that landing. Shutting this unmerged and deleting claude/1501-windows-native-tests.

Superseded by #1505, which landed as `96d2646` and covers this PR's scope and more (committed cross-process and killed-holder lock tests, graded `0o000` arms on Windows, a by-name `post_bump_hooks` refusal). #1501 is ticked against that landing. Shutting this unmerged and deleting `claude/1501-windows-native-tests`.
claude closed this pull request 2026-10-02 22:31:40 +02:00
Some checks are pending
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
base-divergence-check / check (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 7s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 6s
go-ci / lint + build + test (pull_request) Successful in 31s
Required
Details
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 7s
readme-pin-check / mirror (pull_request) Successful in 8s
readme-pin-check / digest (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 32s
Required
Details
tests / dated-examples (pull_request) Successful in 5s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
Required
Details
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
pr / gates (pull_request)
Required
tests / checks (pull_request)
Required

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frankenbit/release-toolkit!1503
No description provided.