Skip to content

fix(config): stop billing provider-command fixtures to the 5s timeout - #810

Merged
kevincodex1 merged 1 commit into
mainfrom
fix/config-provider-command-flakes
Jul 27, 2026
Merged

fix(config): stop billing provider-command fixtures to the 5s timeout#810
kevincodex1 merged 1 commit into
mainfrom
fix/config-provider-command-flakes

Conversation

@Vasanthdev2004

@Vasanthdev2004 Vasanthdev2004 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Both background-child tests fail intermittently on the Windows smoke job, and have since #690. #800 and #802 each relaxed an assertion; neither held, because the deadline itself was the problem.

Why they flake

The tests drove LoadProviderCommand, so the fixture's own process startup was billed to the production 5s budget. On a loaded runner the outer timer wins the select in runProviderCommand, and both symptoms fall out of that:

  • ...OnFailure sees provider command timed out after 5s instead of the exit status it asserts, because the timer preempts the shell's exit /b 7.
  • ...TerminatesBackgroundChild loses its PID file, because the timer's Terminate kills the background child before it records a PID. test(config): wait for background PID file #802 added a 3s poll for that file, but the writer is already dead, so no amount of polling helps.

Reproduced locally by looping both under CPU contention, including the exact CI messages:

command_test.go:120: read sleeper pid file before timeout: ... bg.pid: The system cannot find the file specified.
command_test.go:146: error = "provider command timed out after 5s", want exit status failure

What this does

Neither test is a claim about the 5s budget; they assert which error each cmd.Wait path yields and that the leftover descendant is killed. They now call runProviderCommand directly with a budget of their own, so the production deadline stops competing with process startup. Making the child outlive that budget lets both drop their elapsed-versus-lifetime bounds too, so no wall-clock assertion is left in either test.

That is only worth doing if the two timeout paths can be told apart, and they could not: the WaitDelay path and the outer-timer path both returned the same bare sentinel, so asserting on it would have been satisfied by the very timeout these tests exist to rule out. Hence the one product line here, wrapping instead of replacing on the WaitDelay path. Callers asking errors.Is for the sentinel are unaffected, runProviderCommand has exactly one non-test caller, and no user-visible message changes.

The Windows fixture also waited for its ready marker by launching a second PowerShell. That cold start is pure overhead added after the PID is written, so the wait now runs in cmd itself and is bounded.

Checks

gofmt, go vet, go build ./... clean.

Under the CPU contention that reproduced the failures: 7 failures in 16 iterations before, 0 in 32 after, with per-iteration time going from a spread of 2.3s to 66s down to a steady 2.1s.

The new ErrWaitDelay assertion is load-bearing rather than decorative: dropping the wrap makes it fail.

--- FAIL: TestLoadProviderCommandTerminatesBackgroundChild
    error = provider command timeout, want errProviderCommandTimeout wrapping exec.ErrWaitDelay

Separately: the timeout is not actually a bound

Not fixed here, and I will open an issue rather than widen this PR. While reproducing the above I saw LoadProviderCommand take 19.7s and later 106s against a 5s timeout. Two causes, both in command.go: startCommandProcess runs before the timer is armed, so process creation is unbounded, and <-done after Terminate() on the timer path is unbounded too. The magnitudes came from synthetic load, but the ordering is wrong regardless of load. A user whose provider command is slow on a cold or AV-heavy Windows box can block startup well past the 5s the error text promises, and gets no diagnostics, since the timeout path discards stderr.

Summary by CodeRabbit

  • Bug Fixes

    • Improved timeout error reporting for provider commands while preserving compatibility with timeout detection.
    • Provider command failures now retain their underlying exit details for more accurate diagnostics.
  • Tests

    • Reduced flakiness in command termination and background-process handling tests.
    • Added bounded waiting to make process-related test scenarios more reliable.

Both background-child tests fail intermittently on the Windows runner, and
have since #690. #800 and #802 each relaxed an assertion, which is why neither
held: the deadline itself was the problem.

The tests drove LoadProviderCommand, so the fixture's own startup was billed to
the production 5s budget. On a loaded runner the outer timer won the select in
runProviderCommand, and both symptoms follow from that. The nonzero-exit test
saw a timeout instead of the exit status it asserts, because the timer
preempted the shell's exit. The other test lost its PID file, because the
timer's Terminate killed the background child before it could record a PID,
putting the file permanently out of reach however long the assertion polled.

Neither test is a claim about the 5s budget. They assert which error each
cmd.Wait path yields and that the leftover descendant is killed, so they now
call runProviderCommand directly with a budget of their own and the production
deadline stops competing with process startup. Making the child outlive that
budget lets both drop their elapsed-versus-lifetime bounds as well, so no
wall-clock assertion is left in either test.

That is only worth doing if the two timeout paths can be told apart, and they
could not: the WaitDelay path and the outer-timer path both returned the same
bare sentinel, so asserting on it would have been satisfied by the very timeout
the tests exist to rule out. The WaitDelay path now wraps rather than replaces.
Callers asking errors.Is for the sentinel are unaffected, so no user-visible
message changes.

The Windows fixture also waited for its ready marker by launching a second
PowerShell. That cold start is pure overhead added after the PID is already
written, so the wait now runs in cmd itself and is bounded.

Verified by looping both tests under CPU contention: 7 failures in 16
iterations before, none in 32 after, with per-iteration time going from a
spread of 2.3s to 66s down to a steady 2.1s. Removing the wrap makes the
WaitDelay assertion fail, so it is load-bearing rather than decorative.
@github-actions

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: 780d47f31bcc
Changed files (2): internal/config/command.go, internal/config/command_test.go

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

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Provider command timeout handling now wraps exec.ErrWaitDelay while preserving the timeout sentinel and captured output. Tests use bounded execution budgets, structured error assertions, and bounded Windows polling for background-process termination scenarios.

Changes

Provider command timeout handling

Layer / File(s) Summary
Timeout error wrapping
internal/config/command.go
runProviderCommand wraps wait-delay failures with errProviderCommandTimeout and preserves stdout/stderr.
Termination test coverage
internal/config/command_test.go
Background-child tests use shared time budgets, inspect timeout and exit errors structurally, and use bounded cmd polling for Windows PID readiness.

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

Possibly related PRs

  • Gitlawb/zero#690: Updates the same provider command timeout handling and related assertions.
  • Gitlawb/zero#800: Addresses provider-command background-child timing and Windows readiness handling.
  • Gitlawb/zero#802: Changes background-child PID polling and termination test behavior.

Suggested reviewers: gnanam1990, kevincodex1, jatmn

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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 matches the main change: provider-command fixtures were moved off the 5s production timeout to reduce test flakiness.
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/config-provider-command-flakes

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/config/command_test.go (1)

95-143: 🩺 Stability & Availability | 🔵 Trivial

Sound fix for the startup-vs-timeout coupling.

Switching to runProviderCommand(command, providerCommandTestBudget) with a budget decoupled from providerCommandTimeout correctly stops fixture cold-start from competing with the production 5s deadline, and asserting errors.Is(err, errProviderCommandTimeout) && errors.Is(err, exec.ErrWaitDelay) (line 139) precisely targets the WaitDelay path rather than the outer-timer path, matching the wrapping added in command.go.

Since this test directly exercises the done/timer select and the cmd.Wait() goroutine in runProviderCommand, worth running it (and its sibling below) with -race given they're new coverage over that concurrent path.

As per coding guidelines, **/*_test.go: "add regression tests for behavior changes, and run affected concurrent code under the race detector."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/command_test.go` around lines 95 - 143, Run
TestLoadProviderCommandTerminatesBackgroundChild and its sibling concurrent
command test with the race detector, using the test-specific
providerCommandTestBudget path. Fix any race reports in runProviderCommand or
related test helpers while preserving the assertions for WaitDelay error
wrapping and background-child termination.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/config/command_test.go`:
- Around line 95-143: Run TestLoadProviderCommandTerminatesBackgroundChild and
its sibling concurrent command test with the race detector, using the
test-specific providerCommandTestBudget path. Fix any race reports in
runProviderCommand or related test helpers while preserving the assertions for
WaitDelay error wrapping and background-child termination.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 138e26aa-afc9-4885-aa94-426aa7427f63

📥 Commits

Reviewing files that changed from the base of the PR and between ac50a5a and 780d47f.

📒 Files selected for processing (2)
  • internal/config/command.go
  • internal/config/command_test.go

@kevincodex1
kevincodex1 merged commit f5fa4ff into main Jul 27, 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.

2 participants