Repository navigation
fix(purchase,server): skip the $0 purchase confirmation and report a failed lazy DB init on /health - #1269
Draft
IAmJSD wants to merge 2 commits into
Draft
fix(purchase,server): skip the $0 purchase confirmation and report a failed lazy DB init on /health#1269IAmJSD wants to merge 2 commits into
IAmJSD wants to merge 2 commits into
Conversation
…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 LeanerCloud#339 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heckable 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 LeanerCloud#339 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This branch has not been deployed
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.
First slice of #339 (the 2026-09-02 audit's low-severity correctness findings). It fixes A05-015 and A06-009. A06-019 is deferred to a follow-up PR; the reasons are below.
Refs #339
Per-finding status (re-verified against main at a14dc8f)
ErrPlanStepNoRecommendations), which covers the cron/notification row with an empty list that the audit described. Still reproducible on main: a run whose recommendations are all unselected (a plan step or a direct execute). It goes throughexecuteSingleAccountand sends a confirmation for zero commitments and $0. The multi-account path (executeForAccount) did the same, because it gated onlen(purchaseErrors) == 0 || partial. Both paths now gate on whether any rec was purchased./healthconfig_storereports healthy after a failed post-connect reinitRefs #339)commitmentTypeFormatches only"SavingsPlans", whichpurchase_history.servicenever stores (Savings Plans are stored assavingsplansorsavings-plans-<type>), so AWS Savings Plans are labelled "RI" as well as GCP CUDs. The fix needs a migration that widens thesavings_snapshotsCHECK and relabels existing rows. I left it out of this slice for two reasons. First, the full set came to about 665 changed lines, well over our 400-line slice budget. Second, a migration PR has to wait until open migrations 000139 to 000144 (#1201, #1239, #1240, #1241, fix/issue-47) merge, and these two fixes don't need to wait. The follow-up is written and tested: commit 73bdb19 on local branchfix/issue-339-commitment-type(not pushed yet). It maps(provider, service)to thepkg/commonCommitmentTypevalues, adds migration 000145, and includes fail-before/pass-after unit, migration and Postgres tests.In-flight overlap check: I diffed open PRs #1099, #1111, #1237, #1245 and #1246, the pushed branches fix/issue-47, -54, -532 and -337, and the working trees of the unpushed fix/issue-243 and fix/issue-326 against main. None of them touch
executeSingleAccount,executeForAccount,internal/server/health.go,readiness_test.go,health_test.go, or the edited lines inapprovals_test.go/coverage_extra_test.go. #1111 (fix/issue-835) editsapprovals_test.goat about line 1333, far from the change here. Nothing had to be deferred because of overlap.Root cause and fix
A05-015: confirmation email for a run that bought nothing (
internal/purchase/execution.go)processPurchaseRecommendationsreturns(0, 0, nil, nil)when no recommendation is selected. InexecuteSingleAccountthe failure guard islen(purchaseErrors) > 0 && !anyRecPurchased(...), which is false here, sosendPurchaseNotificationfired unconditionally.executeForAccountgated on "no per-rec errors or partial", which is also true when nothing ran.Fix: send the confirmation only when
anyRecPurchased(exec.Recommendations)is true (single-account) orcommittedis true (per-account; it is the sameanyRecPurchasedresult thatapplyAccountOutcomealready computes). Full and partial successes are unchanged: they always have at least one Purchased rec, andbuildPurchaseConfirmationDataonly ever listed Purchased recs.A06-009:
config_storehealthy over a closed pool (internal/server/health.go)reinitializeAfterConnectsetsapp.Configand can still fail afterwards (encryption key, analytics collector,LoadDefaultConfig).ensureDBthen closes the pool and setsapp.DB = nil, but leavesapp.Configin place anddbConnectedfalse.checkConfigStoresaw a non-nil store with a nil DB and returnedhealthy.Fix:
collectHealthalready snapshots the store pointers underdbMu. It now also snapshotsapp.dbConnectedand passes it tocheckConfigStore, which reportsunhealthy("Database initialization did not complete; config store is not connected") when PostgreSQL is configured but lazy init has not completed. In production this state (Postgres configured,Configset, not connected,dbMufree) only happens after a failed reinit.app.Configis nil untilreinitializeAfterConnectsets it, and whileensureDBholdsdbMuthe existingTryLockpath already reportspending./healthstays HTTP 200 (liveness contract), and/readywas already 503 in this state becauseauth_storefails, so no probe verdict changes. Only the sub-check stops lying. The flag is read inside the existingTryLockcritical section, so the snapshot has no new race.Three readiness tests built a "fully wired" app with
dbConfigset but never setdbConnected. They now setdbConnected: true, which is the real post-ensureDBstate. Without it, two of them would be not-ready for the wrong reason.Tests that pinned the A05-015 bug
TestManager_ApproveAndExecute_EmptyPlanID,TestProcessMessage_ApproveHappyPathandTestProcessMessage_ApproveFourEyesOn_DifferentApproverSucceedsrun executions with no selected recs and registered aSendPurchaseConfirmationexpectation that required the email. They now assert that no confirmation is sent. In the approve test the assertion is the missing mock expectation: an unexpected call panics the mock. Theinternal/mocksvacuous-assertion guard can't resolve a mock returned bynewApproveManager, so an explicitAssertNotCalledthere would be unverifiable.Verification
Commands ran on this branch with Go 1.26.9 and
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true. The A05-015 evidence is unit/mock evidence. The A06-009 evidence includes a real-Postgres integration test (testcontainers) that drives the realensureDB→reinitializeAfterConnectfailure path. No cloud APIs were called. The reinit fails on a deliberately missingAWS_PROFILE, before any AWS client is used.New tests:
internal/purchase/empty_selection_notify_test.goTestExecutePurchase_SingleAccount_NothingSelected_SendsNoConfirmation: plan step, all recs unselected, throughexecutePurchase.TestExecuteForAccount_NothingSelected_SendsNoConfirmation: multi-account per-account path.internal/server/health_reinit_failure_integration_test.go,TestHealth_ConfigStoreAfterFailedReinitialization(integration tag): real Postgres.ensureDBfails inreinitializeAfterConnectafterapp.Configis set. The test asserts thatapp.Configis non-nil,app.DBis nil,config_storeisunhealthy, the overall status isdegraded, and the app is not ready.TestCheckConfigStoregains "config store left over a failed lazy init" (unhealthy) and "config store after a completed lazy init" (healthy).Fail before (
internal/purchase/execution.gorestored fromorigin/main, each test run on its own):Fail before (the new
if app.dbConfig != nil && !connectedguard disabled):Pass after:
Not run here: whole integration packages,
-race ./...and git-secrets. They run on the combined tree below.Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree:
queue/platform@936996ad= origin/maina14dc8f2plus these heads, merged in this order:fix/issue-72andfix/issue-312conflict on one line ininternal/api/handler_dashboard.go(both change thecalculateCommitmentMetricscall). The tree used #72's side with #312's validatedprovidervariable; whichever PR merges second takes that same resolution.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:936996ad:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without--build-tags integration.tests/e2eis a separate module, so it was vetted under its owne2etag.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, 60-minute local timeout because the VM was shared):936996ad, these pass:internal/server,internal/credentials,internal/scheduler,internal/database/postgres/migrations,internal/purchaseandinternal/api.internal/configpasses exceptTestPostgresStoreDB_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).26795376, same branches at earlier heads):internal/analytics,internal/auth,internal/commitmentopts,internal/databaseandcmd/lambda/clear-rate-limitall pass. The later fixes don't touch those packages.httpToLambdaRequestsignature change;scripts/setup-git-secrets.shpatterns 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.Left out of scope / for a maintainer
completed, and on a plan step it still callsCompletePlanStepand advances the ramp.TestProcessMessage_ApproveHappyPathshows that path. fix(plans): fail loudly on plan steps that carry no recommendations #618 made a plan step with no recommendations fail for exactly this reason. Whether a step whose recs were all deselected should also fail, or should count as a deliberate skip, is a product decision, so I didn't change it. The issue's acceptance criteria only ask for the email gate.applyAccountOutcomestill returnspartial, which its only caller now ignores (_, committed = ...). I left the signature alone to keep the diff small.No frontend change is needed.
Maintainer: please mirror the issue's triage labels onto this PR:
triaged,priority/p3,severity/low,urgency/eventually,impact/few,effort/m,type/bug.🤖 Generated with Claude Code