Skip to content

Fix flaky integration tests with polling-based stderr wait#1746

Merged
lpcox merged 3 commits intomainfrom
fix/integration-test-stderr
Mar 10, 2026
Merged

Fix flaky integration tests with polling-based stderr wait#1746
lpcox merged 3 commits intomainfrom
fix/integration-test-stderr

Conversation

@lpcox
Copy link
Collaborator

@lpcox lpcox commented Mar 10, 2026

Problem

Integration tests TestDIFCModeFilterViaEnv and TestDIFCModePropagateViaEnv were flaky — they used a fixed 1-second time.Sleep before reading stderr, but the gateway sometimes hadn't written the expected log output yet.

Fix

Added a waitForStderr() helper that polls a concurrency-safe syncBuffer for expected content with a 5-second deadline (100ms polling interval). This eliminates both the timing race condition and the data race caused by polling a bytes.Buffer concurrently written to by the subprocess.

syncBuffer wraps bytes.Buffer with a sync.RWMutex, providing a thread-safe Write method (used by cmd.Stderr) and a locked String method (used by waitForStderr and assertions).

All waitForStderr call sites now assert on the return value with require.Truef, including the actual stderr content in the failure message to make timeout failures diagnosable.

Affected tests:

  • TestDIFCModeFilterViaEnv
  • TestDIFCModePropagateViaEnv
  • TestFullDIFCConfigFromJSON
  • TestDIFCConfigWithGuards

Verification

make agent-finished passes — all unit + integration tests green.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Replace fixed 1-second sleep with waitForStderr() helper that polls
the stderr buffer for expected output with a 5-second deadline. This
fixes race conditions where the gateway hadn't written to stderr
before the test read and killed the process.

Affected tests:
- TestDIFCModeFilterViaEnv
- TestDIFCModePropagateViaEnv
- TestFullDIFCConfigFromJSON
- TestDIFCWithFullConfig

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 10, 2026 21:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to eliminate flakiness in DIFC integration tests by replacing fixed time.Sleep waits with a polling helper that waits for expected stderr log output before proceeding.

Changes:

  • Added waitForStderr() helper to poll stderr for expected substrings with a deadline.
  • Replaced several fixed time.Sleep(1 * time.Second) calls with waitForStderr() in DIFC/guards integration tests.
Comments suppressed due to low confidence (3)

test/integration/difc_config_test.go:274

  • The return value of waitForStderr is ignored. If the substring never appears within the deadline, the test will continue and fail later with a less clear assertion. Please assert that waitForStderr returns true (and ideally include stderr output in the failure) so timeouts are explicit.
	waitForStderr(&stderr, "Guards enforcement", 5*time.Second)

test/integration/difc_config_test.go:324

  • The return value of waitForStderr is ignored. Please assert on it so the test fails immediately with a clear timeout message if the expected log output never appears.
	waitForStderr(&stderr, "Guards enforcement", 5*time.Second)

test/integration/difc_config_test.go:388

  • The return value of waitForStderr is ignored. Please assert on it (and include stderr) so a missing log line results in a clear timeout failure rather than proceeding and failing later.
	waitForStderr(&stderr, "Guards enforcement", 5*time.Second)


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +51 to +60
// waitForStderr polls buf until it contains substr or the deadline expires.
// Returns true if the substring was found within the deadline.
func waitForStderr(buf *bytes.Buffer, substr string, deadline time.Duration) bool {
end := time.Now().Add(deadline)
for time.Now().Before(end) {
if strings.Contains(buf.String(), substr) {
return true
}
time.Sleep(100 * time.Millisecond)
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

waitForStderr reads buf.String() while the gateway subprocess is concurrently writing to the same bytes.Buffer via cmd.Stderr. bytes.Buffer is not safe for concurrent read/write, so this can introduce data races or corrupted reads (even if it reduces flakiness). Consider switching stderr capture to a concurrency-safe writer (e.g., wrap bytes.Buffer with a mutex and expose a locked String(), or read from cmd.StderrPipe() in a goroutine and synchronize access) and have waitForStderr operate on that safe abstraction instead of *bytes.Buffer.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Copy link
Contributor

Choose a reason for hiding this comment

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

Done in commit 0e95d8f. Introduced a syncBuffer type that wraps bytes.Buffer with a sync.RWMutexWrite takes the write lock and String takes the read lock. waitForStderr now accepts *syncBuffer instead of *bytes.Buffer, eliminating the data race between the subprocess writing and the polling goroutine reading. All four tests using waitForStderr now declare var stderr syncBuffer for their stderr capture. Also added require.Truef assertions on the waitForStderr return value in each call site so timeout failures produce actionable messages including the actual stderr content.

Comment on lines +214 to +215
// Wait for startup — look for server name in logs
waitForStderr(&stderr, "playwright", 5*time.Second)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The PR description mentions updating TestDIFCWithFullConfig, but that test doesn’t appear in this file (and isn’t changed in this diff). Please either update the PR description to match the actual changes or include the intended test update so reviewers can validate the preventive fix claimed in the description.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Copy link
Contributor

Choose a reason for hiding this comment

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

TestDIFCWithFullConfig was a mistake in the PR description — that test doesn't exist. The actual test that was updated (preventively, with syncBuffer + waitForStderr) was TestDIFCConfigWithGuards. Updated the PR description to reflect the correct test names.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…in waitForStderr

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copy link
Contributor

Copilot AI commented Mar 10, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build2883992693/b336/launcher.test /tmp/go-build2883992693/b336/launcher.test -test.testlogfile=/tmp/go-build2883992693/b336/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2883992693/b263/vet.cfg g_.a VhJp/MqnVzrHcl8m8AG1CVhJp x_amd64/vet (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2883992693/b318/config.test /tmp/go-build2883992693/b318/config.test -test.testlogfile=/tmp/go-build2883992693/b318/testlog.txt -test.paniconexit0 -test.timeout=10m0s 64/s�� p/go-build (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build2883992693/b336/launcher.test /tmp/go-build2883992693/b336/launcher.test -test.testlogfile=/tmp/go-build2883992693/b336/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2883992693/b263/vet.cfg g_.a VhJp/MqnVzrHcl8m8AG1CVhJp x_amd64/vet (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build2883992693/b336/launcher.test /tmp/go-build2883992693/b336/launcher.test -test.testlogfile=/tmp/go-build2883992693/b336/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2883992693/b263/vet.cfg g_.a VhJp/MqnVzrHcl8m8AG1CVhJp x_amd64/vet (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2883992693/b345/mcp.test /tmp/go-build2883992693/b345/mcp.test -test.testlogfile=/tmp/go-build2883992693/b345/testlog.txt -test.paniconexit0 -test.timeout=10m0s go_.�� 64/src/net QT x_amd64/vet -p internal/byteord-o -lang=go1.25 x_amd64/vet -o /server/auth.go /server/handlersgithub.com/github/gh-aw-mcpg/internal/logger x_amd64/vet -p 9239489/b151/ -lang=go1.25 x_amd64/vet (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@lpcox lpcox merged commit dbba938 into main Mar 10, 2026
12 checks passed
@lpcox lpcox deleted the fix/integration-test-stderr branch March 10, 2026 21:50
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