Make e2e workload readiness waits reliable - #6012
Merged
Merged
Conversation
The group specs time out waiting for a freshly started workload to reach the running state on loaded CI runners. Reaching running covers the container start plus ToolHive's own initialize probe against the proxy, which the product waits up to 5 minutes for, so a 60s spec-side budget fails while the workload is still legitimately starting. WaitForMCPServer also matched the name and the word running anywhere in the list table, so it could return before the named workload was running, and its timeout carried no diagnostics. Separately, CI pre-pulled hardcoded image tags for servers the tests reach by registry name. Renovate bumps toolhive-catalog daily but has no manager for those docker pull lines, so fetch (1.0.2 vs 1.0.5) and time (2026.1.26 vs 2026.7.10) had drifted and the pre-pull no longer covered what the tests run. Resolve those tags from the binary under test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run() applies TestConfig.TestTimeout (10 minutes), so a single hung list could outlast the readiness budget it is polling for. Callers poll, so an overrunning list is retried rather than fatal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
waitForIsolatedMCPServer was a copy of WaitForMCPServer's old body and carried the same defect: it matched the name and the word running anywhere in the list table. Export FindWorkload, which takes the command builder so an isolated config/home/data spec can supply its own, and have both waits read the named workload's own record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6012 +/- ##
==========================================
+ Coverage 72.11% 72.14% +0.03%
==========================================
Files 720 720
Lines 74745 74745
==========================================
+ Hits 53899 53923 +24
+ Misses 16993 16952 -41
- Partials 3853 3870 +17 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
marked this pull request as ready for review
July 27, 2026 12:59
JAORMX
requested review from
ChrisJBurns,
amirejaz,
aponcedeleonch,
jhrozek,
rdimitrov and
reyortiz3
as code owners
July 27, 2026 12:59
jhrozek
approved these changes
Jul 27, 2026
6 tasks
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
Group Removespec times out waiting for a freshly started workload to reachrunning. Three separate problems, all in test infrastructure:The spec-side budget is far tighter than the product's own. Reaching
runningis not just "container started": the detached worker starts the container, brings up the transparent proxy, then polls the workload's own MCP endpoint untilinitializesucceeds, and only then flips the status (pkg/runner.waitForInitializeSuccess,pkg/runner/runner.go:572) — a probe the product is willing to wait 5 minutes for. The group specs gave it 60s while starting up to 4 workloads at once, so they fail while the workload is still legitimately starting on a loaded runner. Waits now take their budget frome2e.ServerReadyTimeout()(2m default, overridable viaTOOLHIVE_E2E_SERVER_READY_TIMEOUT).The readiness wait was not actually checking the named workload. It matched
strings.Contains(stdout, serverName) && strings.Contains(stdout, "running")against the wholethv listtable, i.e. "this name appears somewhere and some workload is running" — so it could return while the named workload was stillstarting. It now reads that workload's own record fromthv list --all --format json. The same defect was present in three places, all fixed:WaitForMCPServer,IsServerRunning, andwaitForIsolatedMCPServer(a copy of the old body in the upgrade spec). The lookup is now shared via an exportede2e.FindWorkload, which takes the command builder so the isolated config/home/data spec can supply its own and observe its own state.The failure was undiagnosable.
timeout waiting for MCP server X to be runningsaid nothing about why. The error now carries the last status observed (includingstatus_context) and dumps server state via the existingDebugServerStatehelper, so the next occurrence tells us whether the workload was still starting, had errored, or never appeared. The poll's ownthv listis bounded at 30s, sinceRun()would applyTestConfig.TestTimeout(10m) and let one hung list outlast the wait it is being polled for.CI's pre-pull had silently gone stale. The step exists so workload startup does not pay the image-pull cost, but it hardcoded tags for images the tests reach by registry name. Renovate bumps
github.com/stacklok/toolhive-catalogdaily and has no manager for thesedocker pulllines, so they drifted:fetchgofetch/server:1.0.5gofetch/server:1.0.2❌timemcp-server-time:2026.7.10mcp-server-time:2026.1.26❌osvosv-mcp/server:0.1.3osv-mcp/server:0.1.3✅Those tags are now resolved from the binary under test (
thv registry info <server> --format json), so they cannot drift again, and the step fails loudly if an image did not land locally (waitreports success even when a background pull failed).Fixes #6011
Type of change
Test plan
task test)task test-e2e)task lint-fix)Docker is unavailable in my environment, so the e2e suite itself is verified by this PR's CI — that is the point of opening it as a draft. Verified locally:
task lint→ 0 issues;go vet ./test/e2e/...clean.fetch→ghcr.io/stackloklabs/gofetch/server:1.0.5,time→ghcr.io/stacklok/dockyard/uvx/mcp-server-time:2026.7.10.core,proxy && !isolationandnetwork-isolationmatrix entries against a stubbeddocker, confirming the resolved tags and per-matrix image sets.thv registry info --format jsonemits clean JSON on stdout (the update-notification banner goes to stderr, and is skipped in CI anyway), so both thejqcall and the JSON parse inFindWorkloadare safe.Changes
test/e2e/helpers.goServerReadyTimeout(); exportedFindWorkload;WaitForMCPServer/IsServerRunningread the workload's own record; timeout diagnostics; bounded list poll.github/workflows/e2e-tests.ymltest/e2e/upgrade_e2e_test.gowaitForIsolatedMCPServeruses the shared lookup instead of a copy of the old loose matchtest/e2e/rm_group_test.go,group_rm_test.go,group_test.go,list_group_e2e_test.goDoes this introduce a user-facing change?
No.
Special notes for reviewers
coreentry. Tightening the wait to the named workload makes ~50 call sites wait for something they previously could skip. That is the correct contract, but any spec that was passing only because another workload happened to be running will now surface. If something unrelated goes red, that is the signal — I'd rather find it here than leave the helper accepting the wrong thing.errorstatus, deliberately. It is tempting to abort the wait as soon as the workload reportserror, but the status is not monotonic across restarts (thv restartwritesstopped/startingafter a previouserror), so a spec that restarts a workload could read a staleerrorand fail spuriously. The status is reported in the timeout message instead, which never fails earlier than the old code did.upgrade_e2e_test.gokeeps its two explicit120*time.Secondwaits: that equals the new default anyway, and the author raised them above 60s deliberately for the slower upgrade flow, so leaving them hardcoded preserves that intent against an env override. Only its 60s waits moved to the shared budget. Remaining 60s call sites elsewhere are untouched to keep this reviewable;ServerReadyTimeout()is there to adopt if the same family shows up again (E2E tests are intermittently flaky on CI #5690, Flaky api-workloads E2E suite: 15m TEST_TIMEOUT too tight for the workload-heavy matrix entry #5265, e2e vmcp tests are flaky #2741).thv run(cmd/thv/app/run_flags.go:417), which the specs allow 10 minutes for. It burns suite wall-clock and pushes the workload-heavy matrix entries towardTEST_TIMEOUT, which is the Flaky api-workloads E2E suite: 15m TEST_TIMEOUT too tight for the workload-heavy matrix entry #5265 family, so it is fixed here rather than filed separately.Generated with Claude Code