Skip to content

test(remoteagent): name the awaited event instead of panicking on a closed channel - #1400

Open
harshitwandhare wants to merge 3 commits into
google:mainfrom
harshitwandhare:test/a2a-cleanup-await-value
Open

test(remoteagent): name the awaited event instead of panicking on a closed channel#1400
harshitwandhare wants to merge 3 commits into
google:mainfrom
harshitwandhare:test/a2a-cleanup-await-value

Conversation

@harshitwandhare

@harshitwandhare harshitwandhare commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Link to Issue or Description of Change

Not Closes: this does not fix that flake. See the end of the Solution section.

Problem:

TestA2ACleanupPropagation and TestCompat_A2ACleanupPropagation take the first status update with a bare receive:

taskID := (<-statusUpdateEventChan).TaskInfo().TaskID

The goroutine feeding that channel does defer close(statusUpdateEventChan) and returns early when SendStreamingMessage fails, after recording the error with t.Errorf. The receive on the closed channel then yields the zero value of a2a.Event, which is a nil interface, and TaskInfo() dereferences it.

So a streaming error surfaces as a nil-pointer panic that kills the test binary and buries the error that was just recorded. Forcing that error path on main shows both halves:

--- FAIL: TestA2ACleanupPropagation (0.00s)
    a2a_e2e_test.go:536: client.SendStreamingMessage() error = forced
panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]
[signal 0xc0000005 code=0x0 addr=0x28 pc=0x7ff76984d967]
...
google.golang.org/adk/v2/agent/remoteagent/v2.TestA2ACleanupPropagation(0x2231ac88a600)
	agent/remoteagent/v2/a2a_e2e_test.go:555 +0x6a7

Two smaller things in the same two tests:

remoteTaskID := <-remoteTaskIDChan in the v2 variant is unguarded, so if server B is never reached the test hangs there with nothing to say. The compat variant already guards the same receive with its own select and a 1s bound, so this is an asymmetry rather than a decision.

The call-site comments describe the deadline as "per-wait". AwaitN arms one timer before the loop, so it is a budget for all n receives together.

Solution:

Add testutil.AwaitValue, the one-value counterpart to AwaitN. A closed channel is a failure there rather than a receive, because the caller uses the value, and the failure names which value never arrived.

Use it for the three ad-hoc receives across the two tests. That brings the v2 variant in line with compat, and replaces compat's ad-hoc 1s bound with the shared deadline. The 1s is reachable on a loaded machine, which is the same class of problem #1298 is about; by that point in the test remoteCleanupCalledChan has already delivered both values, so server B has been reached and the ID is already buffered.

Same forced error path with this change:

--- FAIL: TestA2ACleanupPropagation (0.00s)
    a2a_e2e_test.go:536: client.SendStreamingMessage() error = forced
    a2a_e2e_test.go:555: first status update: channel closed before a value arrived

The recorded error survives and the wait is named.

This does not fix the flake in #1298. I could not reproduce that on this machine, and I said so on the issue. What it changes is the failure report: a wait that does not complete names the event it was waiting for instead of panicking or hanging anonymously, which is step 1 of what @baptmont asked for there. The AwaitN deadline knob from step 2 is a separate change and is not in this PR.

Testing Plan

Windows, Go 1.26.6, -race via mingw-w64 GCC 16.2.0.

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Targeted:

$ go test -race ./agent/remoteagent/... -count=1
ok  	google.golang.org/adk/v2/agent/remoteagent	1.487s
ok  	google.golang.org/adk/v2/agent/remoteagent/v2	2.320s

Full module:

$ go test -race -mod=readonly -count=1 -shuffle=on work
71 ok, 2 FAIL      # current, merged with main at #1394
70 ok, 3 FAIL      # earlier, merged with main at #1396
69 ok, 4 FAIL      # first run, before either merge

None of the failures is caused by this change, and the set has been shrinking as unrelated
fixes land. internal dropped out because #1394 merged.

The two that remain are both Windows-only and I have now confirmed that rather than assumed
it: on Linux, go test -race -shuffle=on -count=25 under CPU load passes both.

  • internal/configurable/conformance/replayplugin: filepath.Rel cannot relate a C: temp
    dir to a clone on D:. Needs the repo and TMPDIR on different volumes, so it is closer
    to a local-setup edge than a repo bug.
  • internal/telemetry/functionaltest: TestTelemetrySchema_Workflow span ordering.

A third, internal/llminternal
TestRunLiveNoGoroutineLeak/sender_error_after_connection_loss_does_not_leak, showed up in
one full run and not the two since, with no change to that package, and passes -count=3 in
isolation. Shuffle-order dependent rather than related to anything here.

I attributed these by re-running the same command on a clean main with the change stashed
and getting the identical set.

Other gates:

$ go build -mod=readonly work                          # clean
$ go vet ./agent/remoteagent/... ./internal/testutil/  # clean
$ gofmt -l <the three files>                           # empty
$ go mod tidy -diff                                    # empty
$ golangci-lint run
0 issues.

The "added or updated unit tests" box is ticked on the reading that this PR is a test change. No new test function asserts on AwaitValue itself: the behaviour it fixes is a failure-reporting path, so asserting on it means asserting that a t.Fatalf fired, which needs a fake testing.T. The before/after output above is the evidence instead. Happy to add a testutil test with a stub if you would rather have one.

Manual End-to-End (E2E) Tests:

Not applicable, this is test-only and makes no production change. The before/after outputs above were produced by temporarily forcing the SendStreamingMessage error path in the v2 test, running it, and reverting; that edit is not part of the diff.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

AwaitValue returning the zero value after t.Fatalf is unreachable in practice, since Fatalf calls runtime.Goexit, but the compiler needs the return.

…losed channel

The A2A cleanup-propagation tests took the first status update with a bare
receive:

	taskID := (<-statusUpdateEventChan).TaskInfo().TaskID

The producing goroutine defers close on that channel and returns early when
SendStreamingMessage fails, after recording the error with t.Errorf. The
receive then yields the zero value of a2a.Event, which is a nil interface, and
TaskInfo dereferences it. A streaming error therefore surfaced as a nil-pointer
panic that killed the test binary and buried the error that had just been
recorded.

Add testutil.AwaitValue, the one-value counterpart to AwaitN. It treats a
closed channel as a failure rather than a receive, since callers use the value,
and it reports which value never arrived.

Use it for the three ad-hoc receives in these two tests. That also brings the v2
variant in line with the compat one, which already guarded remoteTaskIDChan with
its own select, and replaces that ad-hoc 1s bound with the shared deadline so a
loaded machine cannot trip it.

Also correct the call-site comments and document the deadline: AwaitN arms one
timer for all n receives, so it is a budget for the whole call, not per wait as
the comments claimed.

This does not fix the flake in google#1298. It makes a failure name the event it was
waiting for instead of panicking or hanging anonymously, which is step 1 of what
that issue asks for.

Part of google#1298.
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.

1 participant