Skip to content

test(tools): stop internal/tools failing on stock Windows - #814

Merged
kevincodex1 merged 2 commits into
mainfrom
fix/windows-exec-test-flakes
Jul 28, 2026
Merged

test(tools): stop internal/tools failing on stock Windows#814
kevincodex1 merged 2 commits into
mainfrom
fix/windows-exec-test-flakes

Conversation

@Vasanthdev2004

@Vasanthdev2004 Vasanthdev2004 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

internal/tools fails on every run on a Windows box without PowerShell 7, which is what a stock install is. PowerShell 7 is a separate download; the GitHub runners have it, so CI has never seen any of this. Anyone cloning Zero on Windows hits an intermittent, and in one case constant, failure with no obvious cause.

Three distinct problems, all surfaced by the move to PowerShell in #804.

1. The stop-intent test raced the shell's teardown

TestWriteStdinStopIntentTerminatesNonTTYSession waited a fixed second for a session to die, then asserted on whatever happened to be true. Under cmd.exe a second was plenty. Under PowerShell the tree is slower to stand up and tear down, and the FIRST stop in a fresh process pays the interpreter's cold start, so it routinely returned with termination under way but not finished. The assertion read that as "the stop did not work".

This is the one I originally reported to @anandh8x on #804 as "exit is broken while Ctrl+C still works". That was wrong, and I corrected it there: the failing input alternates, because what matters is which stop runs first, not which input it is. Measured, varying only the wait:

yield 1s  -> reports still running
yield 3s  -> stops cleanly
yield 6s  -> stops cleanly

It now re-sends the stop until the session reports it has exited, rather than guessing a duration. Repeating is safe because Terminate is a no-op against an already-dead tree, and it keeps the interrupt flag that a poll with empty chars would drop.

2. Every session test raced Windows teardown, pre-existing

A terminated process does not release its handles the instant the session reports exited, and Windows refuses to remove a directory with an open handle where POSIX does not. t.TempDir removes once and fails the test on the sharing violation, so ordinary teardown timing was reported as a broken test. This affected TestExecSessionSnapshotsAndStopAll and TestWriteStdinRejectsInputForNonTTYSession as well, and which one surfaced varied by whichever held a handle when cleanup ran.

Those roots now clean up with a bounded retry and give up quietly rather than failing. The process is already terminated, so the worst case is a stale directory under the OS temp root, which is not worth failing a test over.

I fixed this as a class rather than per test, because chasing them one at a time was going to keep producing new names.

3. A new preflight silently shadowed an older assertion

TestBashToolRequireEscalatedMsysGuard asserts a windows_shell_syntax block, using a command containing &&. On PowerShell 5.1 the windows_powershell_version preflight added by #804 fires first and returns a different shell_issue, so the assertion fails. On PowerShell 7 that check does not fire at all, which is why CI is green.

The && was incidental to what the case is about, so it is gone. Worth recording as a shape rather than a typo: adding a preflight changed which block an older assertion received, and only on the shell CI does not run.

Verification

Run repeatedly rather than once, since a single green run proves nothing here:

  • whole package: previously failing on every run, now 3 of 3 passing
  • stop-intent test alone: 8 of 8, and 6 of 6 under saturating CPU load
  • disabling the terminate path still fails the stop-intent test, so the assertion continues to bind rather than having been relaxed into always passing

gofmt, go vet, and builds for linux, darwin and windows are clean.

Test-only apart from nothing; no production code is touched.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of command execution on Windows by making temporary workspace cleanup more consistent.
    • Enhanced stopping behavior for non-interactive sessions by ensuring stop requests fully terminate the session before proceeding.
    • Reduced intermittent failures tied to shell startup, process cleanup timing, and Windows platform-specific command handling.

This package fails on every run on a Windows box without PowerShell 7, which
is what a stock install is: PowerShell 7 is a separate download. CI never sees
any of it, because the runners have 7. Three distinct causes, all surfaced by
the move to PowerShell in #804.

The stop-intent test waited a fixed second for a session to die and asserted on
whatever was true by then. Under cmd.exe that was enough; under PowerShell the
tree is slower to stand up and tear down, and the FIRST stop in a fresh process
pays the interpreter's cold start, so it routinely returned with termination
under way but unfinished. The assertion read that as a failed stop. It now
re-sends the stop until the session reports it has exited. Repeating is safe,
since Terminate is a no-op against an already-dead tree, and it keeps the
interrupt flag that a poll with empty chars would drop.

