Skip to content

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

Merged
IAmJSD merged 4 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-76
Oct 25, 2026
Merged

IAmJSD merged 4 commits 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

Warning

Review limit reached

  • Run on-demand review

This review includes 2 billable files and costs up to $0.50.

Or wait 59 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between a59740d and 091c979.

Files selected for processing: 2
  • internal/analytics/collector_test.go
  • internal/mocks/config_store_copies_guard_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

IAmJSD and others added 2 commits October 25, 2026 16:29
Bring the branch up to date with main.

Conflicted files:
- internal/analytics/collector_test.go: main kept adding stubs to the
  hand-rolled analytics mockConfigStore (HistorySummaryAggregate from
  LeanerCloud#1200, ClaimCollectionInvocation from LeanerCloud#1099, the ReplaceRecommendations
  removal from LeanerCloud#1209, and the pgx import they need). This branch replaces
  that hand-rolled mock with one that embeds mocks.MockConfigStore, which
  already carries all of those methods on main. Kept the branch's version;
  main's stub edits are covered by the shared mock and drop out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring the branch up to date with main again; main moved on while the
previous merge was being checked.

Conflicted files:
- internal/analytics/collector_test.go: main (LeanerCloud#1242) added a
  CancelExecutionAtomicTokenPinned stub to the hand-rolled analytics
  mockConfigStore. This branch replaces that mock with one embedding
  mocks.MockConfigStore, which main already gives the same method. Kept
  the branch's version; the stub is not needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Merged origin/main into this branch twice: first 8f186a8, then c1f1781, because main moved while the first merge was being checked. New head: 9e7fd37.

Conflict resolution (both merges, same file):

Checks on the merged head:

  • build, vet (also with -tags integration) and e2e vet pass.
  • golangci-lint found 0 issues with and without the integration tag on analytics and mocks.
  • go test -race -short passes on both packages. That includes TestNoHandRolledConfigStoreMocks, which still finds no new hand-rolled copies on main.
  • go test -race -tags integration ./internal/analytics passes.
  • The secrets scan is clean and the planted key was flagged. The allowlist test passes.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: REQUEST_CHANGES 9e7fd37a603f5f2de69bed12c39c66824e681b1b

Blocking:

  • internal/analytics/collector_test.go: the branch conflicts with main (GitHub shows CONFLICTING/DIRTY). Since the merge base, main's fix(marketplace): recover pending listings through cancel and never record an empty or revived state #1299 added GetMarketplaceListingAttempt and ResolveMarketplaceListingAttempt stubs to the hand-rolled mockConfigStore, and main also dropped GetPendingExecutionsTx there, in the very block this PR deletes. GitHub won't merge it until that's resolved. The resolution is easy: take this branch's side of the file, since the embedded mocks.MockConfigStore on main already has both new methods. I checked that resolution on top of origin/main: go vet (plain and -tags integration) passes on analytics and mocks, go test -race -short on analytics and mocks passes, and TestNoHandRolledConfigStoreMocks passes. Please merge origin/main with that resolution. Apart from the conflict, the content is approvable as is.

Non-blocking:

  • The guard is a heuristic: it only catches a copy that defines more than half of StoreInterface's 147 methods on one type in a _test.go file. A copy split across embedded helper types, or one in a non-test helper file, would get past it. That's fine as a tripwire, and the allowlist-can-only-shrink checks are a nice touch.

Attacked:

  • Analytics: the collector's only config-store read is GetActivePurchaseHistory, and the override keeps its nil, nil default. Every other method now comes from mocks.MockConfigStore, which panics through m.Called if it is reached unexpectedly, so a new collector read would show up loudly rather than silently returning nil. The four removed func fields have no setters anywhere, including integration-tagged files (go vet -tags integration is clean).
  • Guard fail-before: with main's collector_test.go restored, TestNoHandRolledConfigStoreMocks fails and names internal/analytics.mockConfigStore. With the PR's version it passes. Restored afterwards. StoreInterface has no embedded interfaces, so the parsed method set is complete.
  • Ran: go build ./..., go vet (with and without -tags integration) on analytics and mocks, golangci-lint v2.10.1 (both tag sets, 0 issues), go test -race -short -count=1 on analytics and mocks, and go test -race -tags integration -count=1 ./internal/analytics/ (real Postgres). All pass at the head. Then git merge-tree against origin/main, which conflicts as described above.

Body-stale:

  • "Fix (first slice) ... 527 lines of stubs are gone": the stub block on main has since changed (two methods added, one removed), so after the merge the count differs slightly. Not material.

Reviewer: rB12. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

Brings the branch up to date with main so the PR is mergeable again.

Conflicted files:
- internal/analytics/collector_test.go: main edited the hand-rolled
  mockConfigStore stubs this branch deletes. LeanerCloud#1299 added
  GetMarketplaceListingAttempt and ResolveMarketplaceListingAttempt,
  and LeanerCloud#1217 removed GetPendingExecutionsTx. The branch's version is kept,
  where mockConfigStore embeds mocks.MockConfigStore and overrides only
  GetActivePurchaseHistory. Main's two PRs made the matching changes to
  mocks.MockConfigStore, so the embedded mock already has the new
  methods and lacks the removed one. The file is identical to the
  branch head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Merged current main into the branch. New head: 091c979.

Checks:

  • go build and go vet (default, integration, e2e) pass. golangci-lint reports 0 issues with and without the integration tag.
  • go test -race passes on internal/analytics and internal/mocks. The internal/analytics integration tests also pass with -race.
  • The git-secrets scan is clean, and the allowlist test gives 137 PASS.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 091c9797768d6f489a1df830ae89501c05150e5e

Blocking:

  • none

Non-blocking:

Attacked:

  • Panic risk: switching to the shared mock means every method except the override now goes through m.Called, which panics when no expectation is set. I checked that the collector reads the config store only through GetActivePurchaseHistory (internal/analytics/collector.go:96). mockConfigStore is used only in collector_test.go. The analytics integration tests compile and pass against it.
  • Ran: go build ./..., and go vet on analytics and mocks with and without -tags integration (ok). go test -race -short -count=1 ./internal/analytics/ ./internal/mocks/ passed, go test -race -tags integration -count=1 ./internal/analytics/ passed, and golangci-lint reported 0 issues.
  • Fail-before: with main's collector_test.go restored, TestNoHandRolledConfigStoreMocks fails and names internal/analytics.mockConfigStore. I restored the file afterwards.
  • Refs #76 (not Closes) is right: internal/server.mockConfigStoreForHealth is still hand-rolled and on the allowlist.

Body-stale:

  • "145 methods" / "the threshold is 73": the counts are now 148 and 75, as above. Otherwise accurate.

Reviewer: rB18. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at 091c9797768d6f489a1df830ae89501c05150e5e merged onto main (de56f95b), together with #1114, #1142, #1254, #1274, #1278, #1279, #1280, #1282, #1414 in that order (combined tree e1bf26a0, Linux, Go 1.26.10): go build ./..., go vet with and without -tags integration, the full go test -race -short -count=1 ./..., and go test -race -tags integration -count=1 on every changed package, against Docker Postgres. All pass. CI Success and pre-commit are green on this head, with 1 approving independent review(s) on it. Nothing was run against any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 25, 2026 22:35
@IAmJSD
IAmJSD merged commit 7749401 into LeanerCloud:main Oct 25, 2026
38 checks passed
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 1 approving independent review(s) on 091c9797, green CI, and the combined-tree verification above. Squash-merged at exactly 091c9797768d6f489a1df830ae89501c05150e5e.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 25, 2026
Conflict: internal/analytics/collector_test.go. LeanerCloud#1254 folded the hand-rolled analytics mock onto mocks.MockConfigStore, so took main's file and dropped this branch's two stubs there; the shared mock already carries them from this branch's internal/mocks/stores.go change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 25, 2026
LeanerCloud#1254 (on main) replaced the hand-rolled config-store mock in
collector_test.go and dropped the now-unused pkg/common import. This
branch's new commitment-type assertion uses common.CommitmentType, so the
analytics test build broke after main was merged in although the merge
was textually clean. Same assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 26, 2026
LeanerCloud#974 was squash-merged into main as 97d04ec, whose recommendations
handler files are byte-identical to the LeanerCloud#974 branch tip (11ddfea) this
branch was stacked on. The handler and page files were re-merged with
that tip as the base, which keeps this branch's chunked-scan changes
and main's later LeanerCloud#1266 Cache-Control change. In openapi.yaml the
recommendations parameters take main's LeanerCloud#1100 list. collector_test.go
takes main's LeanerCloud#1254 version, whose embedded mocks.MockConfigStore
already has ListStoredRecommendationsChunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 26, 2026
collector_test.go takes main's LeanerCloud#1254 version: its hand-rolled
ConfigStore mock now embeds mocks.MockConfigStore, which this branch
already extends with ListMarketplaceListingsToPoll,
RecordMarketplaceListingActive and SettleMarketplaceListing, so the
three local stubs the branch had added are no longer needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit that referenced this pull request Oct 26, 2026
…vice (#1302)

* fix(purchase,server): skip the $0 purchase confirmation and report a failed lazy DB init on /health

An execution with nothing selected never reaches a cloud API, but both the
single-account and the per-account fan-out paths still sent a "purchase
confirmed" email listing zero commitments. Gate the confirmation on
anyRecPurchased / committed, so it goes out only when a real commitment
was created (full or partial success).

When reinitializeAfterConnect fails after setting app.Config, ensureDB
closes the pool and clears app.DB but leaves app.Config, and the
config_store health check reported healthy over the closed pool. Snapshot
dbConnected with the store pointers under dbMu and report unhealthy while
lazy PostgreSQL init has not completed.

Audit findings A05-015 and A06-009. A06-019 (commitment_type labels)
follows separately.

Refs #339

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analytics): label snapshot commitment types from provider and service

commitmentTypeFor matched only a service literally named "SavingsPlans",
which purchase_history never stores, so every commitment was stored as
"RI": AWS Savings Plans ("savingsplans" / "savings-plans-<type>") and GCP
committed-use discounts included. Derive the type from (provider, service)
in the pkg/common CommitmentType vocabulary, and widen the
savings_snapshots CHECK in migration 000145 to match. The CHECK still
admits the legacy 'RI' / 'SavingsPlan' literals so an old replica can
write during a rolling deploy; existing rows are relabeled.

Audit finding A06-019.

Refs #339

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(purchase): make the #339 no-confirmation assertions checkable by the mock guard

TestNoUnfailableMockAssertions could not resolve the receiver of two
AssertNotCalled sites (a mock returned by a helper). Declare the mock
directly in the new test, and in the approve test rely on the missing
SendPurchaseConfirmation expectation, which fails the test on any send.

Refs #339

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(analytics): restore the pkg/common import after #1254

#1254 (on main) replaced the hand-rolled config-store mock in
collector_test.go and dropped the now-unused pkg/common import. This
branch's new commitment-type assertion uses common.CommitmentType, so the
analytics test build broke after main was merged in although the merge
was textually clean. Same assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(db): renumber savings_snapshots_commitment_type_vocabulary migration 000145 -> 000141

golang-migrate only applies versions above the database's current one,
so PRs must merge in migration-number order. 000140-000144 had been
taken by PRs that are now held on owner decisions (#1239, #1241,
#1253). The ready PRs move to 000140-000145 so they can merge ahead of
them, and the held ones will take the next free numbers when decided.
Rename only: the SQL statements are unchanged, and the test version
constants and the comments naming the migration follow the new number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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