10 Commits
Author SHA1 Message Date
cgalo5758 6f65c744fe Pin how the plan-wide acts interleave with each other, with a rule change and with a conferral
The plan-wide acts (tier removal, default change, reorder, tier add and
delete) are now sent against each other and against a conferral, each
interleaving chosen by a lock a side transaction holds and releases once
the requests wait on it. dbtest.AwaitLockWait proves a request is queued,
and PostgreSQL grants a row lock to its waiters in the order they queued,
so no test sleeps. The kit gains Backend.Waiters, how many backends must
wait at once, with a test of its own; its first caller is these tests.

TestTierRemovalAgainstDefaultChange runs a removal and a default change
on one pool in each order: queued on the pool's row, and one after the
other. Every case ends with no live position on a deleted tier.
TestDefaultChangeTwiceSerializesOnThePool sends one default change twice
and shows one request grandfathering the pool and the other finding the
legacy grant the first wrote. TestPlanActsRefuseWhileARuleChangeHoldsTheRendezvous
holds the exclusive rendezvous while all five acts run, and pins each
act's contention answer: the removal's and the organization's "Another
entitlement set change is in progress. Try again." and the generic
sentence of the reorder, the add and the delete. It waits out the 5 s
lock timeout once. TestRenumberingCommitsDuringAConferral commits a
reorder, a tier delete and a tier removal while a default change's
conferral waits between its rank reads; the files pin the transition as
it is recorded today (upgrade 0 to 1 for the reorder where a transfer
belongs; downgrade 1 to 0 for the delete and the removal), because a
renumbering takes the rendezvous in shared mode only. Pinned with #187:
TestReorderEnactsAgainstTheRankZeroItSaved sends two reorders whose
enactments queue on one pool; the first enacts against the rank 0 it
saved after the second has put the other product back at rank 0, so it
grandfathers the holder onto a legacy grant of the product that is rank 0
again, and the second finds the holder on that grant and leaves it.

No output changed: the commit adds tests, their golden files and one field
of the test kit, and touches no production file. The names and results of
./internal/server and ./internal/testkit/..., run after a database reset at
the parent and at this commit, differ by the 13 new tests and subtests, all
passing; the new tests passed three runs in a row and 20 runs of the four
that do not wait out the timeout. The edit to the plan-wide acts' golden
test is its JSON encoder, split so the new tests encode their files the same
way; its 44 files are unchanged. The kit's wait.go and wait_test.go and the
AwaitLockWait bullet of docs/testing.md change beside it. 14 breaks, each
caught by its new test (new side only): 12 edit production code unchanged
from the parent and 2 edit this commit's wait.go. The section's tests take
about 6 s, 5 s of it the one wait.

Unprobed:
- Interleavings the tests do not choose, such as a removal that arrives
  between the default change's save and its pool lock; the cases cover the
  two queue orders on the pool and the two serial orders.
- A conferral other than the default change's during a renumbering (a
  reconcile, a grant act, the floor); it is the same database function,
  which the reorder case reaches.
- The default change's own stale candidate: a renumbering between its save
  and its enactment. It uses the same fixed product the reorder case
  reaches; no case pauses the default change there.
- The two new failure messages of AwaitLockWait for a count above one;
  they are text, read only when a wait fails.
2026-10-10 16:42:02 -05:00
cgalo5758 a36003910f Note each Stripe call among the statements around it
A test can now put a line of its own among the statements a Counted
handle recorded, and have the Stripe mock hand it every call it
receives, so one list shows each Stripe call between the statements it
ran between, begin and commit included.

Statements.Note places a line after the statements recorded so far.
Names returns statements and notes in order; Total and ByName leave
notes out; Reset forgets them. MockBackend.OnCall is called for every
request as soon as it is recorded, before Err, PathErrs or Handle
answers it, so a refused request is seen too. The recording moved from
Call into a small record method, which keeps (*MockBackend).Call at its
cognitive complexity of 65. stripetest.Line prints a call as
"stripe <METHOD> <path>" and its form's pairs sorted by key, unescaped.
docs/testing.md names OnCall, Line and Note.

No production code changes, so no output changes: only test helper
packages and docs/testing.md are edited, and the build, vet, gofmt, lint
and deadcode checks pass with the lint snapshot unchanged. The names and
results of ./internal/testkit/..., ./internal/stripetest,
./internal/fulfillment, ./internal/integrations/stripe/... and
./internal/server equal the parent's (1767 at the parent, 1771 here;
every result pass), the four additions being the new tests
TestLineNamesTheCallAndItsSortedForm, TestOnCallSeesEveryCallInOrder,
TestOnCallSeesARequestErrRefuses and
TestNotesTakeTheirPlaceAmongTheStatements, after a database reset before
each side. Eleven breaks, each caught by the new test.

Not probed: OnCall called from concurrent goroutines. The mock appends
under its mutex and calls OnCall outside it, and no test of this series
calls Stripe from two goroutines at once.
2026-10-10 14:50:48 -05:00
cgalo5758 ff04f2ee86 Print a query's rows as text for a golden file
dbtest.Q.Rows runs a query and returns a line of column names, then one line
per row in the query's order, values separated by " | ". NULL reads NULL, a
time reads in UTC as RFC 3339 with its fraction, bytes read as text, anything
else as fmt prints it. The query names its own order. docs/testing.md lists
Rows with the Q methods.

No production file changes. Shown to change no output: the tests of
./internal/testkit/... listed by name and result before and after differ by
the one new test, TestRowsPrintsColumnsThenRowsInQueryOrder, and every test
passes; the kit's imports guard still passes, and the lint snapshot has no
added or removed line.

5 breaks, each caught by the new test (a new test is proven on the new side
only); none dropped.

Unprobed: how the driver hands over each column type (uuid, numeric and json
arrive as text or bytes, both printed as text; which one a type takes is the
driver's choice, so the test does not name it), and the error branches (each
fails the test with the whole query folded onto one line, as Strings does).
2026-10-10 14:50:48 -05:00
cgalo5758 dd38fce93a Name the statements a Counted handle ran, in the order they ran
Statements.Names returns a copy of the recorded names in run order. A
transaction's begin, commit and rollback are among them, because pgx sends
them as statements the tracer sees, so a test can read where each statement
fell against the transactions around it. docs/testing.md lists it beside
ByName, Total and Reset.

No production file changes. The test packages under internal/testkit run
with the same test and subtest names and results at the parent and at this
commit, apart from the new TestCountedNamesStatementsInOrder (37 results
before, 38 after, each run after a reset of the test databases).

4 breaks, each caught by the new test: sorted order, a dropped first name,
the shared slice returned instead of a copy, and Reset leaving the names.

Unprobed: the mutex, because every caller records on one goroutine. That
rollback is recorded is pgx's behaviour; the test asserts it, but no
one-line edit of the kit removes it, so no break covers it.
2026-10-10 14:04:38 -05:00
cgalo5758 2fd63eaca2 Characterize the recent-activity feed: its rows, its failed reads, its reads per event and the landing's paging
No production file changes. The feed's output is unchanged because nothing
the page runs was edited; the commit adds tests, one kit function and the
files the golden compares with.

TestRecentActivityGolden builds nine organizations on one World, for ten
cases (the floor, an upgrade and a revocation on a plan the type defaults
to; an upgrade by a subscription that then ends and resumes the grant; a
live extension; an extension of a grant on hold; a ladder of its own; a
reorder after the tier changes; the console's own removal of a tier, which
writes the only end row with no rank; invoices and payments around one
instant; two organizations with one invoice each at one instant). It
compares the events the feed lists for them, in order, as JSON, and the
activity panel's render of those events, with files under
testdata/operator_activity. Instants are named t01 and on, newest first,
because only their order is a fact. The files were read against the fixture
before they were committed. The feed's four existing readers now go through
wholeFeed.

TestRecentActivityReadFailures fails each of ten reads in turn (the four
sources, the product, the tiers, the organization, the person, a handler
without a database and a closed one) and pins the events the feed keeps and
the warning it writes. TestRecentActivityReadsPerEvent grows the feed by two
organizations on a scratch database and expects exactly the product,
ladder, organization and person reads of two more of each, and no change in
the four source reads or the raw ladder read. The landing page tests read
GET /operator on a scratch database: each source is cut at 100 rows (the
101st invoice is on no page and the pager counts the 101 events the feed
holds), and a feed of 30 events pages by act_page and act_per, with a page
past the end showing page 1 and an unknown size reading as 10.
dbtest.CountedAt is Counted over a given address.

Shown to change no output: 33 breaks of the feed's code, each applied on its
own, are all caught by the new tests; the tests of ./internal/server and
./internal/testkit/... pass three times in a row, each on a reset database,
with no test or subtest of the parent missing or changed in result and 19
added in ./internal/server (the golden, the failure table and its ten
subtests, the reads test, the cut test, the paging test and its four
subtests); the golden passes a second run without -update and leaves the
tree unchanged.

Unprobed, and why:
- Each source is read once per render: no one-line edit doubles a source
  read without also changing the rows.
- The order of two warnings when two sources fail: no subtest fails two
  reads.
- The When cells: the golden replaces them with tokens, and the test of
  billing events' times pins their text.
- The template's id fallback for an unnamed organization or actor: the
  overview render tests, which build their events by hand, pin it.
- An extension older than the transitions window shows as its grant row:
  pinning it needs about 100 later tier changes.
2026-10-09 21:23:00 -05:00
cgalo5758 2790c6bbc7 Count the statements a request runs, and pin the products page at none per row
dbtest.Counted opens the test database on a handle that records the name of
every statement run on it (a generated query by its query name, anything else
by its first line), with Reset, Total, ByName and Diff. A new table test,
TestOperatorPagesReadAFixedNumberOfTimes, grows a page by one row, then by two
more, and asserts that the second request runs 2*perRow more statements than
the first. Its first row is the products page at 0 per row, which holds today.

No production file changes, so no output changes. Tests of internal/testkit/...
and internal/server pass on a reset database, and the read-count test passes
three runs in a row. New tests: TestCountedRecordsEachStatementByName and
TestDiffListsTheNamesWhoseCountsDiffer (dbtest), and
TestOperatorPagesReadAFixedNumberOfTimes with its products subtest (server);
every other name and result equals the parent's. 8 breaks, each caught by the
read-count test (new side only).

Unprobed: the mutex in Statements (every handler under test runs its
statements on the request's goroutine, so nothing records concurrently);
pings and prepares going unrecorded (a ping happens only after a second of
idleness, and nothing in the module prepares a statement); Counted failing on
a database behind the migrations (ledgerBehind, which the Open test pins).
2026-10-09 15:58:51 -05:00
cgalo5758 f8f9551ddd Build the FedWiki tests' fixtures through the kit
Adds internal/testkit/world (New, Providers, Person, Org, Workspace,
Product, Rule, Confer, Person.SignIn), internal/testkit/authtest
(NewSessionManager, SignIn) and internal/integrations/fedwiki/fedwikitest
(the environment, tenant, limit and usage, operator root, site rows), and
moves the FedWiki fixtures onto them: newReqEnv, setupQuotaFixture,
newReconcileTestEnv, siblingWorkspace, governFedwikiLimit (Product, Rule,
Confer, no materialization), newSiteEnv, newClaimTestDB (its raw provider
insert goes), seedOperatorSites, newMemberWorkspace, seedUnderstatedQuota,
newTestWorkspace, seedCountedWorkspace and addSiblingWorkspace.
newReconcileDomainsDB and setupReconcileRoot had nothing left to move. The
six sessions the FedWiki web tests build go through authtest. The import
guard covers the three packages, and scripts/deadcode-testonly.txt lists
them.

The six hand-written lock-wait loops become dbtest.AwaitLockWait
(lifecycle_db_test.go, registration_db_test.go, invoice_reconcile_test.go,
stamp_db_test.go, and the two record-swap tests in requests_db_test.go and
swap_record_db_test.go). Backend gains BlockedBy, Query and Abort. The
registration loop is not a verbatim move: its select matches by query text
within the current database and its done check becomes the Abort probe, as
do the two record-swap loops, each keeping its message ("registration
finished (%v) while the first key was held", "the record finished without
writing the activated site's status: %v", "the record finished without
waiting on the parked request: %v"). Kept weaker on purpose: the
never-waited messages are now the helper's, which names the selection
("backend N never waited on a lock" for the pid loops, "no backend waited on
a lock held by backend N" for the record swaps, "no backend running a
statement containing %q waited on a lock" for the registration and invoice
loops), the record-swap and stamp bounds are the helper's ten seconds (were
the tests' thirty), the stamp loop accepts any waiting count (was exactly
one for its pid), and the invoice loop matches the statement text anywhere
in the query (was its prefix). TestDispatchStampsLockRecordsInOneOrder drops
from cognitive complexity 39 to 31; its pin is re-pinned to 31.

Names and results of the three FedWiki packages, internal/integration,
internal/workflows/desiredstate and internal/integrations/stripe/workflows
equal the parent's apart from the new tests (the import guard's forbidden
list with its three subtests, and the lock helper's selection test with its
three subtests and its Abort test) and one change: the three
TestReconcileDomains* tests in internal/integrations/fedwiki now pass on a
freshly reset database. They failed there at the parent, alone and in the
package run, because nothing had registered FedWiki; the fixture now does
(#33).

Fixture equivalence (each fixture run alone on a reset database, every table
of core, fedwiki and domains dumped before and after). Differences, none
read by an assertion: names and addresses carry the builder's shape
(organization "<name> <suffix>", email "<name>-<suffix>@example.test",
subject "sub-<name>-<suffix>"; the display name is the label for tenant
fixtures); the pool is the organization's default pool (name "Default", key
default, auto-managed) where the fixtures wrote a bare one; a workspace now
sits in the organization's default pool through a pool assignment where
newMemberWorkspace, seedOperatorSites and the claim fixtures wrote neither
(seedOperatorSites gains two pools and two assignments); every FedWiki
fixture registers FedWiki and the System tenant, which adds the provider row
(or fills operator_surface_path on the row a claim fixture inserted), the
operations, connection, state and outbox rows, the System tenant's user,
person, 'system' organization type, organization and workspace, and
refreshes the fedwiki_sites resource key; archived sites carry archived_at,
as the status query sets it; sites are minted under a database-minted id;
governing a limit goes through the rule-change function, which adds a
set-change row. Claim tokens are random in both.

25 breaks: 19 failed the old test at the parent and the new test here (one
code break per FedWiki test file in the function it pins, and one per
converted lock wait with the lock removed; the three loops with an Abort
probe fail through it with their own message), and six on the kit alone
failed the new test. No assertion in a test body changed.
2026-10-08 02:43:55 -05:00
cgalo5758 7c1867f0ab Lock pools through one helper
Queries.LockPools copies its ids, sorts them, removes repeats and runs
LockResourcePool once per id, returning the ids in that order with the
driver's error unwrapped; a missing pool is sql.ErrNoRows.
Queries.LockPoolOfGrant wraps LockGrantPool. No production Go file outside
internal/entitlements/tx.go and the generated queries now takes a pool row
lock.

Converted: RevokeGrantTx, ManageGrantTx, ResumeReplacedGrantsTx (q is created
before the lock) and enactOrgDisposition (q is created before the lock) from
the raw SELECT 1 ... FOR UPDATE; settlePool from LockResourcePool;
ExpireGrantTx and ExtendWaitingGrantTx from LockGrantPool to LockPoolOfGrant.
Each keeps its error wrap or dispositionFailure copy. CommitPlanLadderTierRemoval
passes the holders' pool ids to LockPools and walks the slice it returns in
place of the map dedupe, sort.Strings and lock loop; its refusal copy is
unchanged, and its gocognit pin goes from 61 to the measured 57. The raw
statement selected 1 and the helper selects pool_id; the row lock is the
same. The lockPool helper in concurrency_test.go is deleted; its three
callers use LockPools.

dbtest.AwaitLockWait(tb, d, Backend{PID}) polls pg_stat_activity until that
backend waits on a lock; its first caller is the new
TestLockPoolsTakesAscendingOrder, which holds a pool, asks a second
transaction for (high, low, high), and checks that a third connection is
refused the low pool with 55P03 while the second waits, that the ids come
back sorted and deduplicated, and that a missing pool is sql.ErrNoRows.

Breaks, six records, each caught: LockPools skips the sort, LockPools locks
nothing (both fail the new test); LockPoolOfGrant returns an empty id
without calling the query (TestExpireGrantTx and the four
TestExpireGrantActivity_ tests fail with "resume replaced grants: lock
pool" on the new commit, and on the parent when ExpireGrantTx's
LockGrantPool call is replaced the same way). No test name or result
changes apart from the new test.
2026-10-07 17:33:00 -05:00
cgalo5758 11e0f6ee86 Run the migration tests on scratch databases
dbtest gains UpSQL, ApplyVersion, SimpleProtocol, Tx, On and Q (Exec, Scan,
ID, Int, String, Strings, Attempt, Refuse), WantPgError, Name and the
SQLState constants. internal/db's mustExec, mustScan, mustStrings,
deliveryTx, attempt, mustRefuse, assertPgError, the sqlstate constants and
deliveryName become their dbtest counterparts at every caller; the fx
builders' id scans become Q.ID. The fx builders and the helpers built on
them lose their unused ctx parameter. The two tests that open one
transaction in the parent (TestProviderConnectionConstraints,
TestDeliverySchemaFacts) rebind it in each subtest that runs a statement
with dbtest.On(t, q.Tx()).

TestTaxonomyMigrationBuckets runs on a scratch database through two
connections (simple protocol to apply, extended to assert) instead of a
docker container; its checks are tables of (query, want) in three named
phases, and its gocognit pin is deleted.
TestIntegrationDeliveryMigrationUpDownUp uses Scratch, UpTo and
ApplyVersion; TestInvoiceIssuedPaymentPaidBackfill replays 00022 through
UpSQL on a simple-protocol transaction; the reader tests in internal/migrate
use Tx; TestFullMigrationSequence and the delivery benchmark migrate a
scratch database with migrate.Sources().

The delivery benchmark's skip text is unified with the openers'. No test is
added or renamed. 13 breaks, each failed the old and the new test: the 11 on
the migration subjects, one on a WantPgError caller and one on a Q.Refuse
caller.
2026-10-07 16:22:18 -05:00
cgalo5758 d1ac278e6b Open every test database through dbtest
internal/testkit/dbtest opens TEST_DATABASE_URL and E2E_DATABASE_URL, never
migrates, and fails with "the test database is behind; run
test/reset-test-db.sh" when the core ledger lacks the highest version of
db.BaseSources(). The opener helpers except the scratch-database and
container openers (26 of the 29) and the inline openers of the database
tests call it; helpers that did more than open (providers, fake farms, the
FedWiki provider row, cleanup of override rows) keep that setup. The
stripe/workflows and workflows/entitlements openers keep their search_path
through WithSearchPath. The closed-database seam tests use Closed.

TestMigration18SetsExistingFedwikiRulesToForceReduce runs on a scratch
database through Scratch and UpTo instead of rolling the shared core ledger
back through goose's global state, because no opener migrates the shared
database any more.

The in-package forms tests of internal/server reached the integrations'
forms only because an external test file imported internal/migrate; a test
file now links internal/integrations explicitly.

TestRegisterProviders_DB is re-pinned from gocognit 37 to 34.

New tests: TestLedgerCheckRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase
(dbtest), TestOpenRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase (dbtest,
through opened, the path Open and OpenE2E share),
TestKitPackagesReachOnlyTheirAllowedModulePackages (testkit). The skip
messages of the openers are unified to "TEST_DATABASE_URL not set, skipping
integration test". 4 breaks: Open skipping its ledger check and the ledger
check accepting an empty database fail the new tests; a kit row that no
longer allows internal/testkit fails the import guard; leaving the fedwiki
rules as they were in migration 00018 fails the migration 18 test on both
sides.
2026-10-07 16:22:18 -05:00