Separately, and pre-existing, every test that starts a session hit a teardown
race: a terminated process does not release its handles the instant the session
reports exited, and Windows refuses to remove a directory with an open handle
where POSIX does not. t.TempDir removes once and fails the test on the sharing
violation, so ordinary teardown timing was reported as a broken test. Those
roots now clean up with a bounded retry, and give up quietly rather than
failing: the process is already terminated, so the worst case is a stale
directory under the OS temp root.

Last, the MSYS guard test asserted a windows_shell_syntax block using a command
containing `&&`. On PowerShell 5.1 the windows_powershell_version preflight
added by #804 fires first and shadows it. The `&&` was incidental to what that
case is about, so it is gone. This one is worth noting as a shape rather than a
typo: a new preflight silently changed which block an older assertion got, and
only on the shell CI does not run.

Verified by running the package repeatedly rather than once: previously failing
on every run, now passing 3 of 3, and the stop-intent test passing 8 of 8 alone
and 6 of 6 under CPU contention. Disabling the terminate path still fails it,
so the assertion continues to bind.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dfc32bc3-6065-4db0-bb39-12a3ff7de7d0

📥 Commits

Reviewing files that changed from the base of the PR and between 2003234 and aa7ce57.

📒 Files selected for processing (1)
  • internal/tools/exec_command_test.go

Walkthrough

Changes

Execution test stability

Layer / File(s) Summary
Temporary workspace cleanup
internal/tools/exec_command_test.go
Execution tests use execTestRoot(t) for temporary workspaces, with retrying cleanup for lingering Windows process handles.
Non-TTY stop polling
internal/tools/exec_command_test.go
The stop-intent test resends stop input and polls until the session ID is empty, using shorter yields and a longer deadline.
Windows bash syntax case
internal/tools/bash_tool_test.go
The escalated MSYS guard test uses a standalone cd command while preserving the windows_shell_syntax assertion.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: kevincodex1, jatmn, gnanam1990

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing internal/tools test failures on stock Windows.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/windows-exec-test-flakes

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: aa7ce57a398c
Changed files (2): internal/tools/bash_tool_test.go, internal/tools/exec_command_test.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

gnanam1990
gnanam1990 previously approved these changes Jul 28, 2026

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Approve.

Reviewed at 20032342e55e, base 81a5e6c, re-confirmed live before posting. Up to date with main.

Test-only, and I checked that: git diff origin/main --name-only returns nothing outside _test.go. No production behaviour changes, which sets the bar for this correctly — the question is whether it fixes the flakes without weakening what the tests assert.

It does, and the two bounds are the reason. My concern with a change of this shape is that a Windows-motivated relaxation quietly relaxes the POSIX assertions too. Neither does.

The stop retry is bounded at 30s and ends in t.Fatalf naming the input, the meta and the output. It re-sends until the session reports exited rather than sleeping a guessed duration, but a session that genuinely never stops still fails the test. That is the distinction between fixing a race and deleting an assertion, and it lands on the right side.

The temp-root cleanup is bounded at 10s and then gives up with t.Logf. That is a real relaxation, and I went looking for what it costs on POSIX. Nothing, as far as I can tell: os.RemoveAll succeeds against open handles on POSIX unlink semantics, so t.TempDir would not have caught a leaked handle there either. The retry loop exits on the first attempt on darwin and Linux, so behaviour is unchanged outside Windows. No coverage is lost.

execTestRoot as one cross-platform helper rather than a Windows-tagged variant matches what SKILL.md §2 asks for, and fixing it as a class rather than per test is the right instinct — you are correct that chasing them individually would keep producing new names, and the description says which three surfaced and why the visible one varied.

On the correction in the description. You reported this to @anandh8x on #804 as "exit is broken while Ctrl+C still works", then corrected it there once you found the failing input alternates because what matters is which stop runs first. Recording that in the PR rather than quietly shipping the fix is the useful thing to have done — it stops the wrong diagnosis propagating into #804's history.

Verification. On macOS (darwin/arm64): ./internal/tools/ passing, and passing at -count=3 on the three named tests, so the retry has not introduced flakiness on the platform where the wait was previously sufficient. gofmt -l and git diff --check clean.

