Skip to content

refactor(server/api): share the Cache-Control table and name the confidence thresholds - #1266

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

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

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Root cause

Two drift hazards from the 2026-09-02 audit (A06-020, A09-012):

  • Cache-Control (A06-020): setCacheHeaders (container spaHandler) in internal/server/static.go had a byte-identical copy of the extension switch in cacheControlForExt (Lambda serveStaticForLambda). If you added .avif or .map to only one of them, the same asset got a different cache policy depending on whether the deployment was a container or a Lambda Function URL.
  • Confidence thresholds (A09-012): confidenceBucketFor in internal/api/handler_recommendations.go inlined 200 / 3 / 50, the same cut-offs that pkg/exchange uses for the reshape ranking.

Fix

  • setCacheHeaders now calls cacheControlForExt(strings.ToLower(path.Ext(urlPath))), as the issue suggested. There is now one extension-to-policy table.
  • confidenceBucketFor now uses the named constants confidenceHighSavingsUSD, confidenceHighMinCount and confidenceMediumSavingsUSD. Their doc comment says they must equal exchange.ConfidenceHighSavingsUSD, exchange.ConfidenceHighMinCount and exchange.ConfidenceMediumSavingsUSD.
  • Count 0 is reconciled by documenting it, not by changing behavior. The badge still clamps count < 1 to 1, so savings alone decide between medium and low. This differs on purpose from the exchange ranking, which returns a neutral 0.5 on a zero count: a badge must always show a bucket, while a ranking score should stay neutral when data is missing. The go side documents the same split (refactor(exchange): export confidence bucket thresholds as named constants cloud-commitments-go#522 godoc). Tests now cover this, including negative counts.

There is no behavior change: every served Cache-Control value and every bucket is the same as before.

Refs #343

Why Refs, not Closes: the import is left out on purpose

The issue's second acceptance criterion asks this repo to import the exported constants from pkg/exchange. Those constants landed in cloud-commitments-go d11bdde (LeanerCloud/cloud-commitments-go#522). go.mod pins cloud-commitments-go/pkg v0.0.0-20261018003623-541e05e5fd29, which is older, so the import needs a pkg bump. I left the bump out of this PR because:

Once one of those bumps merges, the follow-up is mechanical: replace the three literals in the const block with exchange.Confidence* (the pinned test table stays the same and keeps guarding the values). After that, #343 can be closed.

Verification

Unit tests only. This is a pure refactor with no DB or integration surface, so no integration test was needed.

Environment: AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true, Go 1.26.9.

gofmt -l internal/                                  # clean
go build ./...                                      # ok
go vet ./...                                        # ok
go vet -tags integration ./...                      # ok
gocyclo -over 10 -ignore "_test\.go" internal/server/static.go internal/api/handler_recommendations.go   # none
golangci-lint run --allow-parallel-runners ./internal/server/ ./internal/api/   # 0 issues (v2.10.1)
go test ./internal/server/ -count=1                 # ok
go test ./internal/api/ -count=1                    # ok
go test ./internal/mocks/ -count=1                  # ok

New and extended tests:

  • internal/server/static_test.go:
    • TestCachePolicyPinned pins the exact header for every listed extension, plus upper- and mixed-case variants and default fall-through cases (.json, .avif, .js.map, .txt, no extension). It checks both setCacheHeaders and cacheControlForExt.
    • TestCachePolicyParity_ContainerAndLambda serves the same files through spaFileServer and serveStaticForLambda and requires the same header from both.
  • internal/api/handler_recommendations_test.go TestConfidenceBucketFor adds cases at and just below each threshold, and for count 0 with high and low savings and a negative count.

Before/after evidence. This is a behavior-preserving refactor, so the pins are meant to pass both before and after:

  • The tests pass on this branch and pass with origin/main's static.go / handler_recommendations.go swapped back in.
  • Drift probe on main: adding .avif to only the Lambda switch makes TestCachePolicyPinned/hero.avif fail on the cacheControlForExt assertion while the setCacheHeaders assertion still passes (the copies diverge). TestCachePolicyParity_ContainerAndLambda/hero.avif also fails. With the fix, the same one-line edit moves both paths together.
  • Mutation probes on confidenceBucketFor, each killed by TestConfidenceBucketFor: 200→199, 3→2, 50→51, >=→> on high savings, and removing the count clamp.

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

Branch Tested head
fix/issue-337 ff3420c
fix/issue-169 7d75eb5
fix/issue-342 66b221f
fix/issue-310 5e11c21
fix/issue-326 5f0ea59
fix/issue-243 365e959
fix/issue-343 6c94036
fix/issue-312 d2848a8
fix/issue-316 acd60dd
fix/issue-339 75ea9b9
fix/issue-339-commitment-type 73bdb19
fix/issue-353 5d25fcd
fix/issue-313 3156bc4
fix/issue-357 53d9e58
fix/issue-368 0ba727d
fix/issue-419 61648c9
fix/issue-111 b04c372
fix/issue-686 0ad90b7
fix/issue-674 793095d
fix/issue-1055 4ec96f1
fix/issue-559 dfa9525
fix/issue-558 034eed4
fix/issue-874 1720539
fix/issue-666 1e120da
fix/issue-668 fb26345
fix/issue-568 ce04173
fix/issue-311 e54f250
fix/issue-177 0cbb88a
fix/issue-151 c21377c
fix/issue-166 41fb77b
fix/issue-27 2d67622
fix/issue-323 69288ae
fix/issue-59 36463e8
fix/issue-43 35e4069
fix/issue-158 6f76f8a
fix/issue-104 bb00491
fix/issue-55 971ef1b
fix/issue-361 89ac220
fix/issue-120 b458719
fix/issue-36 42bac6e
fix/issue-525 57c97dd
fix/issue-712 e5bcda0
fix/issue-72 6cc63ed

fix/issue-72 and fix/issue-312 conflict on one line in internal/api/handler_dashboard.go (both change the calculateCommitmentMetrics call). The tree used #72's side with #312's validated provider variable; 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:

  • Passed at 936996ad: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks at 936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. tests/e2e is a separate module, so it was vetted under its own e2e tag. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package, one package at a time (-tags integration -count=1, 60-minute local timeout because the VM was shared):
    • At 936996ad, these pass: internal/server, internal/credentials, internal/scheduler, internal/database/postgres/migrations, internal/purchase and internal/api.
    • 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).
    • These were run on the first build of this tree (26795376, same branches at earlier heads): internal/analytics, internal/auth, internal/commitmentopts, internal/database and cmd/lambda/clear-rate-limit all pass. The later fixes don't touch those packages.
  • What the combined run caught, each fixed on its own branch before this PR opened:
    • a compile break between fix/issue-104's test and fix/issue-326's httpToLambdaRequest signature change;
    • fix/issue-316 rejecting legacy-keyed plans;
    • fix/issue-120 versus main's global-filter test.
  • 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

  • Bumping cloud-commitments-go/pkg and importing exchange.Confidence* (see above).
  • The RecommendationDetailResponse / confidenceBucketFor comments still say the heuristic "previously lived" in frontend/src/recommendations.ts. That is historically accurate (the copy no longer exists), so I left them.

No frontend change is needed.

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

🤖 Generated with Claude Code

…idence thresholds

setCacheHeaders (container spaHandler) carried a byte-identical copy of
the extension switch in cacheControlForExt (Lambda static path), so
adding an extension to one gave the same asset a different cache policy
per deployment mode. setCacheHeaders now calls cacheControlForExt.

confidenceBucketFor inlined 200 / 3 / 50. They are now named constants
documented as mirrors of pkg/exchange's ConfidenceHighSavingsUSD,
ConfidenceHighMinCount and ConfidenceMediumSavingsUSD. Importing those
needs a cloud-commitments-go/pkg bump past d11bdde, which is left out
here. The count-0 clamp is kept and documented as a deliberate
difference from the exchange ranking's neutral score.

No behavior change. New table tests pin every Cache-Control value
through both static paths and the exact confidence boundaries,
including count 0 and negative counts; they pass before and after.

Refs LeanerCloud#343

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/eventually No deadline effort/xs Trivial / one-liner 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/xs Trivial / one-liner 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/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant