Skip to content

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

Merged
Dumbris merged 4 commits into
mainfrom
fix/telemetry-blockedvalues-shuffle-isolation
Sep 9, 2026
Merged

fix(test): isolate every pass-expecting ScanForPII fixture from the runtime blocklist#1231
Dumbris merged 4 commits into
mainfrom
fix/telemetry-blockedvalues-shuffle-isolation

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #1230. That PR isolated one test from the package-global BlockedValues, but the -shuffle tail is wider: any test that calls Service.Start (e.g. TestHeartbeatSend) runs PopulateBlockedValues, which appends the hostname, home-dir basename and sensitive env values to BlockedValues for the rest of the process. Every later pass-expecting ScanForPII caller then fails whenever one of those values is a substring of its payload. Concretely, a home dir named ted trips TestScanForPII_V7FieldValidValues on "completed" (rule 2 blocked_value, 3-byte minimum).

Changes

  • Apply the existing withoutBlockedValues(t) helper (t.Cleanup save/restore) to the ten un-isolated pass-expecting fixtures: TestScanForPII_CleanPayload, TestScanForPII_V7FieldValidValues, TestPayload_WizardConnectStepCompletedExternal, TestPayload_PreChurnPassesAnonymityScan, TestPayload_FunnelFieldsPopulated, TestBuildPayload_PreflightJSONRoundTrip, TestPayloadV5_DockerCLISourceIsEnumOnly, TestPreflightCounters_NoLeakPII, TestScanForPII_PreflightAllowedKeysMatchWireForm, TestScanForPII_AcceptsWellFormedPreflight.
  • Replace the seven hand-rolled prev := BlockedValues / BlockedValues = nil / defer restore copies with the same helper (identical semantics, one definition). Tests that expect an error, or that inject their own BlockedValues, are untouched.
  • A package TestMain was considered and rejected: PopulateBlockedValues is a sync.Once that fires inside Service.Start, i.e. after TestMain, so clearing there would not help.
  • The -race gate surfaced a second 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 (WARNING: DATA RACE, write at withoutBlockedValues, read at optout.go:116). The test now points the beacon at a server that records the attempt and hijacks/drops the connection (still a transport-level send failure), and joins on that signal before returning. No production code changes.

Proof the change bites

Blocklist leak, HOME=/tmp/bite/ted, targeted pair, -shuffle=3 (heartbeat test runs first):

TestScanForPII_V7FieldValidValues
before FAILrule=blocked_value pattern="ted" (anonymity_test.go:303)
after PASS

(TestHeartbeatSend itself fails under that HOME in both runs because the service correctly refuses to ship a payload containing the home-dir basename. That is the leak's source, not a target.)

Beacon race, GOMAXPROCS=1 go test -race -count=20 -shuffle=7 -run 'SendFailureStillDisables$|TrustModeDistributionAndFunnelCounters$':

result
before WARNING: DATA RACE, --- FAIL: TestPayloadV9_TrustModeDistributionAndFunnelCounters
after ok

Gates

  • go test -race -count=1 -shuffle=on ./internal/telemetry/ × 3: ok (seeds 1788936195458933000, 1788936209447958000, 1788936224100075000)
  • /opt/homebrew/bin/golangci-lint run --config .github/.golangci.yml ./internal/telemetry/: 0 issues

Follow-up: a second, unrelated shuffle-lane failure (commit e4750fb)

The first CI run of this PR failed Unit Tests (shuffle) in cmd/mcpproxy, not in internal/telemetry:

--- FAIL: TestOutputActivityError_TableFormat
    "Error: test error message\n" does not contain "Hint:"

Same class of defect, different package, and pre-existing on main (this branch does not otherwise touch cmd/mcpproxy). TestOutputServers_InvalidFormat assigns globalOutputFormat = "invalid-format" and never restores it. Those globals feed ResolveOutputFormat on every command path, so afterwards GetOutputFormatter fails inside outputActivityError, which takes its early-return branch and prints the error without the Hint: line.

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

-shuffle=1 and -shuffle=3, TestOutputServers_InvalidFormat first TestOutputActivityError_TableFormat
before FAIL — no Hint:
after PASS

Gates re-run over both packages: go test -race -shuffle=on -count=1 ./cmd/mcpproxy/ ./internal/telemetry/ × 3 all ok; golangci-lint v2 over both, 0 issues.

Note: cmd/mcpproxy/activity_cmd_test.go is unformatted on main (a struct-field alignment block at ~line 861). It is untouched here to keep this diff to the fix.


Merge with main: the cmd/mcpproxy half was fixed upstream first

#1233 landed the same cmd/mcpproxy fix on main while this PR was open, under the helper name setOutputGlobals (identical save/restore-via-t.Cleanup semantics, plus a self-test). Merged origin/main in and resolved both conflicted files to main's version, then deleted this branch's now-duplicate setOutputFormat helper. Commit e4750fb is therefore a no-op against current main, and the surviving diff is internal/telemetry only.

Re-audited cmd/mcpproxy after the resolution: no test assigns the output globals without restoring them. Gates re-run on the merged tree — go test -race -shuffle=on -count=1 ./cmd/mcpproxy/ ./internal/telemetry/ twice, both ok; golangci-lint v2 over both packages, 0 issues.


Cross-model review (opencode, github-copilot/gpt-6-astra)

Reviewed in two file-named chunks.

Chunk 1, optout_test.go (the beacon join). Approved for the race fix, with one accuracy finding and one defensive note, both applied in 7e13ade:

  • The comment claimed the test "joins the beacon goroutine". It does not. Receiving from attempted waits until the goroutine has reached the HTTP send, which is past its ScanForPII read of BlockedValues; nothing it does after the send touches the blocklist. Both comments now claim only that.
  • The capacity-one channel is received exactly once, so a retried or duplicated request would wedge the handler and hang the deferred server Close. Not an exercised path, but the signal is now a non-blocking select.
  • Confirmed the hijack-and-drop still produces a genuine transport-level send failure, and that the Hijack-unsupported fallback returns 500, which SendOptOutBeacon also treats as an error.

Chunk 2, the ten isolation files. Approved with no findings. Specifically confirmed: no pass-expecting ScanForPII caller was missed; no error-expecting test or deliberate-blocklist test was weakened (TestScanForPII_RawMachineIDBlocked and TestScanForPII_BlockedValue_EnvVar keep their own injected values, and TestPopulateBlockedValuesFrom still clears before injecting); and every defer to t.Cleanup conversion is at top-level test scope, so parent cleanup runs after all subtests and table iterations.

Gates re-run after the fixes: go test -race -shuffle=on -count=1 ./cmd/mcpproxy/ ./internal/telemetry/ × 3 all ok; the GOMAXPROCS=1 -count=20 race stress ok; golangci-lint v2 over both packages, 0 issues.

…untime 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.
@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: 7e13ade
Status: ✅  Deploy successful!
Preview URL: https://861dbfa5.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-telemetry-blockedvalues.mcpproxy-docs.pages.dev

View logs

@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!

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/telemetry-blockedvalues-shuffle-isolation

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 34341763996 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

…t 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/
…values-shuffle-isolation

# Conflicts:
#	cmd/mcpproxy/tools_cmd_test.go
#	cmd/mcpproxy/upstream_cmd_test.go
… 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.
@Dumbris
Dumbris merged commit d7ad1dc into main Sep 9, 2026
39 checks passed
@Dumbris
Dumbris deleted the fix/telemetry-blockedvalues-shuffle-isolation branch September 9, 2026 15:45
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