Limitations, and they are the whole point of the change. I have no Windows host and no PowerShell 5.1-only box, so the failures this fixes are unreproducible for me. Every claim about PowerShell cold start, handle release and the t.TempDir sharing violation rests on your measurements and on reading. The yield 1s / 3s / 6s table is the kind of evidence that makes that acceptable, but a Windows reviewer confirming the flakes are gone would close it properly.

Process. No parent issue. You are a collaborator so this is not the community gate, and a test-only Windows fix is a reasonable thing to carry without one — but it is worth an issue if only so the PowerShell 7 assumption is recorded somewhere findable.

Merge is kevin's call per the program gate.

The retry loop added to survive a slow PowerShell teardown also made the
test unfalsifiable. The helper sleeps 5s, well inside the 30s deadline, so
a terminate that did nothing at all would let the sleep end on its own and
every existing assertion would still hold: the session reports exited, it
reports an exit code, and the interrupted flag echoes the request rather
than the outcome. Neutering KillProcessTree confirmed it, the test passed.

Assert instead that the helper's completion line never appears, which
distinguishes a killed session from a finished one without depending on
how long the loop took. Output is accumulated across every poll rather
than read off the last result, because each continuation returns only what
the session produced since the previous collection.
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

Went back over this one before asking anyone else to look at it, and the retry loop I added turned out to defeat the test it was meant to stabilise.

The helper sleeps 5s and the deadline I gave the loop is 30s. So if termination did nothing at all, the loop would just spin until the sleep ended by itself, and every assertion still passed: the session reports exited, it reports an exit code, and interrupted echoes the request rather than the outcome. I checked rather than assumed, by making KillProcessTree a no-op:

--- FAIL: TestWriteStdinStopIntentTerminatesNonTTYSession
    stop input "�" let the session run to completion instead of terminating it
    ... interrupted: true, exit_code: 0, output: long sleep finished

Before the retry loop the single 1s yield made that impossible, so this was a regression the flake fix introduced, not something pre-existing.

Fixed by asserting the helper's completion line never appears, which separates a killed session from a finished one without caring how long the loop took. Output is accumulated over every poll instead of read off the last result, since each continuation only returns what arrived since the previous collection and the marker can land in an earlier one.

Rest of the PR held up. The && removal still asserts windows_shell_syntax positively, and I traced the re-send path for the reap race: Continue only removes a process when it observes the exit, which is the same moment the loop breaks, so a re-send can never hit a reaped session.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@kevincodex1 ready to merge from my side. All nine checks green on aa7ce57a, Windows smoke included, and CodeRabbit has approved the current head.

One thing to square first, and it is my doing. @gnanam1990 approved this at 20032342 earlier today and my follow up push dismissed that approval, so the count sits at zero human approvals and GitHub is holding it. I have re-requested.

The push was worth making. Reviewing my own work before handing it over, the retry loop I had added to fix the flake turned out to make the test unfalsifiable: the helper self exits after 5s and the loop waits up to 30s, so a terminate that did nothing at all would let the sleep end on its own and still satisfy every assertion, interrupted: true included, since that flag echoes the request rather than the outcome. Confirmed by neutering KillProcessTree, at which point the old test still passed. Now it asserts the helper's completion line never appears, which separates a killed session from a finished one regardless of timing, and it fails under the same mutation.

gnanam, the delta since your approval is 18 added lines in exec_command_test.go and nothing else. No production code moved, so your reasoning about the change itself still stands.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@gnanam1990 sorry to bounce this back to you. Your approval at 20032342 was dismissed by my own follow up push, so this needs you again to unblock the merge.

To save you re-reading the whole thing, the delta is 20032342..aa7ce57a, 18 added lines in exec_command_test.go and nothing else. No production code moved.

If you want to confirm the new assertion has teeth rather than take my word for it:

# in internal/execution/process_windows.go, make the kill a no-op
func KillProcessTree(pid int) error {
	if pid > 0 { return nil }
	...
}
go test ./internal/tools/ -run TestWriteStdinStopIntentTerminatesNonTTYSession -count=1

Before the new line that fails with interrupted: true, exit_code: 0, output: long sleep finished, which is the whole problem: the retry loop waits 30s, the helper self exits after 5s, so it passed by waiting the helper out. On the current head it fails as it should.

@kevincodex1
kevincodex1 merged commit c9f9dc0 into main Jul 28, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants