Repository navigation
fix: wait for full completion before asserting in tests - #30885
eric-hughes-coder wants to merge 17 commits into
Conversation
Catalogs the flake class behind PLAT-609: a test waits on one value, then asserts another that is updated separately. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds a TESTING.md section stating that a wait covers only the condition it polls, with Prometheus metrics as the leading case, and points the failure catalog entry at it. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The package comment states that values from separate helper calls need not agree and refers to the TESTING.md section on waiting for one value and asserting another. MetricValue and its wrappers point back to it. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestServer_Metrics/Forwarding waited for batches_dropped_total and then asserted logs_dropped_total, which the forwarder updates separately afterward. The poll now waits for both, and both are asserted. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Phase 3 of TestServer_Metrics/Forwarding waited for the boundary_channel_full series and then asserted boundary_batch_full, which the reader goroutine updates separately afterward. The poll now waits for both. The TESTING.md citation moves to the comment on the Forwarding subtest, since it applies to every phase. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Phase 2 of TestServer_Metrics/Forwarding waited for the fake reporter to record the logs and then asserted batches_forwarded_total, which the forwarder increments only after ReportBoundaryLogs returns. The poll now waits for both, and both are asserted. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestServer_Metrics/BufferFull waited for batches_dropped_total and then asserted logs_dropped_total, which bufferLogs updates separately afterward. The poll now waits for both, and both are asserted exactly. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestMetrics_TokenUseCount waited for the input series and then asserted the others, which are recorded separately and, for Anthropic streaming, across two records. It passed only because the handler waits for its recorders before the response ends. The poll now waits for every series to reach its expected value, so the test no longer depends on that ordering. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four metadata batcher tests waited for BatchesTotal and then asserted MetadataTotal. They passed only because the flush writes MetadataTotal first, and in TestMetadataBatcher because the capacity timer is reset after the flush. The polls now wait for both metrics, and both are asserted. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Renames the TESTING.md section to "Wait for Full Completion before Asserting" and states the general rule, with waiting on one value and asserting another as one case and asserting that a value is unchanged as another. Updates every citation of the old heading: the failure catalog link, the promhelp package comment, and the test comments. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestAgentStatsSessionCounts waited for the execution histogram and then asserted the session counts, which AgentStats commits separately before recording the histogram. The first phase now waits for the counts themselves. counts takes a require.TestingT so it can run inside EventuallyWithT. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The phases of TestAgentStatsSessionCounts that assert unchanged session counts can only wait on the execution histogram. The comment at that wait now states that they rely on AgentStats recording it after committing the gauges. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The last phase of TestAgentStatsSessionCounts waited for the execution histogram and then asserted the updated session counts. It now waits for the counts themselves, as the first phase does. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestMetadataBatcher_DropsWhenFull waited for the update channel to empty and then asserted the flush metrics. The channel empties when the run loop receives the last entry, before the final flush runs. The test now waits for the flush metrics to reach their final values. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each comment already states why its wait covers what it asserts, and agents load TESTING.md every session. The citation stays at the one wait that deliberately relies on a value other than the one asserted, in TestAgentStatsSessionCounts, and in the promhelp package comment. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nickvigilante
left a comment
There was a problem hiding this comment.
I did a manual verification of the Markdown files in this PR along with an agent review of everything, and it LGTM. I attached some of the feedback here from my agent.
| @@ -655,9 +662,13 @@ func TestMetadataBatcher_Deduplication(t *testing.T) { | |||
| clock.Advance(defaultMetadataFlushInterval).MustWait(ctx) | |||
|
|
|||
| // Verify flush occurred with correct number of entries | |||
There was a problem hiding this comment.
Nit: the existing comment has no period and runs straight into the new one, so the two read as one sentence. Same pattern on lines 763 and 1017.
| // Verify flush occurred with correct number of entries | |
| // Verify flush occurred with correct number of entries. | |
| // |
| testutil.Eventually(ctx, t, func(ctx context.Context) bool { | ||
| return float64(1) == prom_testutil.ToFloat64(b.Metrics.BatchesTotal.WithLabelValues(flushCapacity)) | ||
| return prom_testutil.ToFloat64(b.Metrics.BatchesTotal.WithLabelValues(flushCapacity)) > 0 && | ||
| prom_testutil.ToFloat64(b.Metrics.MetadataTotal) == 507 |
There was a problem hiding this comment.
Nit: 507 now appears in both the poll and the require.Equal on line 405, and the poll already waits for that exact value, so line 405 can't fail. A named constant (or a short note that it's the earlier flushes' 7 plus defaultMetadataBatchSize) would make the number easier to maintain if the earlier flushes change.
There was a problem hiding this comment.
The test on 405 is indeed redundant. Reworked.
Caught that on another test. Didn't on this one.
| @@ -750,9 +761,12 @@ func TestMetadataBatcher_TimestampOrdering(t *testing.T) { | |||
| psCap.requireContainsAll([]uuid.UUID{agent}) | |||
|
|
|||
| // Verify only 1 entry was flushed (newest timestamp wins) | |||
There was a problem hiding this comment.
Same as above.
| // Verify only 1 entry was flushed (newest timestamp wins) | |
| // Verify only 1 entry was flushed (newest timestamp wins). | |
| // |
| // intermittently. See "Wait for Full Completion before Asserting" in | ||
| // .claude/docs/TESTING.md. |
There was a problem hiding this comment.
Optional: the explanation in lines 4-7 already stands on its own. Pointing a Go package doc at .claude/docs/TESTING.md is unusual, since that path means nothing on pkg.go.dev or in an IDE hover. Fine to keep for a test-only package, but you could also drop the pointer and leave the cross-reference to the AGENT_FAILURES.md entry, which already links back to this package.
There was a problem hiding this comment.
I'm leaving this as-is. It's a compromise.
What's really missing is a human-audience version of the potential testing hazard. I picked the citation as a substitute. The downside of writing a longer version is dual maintenance, because specific guidance for agents needs to be short.
| @@ -1001,8 +1015,11 @@ func TestMetadataBatcher_ConcurrentAddsToSameAgent(t *testing.T) { | |||
| psCap.requireContainsAll([]uuid.UUID{agentID}) | |||
|
|
|||
| // Verify exactly 3 unique keys were flushed | |||
There was a problem hiding this comment.
Same as above.
| // Verify exactly 3 unique keys were flushed | |
| // Verify exactly 3 unique keys were flushed. | |
| // |
| t.Fatal(ctx.Err()) | ||
| } | ||
| polls++ | ||
| // The histogram sample is the only signal that the collector |
There was a problem hiding this comment.
Not blocking, and this comment already names the dependency well. One thing worth spelling out about the failure mode: if AgentStats ever moved timer.ObserveDuration() ahead of the Commit() calls (prometheusmetrics.go around lines 660-669), phases 1 and 4 would still be correct because of the new EventuallyWithT. Phases 2 and 3 ("must retain the last snapshot") would keep passing too, but without proving anything, because they would read the counts before the cycle finished. A reader may assume a reorder makes the test fail loudly. It doesn't for those two phases, so it might be worth saying so here.
There was a problem hiding this comment.
Added a sentence to that effect.
| counts := func(t require.TestingT) map[string]float64 { | ||
| metrics, err := registry.Gather() |
There was a problem hiding this comment.
Nit: switching the parameter to require.TestingT dropped t.Helper(), so failures from the require calls in here now report this closure's line instead of the caller's. *testing.T implements Helper() and *assert.CollectT does not, so a type assertion keeps both working:
| counts := func(t require.TestingT) map[string]float64 { | |
| metrics, err := registry.Gather() | |
| counts := func(t require.TestingT) map[string]float64 { | |
| if h, ok := t.(interface{ Helper() }); ok { | |
| h.Helper() | |
| } | |
| metrics, err := registry.Gather() |
Ends each existing "Verify" comment with a period and joins it with the new explanation. Folds the TestMetadataBatcher flush 4 poll and its duplicate assertions into one EventuallyWithT, writing 507 as the 7 earlier entries plus defaultMetadataBatchSize. Notes that the unchanged count phases of TestAgentStatsSessionCounts would pass vacuously if the histogram moved before the commits, and restores t.Helper in counts. Refs PLAT-609 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes the
TestServer_Metrics/Forwardingflake reported in PLAT-609 and, in addition, other defects similar to it.Testing defects
The Original
TestServer_Metrics/Forwardingwaited forbatches_dropped_totaland then asserted onlogs_dropped_total. The forwarder updates those two counters separately, on adjacent lines, so a read between them sees the first updated and the second not. CI caught exactly that window: expected 2, actual 0 atproxy_test.go:780. Each counter is atomic on its own, so the race detector reports nothing.The Additional
The fallacy of the original is a de facto assumption that sequential updates are atomic as a group. The defect arose from not taking into account that they are atomic individually. The test asserted before the work was completed. A repo-wide sweep for related metrics written together found similar mistakes in other tests:
TestServer_Metrics/ForwardingandTestServer_Metrics/BufferFullinagent/boundarylogproxy, andTestMetadataBatcher_DropsWhenFull, which waited for the update channel to empty although the final flush runs after that.TestMetrics_TokenUseCountinaibridge, four tests incoderd/agentapi/metadatabatcher, andTestAgentStatsSessionCountsincoderd/prometheusmetrics. These pass today but would start failing if the code under test reordered its work.Fixes
All tests the sweep found were fixed. Each test now waits for every value it asserts before asserting it. Where a value is already non-zero from earlier work, or legitimately zero, the wait is for the final value rather than for a change, and several tests use
require.EventuallyWithTso the wait and the assertion are one step. Two Forwarding phases gain assertions on values they already waited for:batches_dropped_total == 1in phase 1, and the reporter's recorded request count in phase 2.Two phases of
TestAgentStatsSessionCountsassert that the counts did not change. No wait on an unchanged value can show that the collection cycle ran, so they keep waiting on the execution histogram, and a comment at that wait names the dependency.Each fix was checked by temporarily blocking or reordering the code under test between the relevant writes: the old test failed on every run and the new one passed.
Documentation
As a guard against recurrence of defects of this kind, we add the following documentation targeted mostly at agents. The material in
TESTING.mdprovides guidance about how to write such a test correctly in the first place. That inAGENT_FAILURES.mddescribes how to recognize the symptoms of this class of test failure, should they arise..claude/docs/TESTING.md: new section "Wait for Full Completion before Asserting", covering waits on one value with assertions on another, Prometheus metrics in particular, and assertions that nothing changed..claude/docs/AGENT_FAILURES.md: a catalog entry for the symptom.coderd/coderdtest/promhelp: doc comments noting that each helper call is a separate gather.Generated with Claude Code under the direction of @eric-hughes-coder.