Repository navigation
fix(purchase): persist the Redshift first-dispatch time and pass it as RequestedAt - #1762
Conversation
Part of #702 (Redshift re-drive idempotency, PR1 commit 1 of 3). - Migration 000158 adds a nullable first_dispatch_at timestamptz. - PurchaseExecution.FirstDispatchAt is server-only (json:"-") and is written once at INSERT; the upsert's DO UPDATE SET leaves it out, so a stale in-memory copy can never clear or move it. - Every full-row projection and the shared scanner read it. Nothing sets it yet; a later commit stamps it before the first dispatch and passes it to the library as PurchaseOptions.RequestedAt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of #702 (Redshift re-drive idempotency, PR1 commit 2 of 3). StampFirstDispatchTx sets first_dispatch_at on the claimed running row in the caller's transaction and returns it with a DB-clock horizon check: - COALESCE keeps an existing stamp, so a re-drive sees the same time. - A fresh stamp is minted only at attempt 0, or when the predecessor ended no_submission at attempt 0. A retry of a possibly submitted lineage with no stamp gets none, which callers treat as a refusal. - Taking the caller's transaction lets the dispatch gate stamp and check conflicts atomically, so a rollback undoes the mint. Not called yet; the next commit wires it into dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of #702 (Redshift re-drive idempotency, PR1 commit 3 of 3). executeAndFinalize stamps first_dispatch_at for an execution with a selected Redshift rec, after the pre-execution refusals and before the provider call, and the stamp becomes PurchaseOptions.RequestedAt in place of the per-dispatch time.Now() from #1138. A re-drive of the same lineage therefore hands the library's DescribeReservedNodes dedupe the same window start as the first dispatch. - Fan-out per-account rows inherit the root stamp by struct copy and are never stamped themselves. - A store error at attempt 0 fails before submission. A missing stamp on a later attempt is a plain error, so a possibly submitted lineage is never re-stamped. - A Redshift rec that reaches the provider without a stamp is refused instead of buying with a zero RequestedAt. Redshift re-drive and retry stay refused in this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 44 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
LeanerCloud#1762 took 000158. Rename the up, down and test files and update the test names, identifiers and comments that name this migration. The tests still resolve the version just below it dynamically. Refs LeanerCloud#47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@coderabbitai review |
Action not completed
|
|
APPROVE @ 3a6caba Three commits on merge-base 4c02609. go is pinned at 61111428a353, and only the Redshift dedupe reads Before 000158 is applied. Every full-row projection and the INSERT now name The stamp is written once
RequestedAt reaches the provider. The path is The down migration ( Tests.
Fail on main. With main's non-test sources, the purchase and config integration tests don't build ( Mutation probes (each reverted)
There's no security surface ( Findings (none blocking)
Not merging. |
|
Thanks @cc-libs. Responses to the non-blocking findings:
|
|
@coderabbitai review |
Action not completed
|
|
Money-path cold review: APPROVE at 3a6caba, with one rollout requirement for the PR body or release note. Base main 4c02609. The four requested checks
Mutation probes (all 8 killed)
Note for PR3a/3b (not blocking here)The Retry successor builder ( Exact-head gate (Go 1.26.10)
|
Part of #702. Migration numbers are now assigned at merge time as the next free number on main (000153 is the highest today). Rename only: the migration SQL is unchanged; its test and one comment follow the number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merging at fe14519 (claude-fleet, author). The approvals from @cc-libs and @as-qa-2 were given at 3a6caba. The only change since then is fe14519, Verified on the merged tree (origin/main 0015bea plus this head, via
CI is green at this head. CodeRabbit is waived from the gate. Prod: apply migration 000154 before the prod code deploy, and never down-migrate it while this code is live. Dev auto-migrates on merge. |
LeanerCloud#1762 added first_dispatch_at as the 30th SavePurchaseExecutionTx argument. The Save SQL on the merged tree still has canceled_by = COALESCE(purchase_executions.canceled_by, $17), and LeanerCloud#1762 added no cancelled_by writer or bare reader. Refs LeanerCloud#47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nerCloud#1762 Main now has 000154_purchase_executions_first_dispatch_at. Move the recommendations az_config expand to 000155, the purchase_suppressions expand to 000156 and the contract to 000157, and update every reference: SQL headers, store comments, fixtures and gate test versions. The gate error and 503 message now build the migration numbers from two constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…HECKs, write only canceled_by (#1253) * fix(db): contract the canceled rename: normalize, narrow the status CHECKs, stop using cancelled_by Migration 000144 is the contract half of 000089. Under a table lock it normalizes every legacy 'cancelled' status in purchase_executions and ri_exchange_history, drains legacy-only actors into canceled_by, and narrows both status CHECKs to the 000089 lists minus 'cancelled'. The code stops reading and writing purchase_executions.cancelled_by: reads project canceled_by, the cancel transition and SetCancelledBy write only canceled_by, and SavePurchaseExecution writes canceled_by, keeping an actor already on the row (the value reads saw before). The LegacyStatusCanceled constant and every dual-spelling branch are gone. The cancelled_by column is kept. The previous build still reads COALESCE(canceled_by, cancelled_by) and writes cancelled_by, and it keeps serving during a rolling deploy and after a code-only rollback, so dropping the column in the same release would break every execution read on it. A trigger mirrors any legacy-only actor write into canceled_by until a follow-up migration drops the trigger and column. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): drop the legacy canceled spelling from the history summary test The test came in from main (#1200) and seeds executions with LegacyStatusCanceled and queries it as an execution status. This branch removes that constant, and migration 000144 rejects the 'cancelled' status, so the seed would fail the narrowed CHECK. Seed and query only the canonical spelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(deps): bump golang.org/x/net to v0.62.0 (GO-2026-6860, GO-2026-6866) govulncheck in the Docker image build now fails on these two advisories, both fixed in x/net v0.62.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(migrations): target renumbered canceled spelling migration * fix(db): renumber the canceled contract migration to 000158 000153 now belongs to marketplace_listing_settlements on main (000154-000157 are in flight in #1239 and #1241). Pure renames of the up, down and test files, plus the identifier and comment number fixes. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(db): slim the 000158 canceled contract Only normalize 'cancelled' to 'canceled' on purchase_executions and ri_exchange_history and narrow both status CHECKs. The mirror trigger, the cancelled_by drain and the down backfill are cut: this release keeps reading COALESCE(canceled_by, cancelled_by), so nothing reads canceled_by alone yet. The row-level updated_at triggers (000128 and 000009) are disabled around the normalization so the plan-health and retention clock is not restamped. The down only re-widens the CHECKs. The old test is removed; the new 000158 test follows. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(db): cover the 000158 canceled contract migration Seeds legacy 'cancelled' rows in both tables with a 90-day-old updated_at, then asserts the normalization, that updated_at is unchanged on each table (each assertion fails without its trigger disable), that both triggers are enabled again, the narrowed CHECKs (23514 on the legacy spelling), and that the down restores the widened CHECK text exactly. Also runs the write statements of the build live today (the SavePurchaseExecutionTx upsert, SetCancelledBy, the transitionExecutionStatus dual write and CancelAllPendingExchanges) against the narrowed schema: no 23514, and the actor reads back via COALESCE(canceled_by, cancelled_by). Written relative to the version below 158, so it follows the tree as 154-157 land. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): keep tolerant canceled reads The PR removed every tolerant read of the legacy spelling, which is only safe once 000158 has run and no older build can be rolled back to. Keep them until the follow-up: LegacyStatusCanceled and the lists and switches that use it, COALESCE(canceled_by, cancelled_by) in every execution projection, and status IN ('cancelled', 'canceled') in the cleanup SQL. Their tests (the legacy-spelling fixtures) are restored. The PR's write changes stay: SavePurchaseExecution writes canceled_by = COALESCE(purchase_executions.canceled_by, $17) and no longer writes cancelled_by, and the cancel transition and SetCancelledBy write canceled_by only. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): pin cancel actor reads and writes with pgxmock Two tests. The projections test asserts all ten purchase_executions projections read COALESCE(canceled_by, cancelled_by), so a legacy actor on cancelled_by still scans back. The writes test rejects any INSERT or UPDATE that names the legacy column in its write part (RETURNING reads are exempt) and pins SavePurchaseExecution's COALESCE(purchase_executions.canceled_by, $17), so a stale-copy save cannot erase an existing actor. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): run the new code against the pre-contract schema Migrates to the version below 000158 and seeds what an older build wrote: the legacy status spelling and an actor only in cancelled_by. Covers GetExecutionByID, the History page and summary, CountLiveExecutionsForAccount, holdsCapacity, the ramp-step occupied check, SavePurchaseExecution on a legacy-actor row (the actor is carried into canceled_by and a stale save cannot erase it), CleanupOldExecutions, and the handler's cancel-race recovery (cancelOrRecoverExecution). The build and the migration roll out in either order, so both orders must read correctly. Against the original #1253 strict reads the config test fails on the actor scan, the live count, holdsCapacity, the save and the cleanup, and the api test fails with a 409. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: drop legacy-spelling rows from DB-backed fixtures, fix lint On a fully migrated schema 000158 forbids the legacy status spelling, so integration fixtures that INSERT it cannot run. Revert the DB-backed fixtures to the PR's versions (history summary, ladder resolve, ramp baseline, ramp step, ladder reconcile, submit dedupe, the db retention test, the retention writers case), and have the account-delete tests iterate TerminalExecutionStatuses minus the legacy spelling. The unit tests keep their legacy fixtures, and the legacy rows are covered at the pre-000158 schema by the new integration tests. Also reword comments and mark the SQL constants that name the legacy column so the misspell linter passes, and drop a stray blank line. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(known-issues): record the two ramp reads without the legacy cancel actor ListPlanCommittedTx and ListExecutionsForPlanStepTx select canceled_by alone. Pre-existing and decision-neutral (both callers branch on status); the drain in #1747 fixes the rows at the source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(db): renumber the canceled contract migration to 000159 #1762 took 000158. Rename the up, down and test files and update the test names, identifiers and comments that name this migration. The tests still resolve the version just below it dynamically. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(db): renumber the canceled contract migration to 000155 Migration numbers are assigned at merge time: main's highest is now 000154 (first_dispatch_at). Rename the up, down and test files and update the test names, identifiers and comments naming this migration. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: derive the pre-contract cutoff from the migration file The config and api legacy-canceled tests hardcoded the migration number as the cutoff for "the schema just below the contract". Read it from the file ending _contract_canceled_spelling.up.sql instead and fail loudly if it is missing, so a renumber needs no test edit. Checked in a scratch copy with the migration renamed to 000170 and a 000169 present: derived 169, where the old hardcoded cutoff would have picked 154. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): update the pgxmock Save arity after #1762 #1762 added first_dispatch_at as the 30th SavePurchaseExecutionTx argument. The Save SQL on the merged tree still has canceled_by = COALESCE(purchase_executions.canceled_by, $17), and #1762 added no cancelled_by writer or bare reader. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): pin that a stale save cannot replace the stored cancel actor A stale copy carrying a different non-nil actor B must not replace the actor A already in canceled_by. Swapping the save to COALESCE($17, purchase_executions.canceled_by) in a scratch copy now fails with expected concurrent@example.com, actual stale-copy@example.com. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(known-issues): use the CONTRIBUTING audit line format Add the resolved count to the audit status of the cancel-actor entry. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(db): renumber the canceled contract migration to 000157 after #1241 #1241 took 000155 and 000156. Rename the up, down and test files and update the test names, identifiers and comments naming this migration. Refs #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Buddy <buddy@users.noreply.github.com> Co-authored-by: Cristian Magherusan-Stanciu <cristi.magherusan@gmail.com>
…n RDS az_config (#408) (#1239) * feat(db): add generated az_config column and 12-column recommendations index (#408) Migration 000140 adds a generated az_config column, read verbatim from payload->'details'->>'az_config' (RDS single-az/multi-az, Azure SQL zone redundancy, '' otherwise), and two unique indexes that include it: the 11-column natural key plus az_config (the new upsert arbiter) and the 000120 source-account key plus az_config. The existing 11-column and 000120 source indexes are kept, so the current upsert keeps working and this can be applied before the store switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(purchase): carry RDS az_config in rec ID, identity key and idempotency key (#408) Single-AZ and Multi-AZ RDS recommendations of the same class, engine, term and payment shared one rec ID and one identity key. Read az_config verbatim from Details (RecommendationRecord.DetailsAZConfig, pinned to the 000140 generated column by shared vectors) and add it to RecIdentityKey, the rec ID (appended only when set, so every other rec keeps its ID), the purchase idempotency key and describeRec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): upsert recommendations on the 12-column index with az_config (#408) Requires migration 000140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(db): drop the recommendations indexes without az_config (#408) Migration 000141 drops the 11-column natural key and the 000120 source index, both superseded by the 000140 indexes that add az_config. While either exists, a collection batch carrying a Single-AZ and a Multi-AZ RDS recommendation of the same class fails as a whole. The rollback collapses az_config-only duplicates to the newest row and recreates both. Apply only after the 12-column upsert is live everywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(deps): bump golang.org/x/net to v0.62.0 (GO-2026-6860, GO-2026-6866) govulncheck in the Docker image build now fails on these two advisories, both fixed in x/net v0.62.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(recommendations): scope az_config identity to AWS RDS (#408) Azure SQL's az_config is a subscription-wide dominant value that is empty when the server lookup is incomplete, so keying the rec ID, identity key and purchase fingerprint on it flipped Azure SQL IDs between collections for no benefit. Add IdentityAZConfig, which returns the Details az_config for AWS RDS and "" for every other rec, and use it in those three places. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(db): add az_config to purchase_suppressions variant (#408) Add a nullable az_config column and widen the per-execution UNIQUE key so a Single-AZ and a Multi-AZ RDS suppression of one execution can coexist. Backfill rule: NULL az_config means unknown or not applicable and is a wildcard, so every existing row keeps suppressing both offerings, failing safe against a double purchase. Down merges rows that differ only by az_config (summed count, latest expiry) before restoring the 000134 key. Apply as its own deploy step, before the code that writes the column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(suppressions): key purchase suppressions on RDS az_config (#408) Migration 000155 lets Single-AZ and Multi-AZ RDS recs of one class coexist, but the suppression variant lacked az_config, so buying 2x Single-AZ wrote a suppression that also matched, and hid, the Multi-AZ rec (an under-buy). SuppressionVariant now carries AZConfig, set for AWS RDS recs only, and is stored in the new purchase_suppressions.az_config column. An empty AZConfig is a wildcard on either side: suppressions written before migration 000156 and RDS recs without an az_config keep matching both offerings. Regression: TestApplySuppressions_RDSAZConfigScopesToPurchasedOffering failed before (Multi-AZ count 3, want 5) and passes now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(recommendations): scope generated az_config to AWS RDS The az_config column was derived for every provider while the rec ID, identity key and fingerprint only carry it for AWS RDS. A cron collect and a refresh overlapping on an Azure SQL rec could commit it with az_config 'none' and '', the 12-column upsert key inserted a second row and the eviction spared it, so the rec was listed twice and could be bought twice. The column is now '' outside provider aws with service rds or relational-db, mirroring IdentityAZConfig. Edited in 000154 in place: it is unmerged and 000155/000156 do not reference the expression. Refs #408 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(scheduler): cover Azure SQL zoneRedundant to none flip in suppressions Refs #408 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrations): order both az_config expand steps before the contract The suppression az_config migration sorted after the contract migration that drops the 11-column recommendations key, but the contract can only run after the release. New code on a database at the last pre-contract version failed on purchase suppression writes and listing. Renumber so both expands run first: 000154 recommendations az_config, 000155 purchase_suppressions az_config, 000156 contract. Update the cross-references in migration comments and store comments, and add an integration test that runs the new code against a database stopped just below the contract and again after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(config): gate az_config code on the expand migrations New code that writes az_config failed mid-transaction on a database without migrations 000154 and 000155. Generalise checkAzurePricingMigration into checkSchemaVersion and add CheckAZConfigMigration, which returns the typed ErrAZConfigMigrationNotReady naming the missing migrations. The purchase path refuses with a 503 before it opens its transaction. Recommendation writes and suppression listing return the same error, wrapped with what was skipped, instead of a SQL failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): gate suppression writes on the az_config expand CreateSuppressionTx inserts az_config, so every caller (the plan step claim, attach recommendations, CreateSuppression) failed with a raw SQL error below migration 000155. Check the schema version at the top of CreateSuppressionTx and return the typed ErrAZConfigMigrationNotReady, so the transaction rolls back cleanly for all callers. The handler keeps its pre-transaction 503. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(purchase): migrate the Azure pricing fixture past the az_config expand The fixture stopped at 000154, but the recommendations upsert is now gated until the 000155 expand, so the re-collection step was refused. Migrate to 000155 and say why in the comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): run the suppression-write schema check on the caller's transaction CreateSuppressionTx queried the pool for the az_config schema version while the caller's transaction was open, taking a second pool connection per suppression row. Under the plan-step claim's advisory and row locks and a pool of 3 that can exhaust the pool. Take the check through a rowQuerier satisfied by both the pool and pgx.Tx, and run it on the tx. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(db): renumber the az_config migrations to 000155-000157 after #1762 Main now has 000154_purchase_executions_first_dispatch_at. Move the recommendations az_config expand to 000155, the purchase_suppressions expand to 000156 and the contract to 000157, and update every reference: SQL headers, store comments, fixtures and gate test versions. The gate error and 503 message now build the migration numbers from two constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(api): answer the recommendations list with a 503 below the az_config expand The list returned a generic 500 when the az_config migrations were missing. Map ErrAZConfigMigrationNotReady to the same 503 the purchase paths return, through one shared helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(db): renumber the az_config migrations to 000157-000159 after #1241 Main now has 000155 and 000156 from #1241. Move the recommendations az_config expand to 000157, the purchase_suppressions expand to 000158 and the contract to 000159, with the gate constants, error text, fixtures and comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(db): renumber the az_config migrations to 000158-000160 #1253 took 000157 on main. Number-only: the files, the gate constants, the message checks, the migration-version literals and comments move by one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Buddy <buddy@users.noreply.github.com> Co-authored-by: Cristian Magherusan-Stanciu <cristi.magherusan@gmail.com>
Part of #702. This is PR1 of 4 for the Redshift re-drive idempotency work. It does not close the issue.
Why
The go library dedupes a Redshift reserved-node buy with
DescribeReservedNodes. It looks for a node that started no earlier thanRequestedAtminus 5 minutes (cloud-commitments-go#509). Since #1138, the platform passes the current time at each dispatch. A re-drive of the same lineage would therefore search a later window and could miss a node the first attempt bought. Redshift re-drive is refused today for exactly this reason.This PR persists the time of the first dispatch and passes that time instead, so every attempt of a lineage gets the same window start. Re-drive and retry stay refused here. PR2 adds the cross-lineage conflict check, PR3a the Retry gates, and PR3b lifts
awsPurchaseHasNoDuplicateGuardfor Redshift.What
feat(config): migration 000154 adds a nullablepurchase_executions.first_dispatch_at.PurchaseExecution.FirstDispatchAtis server-only (json:"-").DO UPDATE SET.feat(config):StampFirstDispatchTxstamps the claimedrunningrow inside the caller's transaction.now(), andCOALESCEkeeps an existing stamp.no_submissionat attempt 0.fix(purchase):executeAndFinalizestamps an execution that has a selected Redshift rec, after the pre-execution refusals and before the provider call. The stamp becomesPurchaseOptions.RequestedAt.Review
Deploy order
auto_migrate=false): apply migration000154_purchase_executions_first_dispatch_atBEFORE deploying this code. Never down-migrate it while this code is live. Every full-rowpurchase_executionsread and insert names the new column. Code running without it fails loud with 42703 (no execution loads, nothing is bought) until the migration runs, the same as thecreated_by_systemprecedent. Old code against the migrated schema is safe, since the column is additive and nullable.internal/server/app.go), so no manual step is needed.Migration number
000154, the next free number on main at merge time, under the merge-time numbering policy. The rename is the final commit and is rename-only: the migration SQL is unchanged, and its test and one comment follow the number. Migrations apply in order, so a later PR must take a higher number.
Owner decisions
These are on #702, with fail-closed defaults. None affects this PR's behaviour: the 24h horizon is computed but not yet enforced.
Tests
Run on the rebased head:
make build,make lint;go test ./internal/...;go test -tags=integration ./internal/config/... ./internal/database/postgres/migrations/... ./internal/purchase/...(Postgres testcontainers).The only lint finding is the existing
preallocatinternal/config/store_postgres_history_page_integration_test.go:141, which is unchanged from main.🤖 Generated with Claude Code