Skip to content

ci: add an advisory go test -shuffle=on lane so order-coupled tests fail in CI - #1230

Merged
Dumbris merged 3 commits into
mainfrom
ci/shuffle-lane
Sep 9, 2026
Merged

ci: add an advisory go test -shuffle=on lane so order-coupled tests fail in CI#1230
Dumbris merged 3 commits into
mainfrom
ci/shuffle-lane

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 9, 2026

Copy link
Copy Markdown
Member

Motivation

#1222 (@loloDawit) fixed two internal/server fixtures that flipped the process-global registry SSRF allow-policy and never restored it. TestBuildRegistrySourceEntry_RejectsSSRFLiteralIP — the CWE-918 regression test — only passed because its file sorted ahead of both fixtures. The PR closed with:

-shuffle=on in a CI lane. It would prevent this class from recurring, but it may surface unrelated order dependencies elsewhere in the tree, which should not be resolved as part of this fix. Happy to open it separately.

This is that lane. Nothing under .github/workflows/ ran with -shuffle before, so no CI job could ever see an order other than file/declaration order.

What the lane runs

A new job Unit Tests (shuffle) in unit-tests.yml, ubuntu-latest only:

go test -v -race -shuffle=on -skip "E2E|Binary|MCPProtocol|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint" ./...

That is the required ubuntu lane's invocation with one flag added (-shuffle=on) and one dropped (-coverprofile — Codecov already gets the ordered run). It reuses the same provisioning the required job needs for go test ./...: Go 1.25 with module cache, the pinned TSCG Node shim (npm ci --prefix bench/tscg, Spec 083 FR-006 hard-fails without it), the frontend-dist artifact copied into web/frontend/ for the embed, and the warmtiktoken pre-step that removes the tiktoken download rename race. The required Unit Tests job is unchanged.

Why advisory first

The job is deliberately not in the branch-protection required list (Unit Tests (ubuntu-latest, 1.25), Lint, Build (ubuntu-latest), … — gh api .../branches/main/protection/required_status_checks), so a newly exposed order coupling in an unrelated package cannot block a PR while the tail of such tests is being found. It has no continue-on-error: a failure shows as a red check on the PR so it gets looked at, it just cannot block the merge.

Each CI run draws a fresh seed, so exposure accumulates across runs rather than re-testing one order forever.

Proposal: promote it to required after 10 consecutive green runs on main.

Reproducing a failure from the log

With -shuffle on, every test binary prints its seed as the first line of the package's output:

-test.shuffle 1788926312892506000
--- FAIL: TestScanForPII_V7FieldViolations (0.00s)

Re-run the failing package with that exact seed:

go test -race -count=1 -shuffle=1788926312892506000 ./internal/telemetry/

-count=1 matters — the seed reproduces the order, not the cache, and a cached ok would hide the failure. Add the -skip regex from the workflow if the failing test lives next to E2E tests. The workflow carries this recipe as a comment above the job.

Local run

go test -race -shuffle=on -count=1 -skip 'E2E|Binary|MCPProtocol|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint' ./internal/...

on this branch's base (a742ef75d): 64 packages ok, 1 FAILinternal/telemetry, seed 1788926312892506000:

--- FAIL: TestScanForPII_V7FieldViolations/previous_shutdown_outside_enum (0.00s)
    anonymity_test.go:265: expected rule=v7_field_invalid, got "blocked_value"
    anonymity_test.go:268: expected pattern="previous_shutdown", got "user"

Same class as #1222, different global. Service.Start calls PopulateBlockedValues(), a sync.Once that appends the real hostname and home-dir basename to the package-global BlockedValues and is never undone. Five test files call Start. Every other ScanForPII test in anonymity_test.go snapshots BlockedValues and restores it; TestScanForPII_V7FieldViolations was the one that did not, so when a Start test runs first, a home basename such as user trips rule 2 (blocked_value) on the "terminated by user" payload before rule 7 can report v7_field_invalid.

Fixed here because it is trivially the #1222 shape — three lines of save/nil/restore, identical to the neighbouring tests in the same file. Verified: the reproducing seed fails on the base commit and passes with the fix; ordered run and seeds 1, 2, 3 all pass; golangci-lint v2 with .github/.golangci.yml reports 0 issues on the package.

Note this one is environment-dependent: it bites only when the runner's home basename or hostname is a substring of a test payload, so CI (/home/runner) may never have hit it, and a laptop with a different login may not either. The seed reproduces it on a machine where basename $HOME is user.

Ordered run: TestGetServerLogs_MissingFileReturnsEmptyNotError, which #1222 flagged as a pre-existing -count=2 failure, was fixed in #1228 and did not fail here.

Follow-ups

  • Watch the first CI runs: they cover ./... (bench, cmd, …), while the local run covered ./internal/... only, so the first few seeds may surface more.
  • After 10 consecutive green runs on main, add Unit Tests (shuffle) to the required checks.
  • If a failure repeats, fix it in the fix(test): restore the registry SSRF allow-policy after loopback fixtures #1222 / this-PR shape (fixtures restore what they flip; readers of a process global snapshot it) rather than by pinning a seed.

Testing

  • actionlint .github/workflows/unit-tests.yml — clean
  • Local shuffled run over ./internal/... — one order coupling found, fixed here, re-verified against the reproducing seed
  • golangci-lint v2 (.github/.golangci.yml) on the changed package — 0 issues

…ail in CI

The registry SSRF regression test only passed because its file sorted
ahead of the two fixtures that flipped the process-global allow-policy
(#1222). Nothing in .github/workflows/ ran with -shuffle, so no lane
could see a different order.

Add 'Unit Tests (shuffle)': the required ubuntu lane's flags and
provisioning (skip regex, -race, tscg shim, frontend embed, tiktoken
warm-up) plus -shuffle=on, without coverage. It is not in the
branch-protection required list, so a newly exposed order coupling in
an unrelated package cannot block a PR while the tail of such tests is
found. Each package prints its seed as '-test.shuffle <n>'; re-run with
-count=1 -shuffle=<n> to reproduce.
…lockedValues

Service.Start reaches PopulateBlockedValues, a sync.Once that appends
the real hostname and home-dir basename to the package-global
BlockedValues and is never undone. Five test files call Start, and this
was the one ScanForPII test that did not snapshot the global, so under
-shuffle a basename such as "user" tripped rule 2 (blocked_value) on
the "terminated by user" payload before rule 7 could report
v7_field_invalid.

Found by the new shuffle lane's first local run
(-shuffle=1788926312892506000). Same save-and-restore the neighbouring
ScanForPII tests already use.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 9, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 63249ac
Status: ✅  Deploy successful!
Preview URL: https://86893fb9.mcpproxy-docs.pages.dev
Branch Preview URL: https://ci-shuffle-lane.mcpproxy-docs.pages.dev

View logs

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: ci/shuffle-lane

Available Artifacts

  • archive-darwin-amd64 (29 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (29 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (21 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 34309799091 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

-shuffle is not a cacheable go test flag, so a seed replay is never served
from the result cache; the note claimed the opposite. Reword to say why
-count=1 is still kept and what the seed does and does not reproduce.
@Dumbris
Dumbris merged commit 0393561 into main Sep 9, 2026
36 checks passed
Dumbris added a commit that referenced this pull request Sep 9, 2026
…them (#1233)

The shuffle lane added in #1230 went red on its first unrelated PR, which is
what it is for.

globalOutputFormat and globalJSONOutput are package-level flag targets in
cmd/mcpproxy. Fifteen tests assigned them and never restored, so whichever
ran last left its choice installed for the rest of the binary. In file order
that is invisible. Under -shuffle=on it is not: TestVersionCommandTableOutput
and TestOutputActivityError_TableFormat both render through
clioutput.ResolveFormat and assert the default table output, so an inherited
"json" fails them. Seed 1788937037978355714 reproduces both on origin/main.

Adds setOutputGlobals(t, format, jsonOut), which saves the pair and restores
it from t.Cleanup, and routes all fifteen sites through it. The sites that
already saved and restored by hand are left alone. No production code
changes, and no test's intended format changes.

Verified: the CI seed fails on main and passes here; five fresh
-race -shuffle=on runs of the package are green; golangci-lint v2 with the
CI config reports 0 issues.

Note for a follow-up, not touched here: cmd/mcpproxy/activity_cmd_test.go is
already unformatted on origin/main.
Dumbris added a commit that referenced this pull request Sep 9, 2026
…untime blocklist (#1231)

* fix(test): isolate every pass-expecting ScanForPII fixture from the runtime blocklist

PR #1230 isolated TestScanForPII_V7FieldViolations from the package-global
BlockedValues, but the -shuffle tail was wider: any test that calls
Service.Start (TestHeartbeatSend and friends) runs PopulateBlockedValues,
which appends the hostname, home-dir basename and sensitive env values to
BlockedValues for the rest of the process. Every later ScanForPII caller
that expects a clean verdict then fails whenever one of those values is a
substring of its payload (a home dir named "ted" trips on "completed").

Apply the existing withoutBlockedValues(t) helper to the ten un-isolated
pass-expecting fixtures and replace the seven hand-rolled
save/nil/defer-restore copies with the same helper. Tests that expect an
error, or that inject their own BlockedValues, are untouched.

A package TestMain would not work here: PopulateBlockedValues is a
sync.Once that fires inside Service.Start, i.e. after TestMain has run.

The -race gate also surfaced a second order-coupled tail in the same
family: TestNotifyConfigChanged_SendFailureStillDisables returned while
its fire-and-forget opt-out beacon goroutine was still inside ScanForPII
reading BlockedValues, racing the next test's reset. The test now points
the beacon at a server that records the attempt and drops the connection
(still a transport-level send failure), and joins on that signal before
returning.

* fix(test): restore the CLI output-format globals after every test that sets them

The advisory shuffle lane failed on TestOutputActivityError_TableFormat with
`"Error: test error message\n" does not contain "Hint:"`. Root cause is in a
different test: TestOutputServers_InvalidFormat assigns
globalOutputFormat = "invalid-format" and never restores it. Those globals feed
ResolveOutputFormat on every command path, so once one test leaves them dirty
GetOutputFormatter fails inside outputActivityError, which takes its
early-return branch and prints the error without the Hint line.

Add setOutputFormat(t, format, jsonAlias), which saves both globals and
restores them via t.Cleanup, and use it at the thirteen sites that assigned
them without restoring. The sites that already saved and restored are
unchanged.

Pre-existing on main and unrelated to the telemetry change in this branch; it
is fixed here because it blocks this PR's shuffle lane.

Reproduces before the fix and passes after, same seeds:
go test -count=1 -shuffle=1 (and 3) \
  -run 'TestOutputServers_InvalidFormat$|TestOutputActivityError_TableFormat$' \
  ./cmd/mcpproxy/

* fix(test): say what the beacon wait actually guarantees, and make the signal non-blocking

Cross-model review (opencode, gpt-6-astra) approved the change but caught the
comment overstating it: receiving from `attempted` waits until the beacon
goroutine has reached the HTTP send, which is past its ScanForPII read of
BlockedValues. It does not join the goroutine, and nothing the goroutine does
after the send touches the blocklist. Reworded both comments to claim only
that.

Also made the handler's signal a non-blocking select. The capacity-one channel
is only received once, so a retried or duplicated request would otherwise wedge
the handler and hang the deferred server Close. Not an exercised path today,
but free to rule out.

No behaviour change to what the test proves: the connection is still hijacked
and dropped without a response, so the send still fails at the transport level.
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