Skip to content

test(analytics): fold the hand-rolled ConfigStore mock onto mocks.MockConfigStore - #1254

Draft
IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-76
Draft

IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-76

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Root cause

Three test doubles implemented the whole of config.StoreInterface (145 methods) by hand: internal/mocks.MockConfigStore, internal/analytics.mockConfigStore and internal/server.mockConfigStoreForHealth. Each new store method therefore needed a stub in all three places. Nothing stopped a fourth copy from appearing.

Fix (first slice)

  • internal/analytics/collector_test.go: mockConfigStore now embeds mocks.MockConfigStore and overrides only GetActivePurchaseHistory, the one config-store read the collector makes (collector.go:96). The default stays nil, nil, as before. 527 lines of stubs are gone. The four other func fields (getPurchaseHistoryFunc, getAllPurchaseHistoryFunc, getPurchaseHistoryByPurchaseIDFunc, markPurchaseRevokedFunc) were never set by any test, so they go too. No test call site changed.
  • internal/mocks/config_store_copies_guard_test.go (new): a repo-wide syntactic guard, TestNoHandRolledConfigStoreMocks. It reads the StoreInterface method set from internal/config/interfaces.go. It fails when any type declared in a _test.go file defines more than half of those methods. Today the largest partial override defines 24 of 145 (internal/server.mockConfigStoreForExchange), and the threshold is 73, so there is plenty of margin. internal/server.mockConfigStoreForHealth is allowlisted until it is folded. The allowlist can only shrink: the guard fails if an entry no longer hand-rolls the interface or no longer exists.

No production code changed. internal/mocks/stores.go is untouched, to keep conflicts with the open branches that add store methods to a minimum.

Refs #76

Verification

All of this is unit/mock evidence. The change is test-only.

export PATH=$HOME/.local/go/bin:$HOME/go/bin:$HOME/.local/bin:$PATH
export AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null
go build ./...                                  # ok
go vet ./...                                    # ok
go vet -tags integration ./...                  # ok (analytics integration tests still compile against the new mock)
go test ./internal/mocks/ -count=1              # ok
go test ./internal/analytics/ -count=1          # ok
golangci-lint run --allow-parallel-runners ./internal/analytics/ ./internal/mocks/                     # 0 issues
golangci-lint run --allow-parallel-runners --build-tags integration ./internal/analytics/            # 0 issues

Fail-before / pass-after for the guard:

  • With main's internal/analytics/collector_test.go restored: go test ./internal/mocks/ -run TestNoHandRolledConfigStoreMocks FAILS and reports internal/analytics.mockConfigStore as a hand-rolled config.StoreInterface test double.
  • With this branch: PASS.

Mutation probes:

  • Replacing the allowlist entry with a nonexistent type makes the guard FAIL twice: once for the stale entry, and once because mockConfigStoreForHealth is now reported as a copy.
  • Deleting the GetActivePurchaseHistory override from the analytics wrapper makes TestCollectorCollect FAIL with a panic. The shared mock dispatches that method through m.Called without a default, so the wrapper's override really is what the collector tests exercise.

No non-test Go files changed, so gocyclo has nothing to check.

Combined-tree verification. This branch was tested together with six other queued branches on a local, never-pushed tree: queue/platform @ 77d116c6 = origin/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-47 62feb4f
fix/issue-76 3b8c241
fix/issue-1233 e1638b7
fix/issue-153 8729074
fix/issue-532 0971624
fix/issue-64 fefdc43
fix/issue-54 91827b9

Linux, Go 1.26.9, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:

  • Passed: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks (changed packages): golangci-lint v2.10.1 found 0 issues, with and without --build-tags integration, and gocyclo -over 10 is silent on the changed non-test files. These ran at b61a9c30, which differs from 77d116c6 only by one .gitallowed line on fix/issue-1233.
  • Integration tests, every changed package, one package at a time (-tags integration -count=1):
    • These packages pass: cmd/cleanup-lambda, internal/analytics, internal/auth, internal/database/postgres/migrations and internal/purchase.
    • internal/api passes, except two tests whose Postgres container didn't start, or whose migrations hit a timeout, under machine load. Rerun alone on the same tree, both pass: TestDeleteManagedPlanByExecutionStatus and TestNotifiedRow_RescheduleDoesNotOverwriteConcurrentCancel.
    • internal/config passes except TestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).
    • Packages were run with a 60-minute timeout (rather than CI's 20) because the VM was shared with other test runs. Some reruns used TESTCONTAINERS_RYUK_DISABLED=true to avoid a testcontainers reaper race under load.
  • Secrets: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.
  • Nothing was applied to any cloud.

Out of scope / remaining for #76

  • internal/server/test_helpers_test.go mockConfigStoreForHealth (145 methods, about 520 lines) still needs folding. It is left for a follow-up to keep this diff small. It needs more care than the analytics copy:
    • Roughly 60 MockConfigStore methods (for example GetServiceConfig, SaveGlobalConfig, GetPendingExecutions, GetRIExchangeDailySpend, ReserveRIExchange) call m.Called unconditionally, so they panic when no expectation is set.
    • The health mock returns benign values instead, and several server tests and embedders (handler_ladder_test.go, handler_ri_exchange_test.go) rely on that implicitly.
    • The follow-up has to keep overrides for every such method the server tests reach, and must not let a recovered panic turn into an expected error path.
    • When it lands, delete its entry from handRolledConfigStoreAllowlist and change this to Closes #76.
  • Audit finding A15-007 (the duplicated Azure MockHTTPClient, under providers/azure/...) belongs to the cloud-commitments-go repository. That code does not exist in this repo, so it is not addressed here.

Maintainer: please mirror the issue's triage labels on this PR: impact/internal, type/chore, triaged, priority/p3, severity/low, urgency/this-quarter, effort/m.

🤖 Generated with Claude Code

…kConfigStore

The analytics collector test carried a 145-method copy of
config.StoreInterface, so every new store method needed a stub there as
well as in internal/mocks. The collector only reads
GetActivePurchaseHistory, so the test double now embeds the shared
MockConfigStore and overrides just that method; the four other func
fields it carried were never set by any test.

Add a repo-wide guard in internal/mocks that fails when a _test.go type
defines more than half of StoreInterface's methods, so a new full copy
cannot appear. internal/server's mockConfigStoreForHealth is allowlisted
until it is folded the same way; the allowlist fails on stale entries,
so it can only shrink.

Refs LeanerCloud#76

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added impact/internal Team-internal only type/chore Maintenance / non-user-visible triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/this-quarter Within the quarter effort/m Days labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/internal Team-internal only priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant