Commit Graph
48 Commits
Author SHA1 Message Date
cgalo5758 8c0fb4ae42 Archive the suspended-plans and reconcile pool-lock changes
reconcile-takes-pool-lock (#193) and suspended-plans-stay-held (#148)
move to openspec/changes/archive/ with every task ticked, and their
deltas are synced into the main specs:

- plan-transitions: 1 requirement modified (#193).
- member-dashboard, member-entitlement-view, member-offer: 1 modified
  each; member-plan-upgrade: 1 renamed ("active" to "live" ladder
  attachments) and 1 modified; member-invoice-history: 2 added;
  member-product-discovery, stripe-subscription-creation: 1 added each;
  plan-enrollment-administration: 1 added, 3 modified;
  stripe-subscription-management: 1 added, 2 modified (#148).

What #148's research and sandbox check taught, written into the pages
that own each topic:

- docs/models/plan-ladders-transitions.md: the member position's readers,
  all eight, each holding a suspended position as the organization's plan.
- docs/models/payments-billing.md: pausing collection is not the paused
  status, so the plan keeps delivering.
- docs/stripe.md: an expired pending upgrade's invoice is voided; a
  period-end cancellation stops retrying only that subscription's open
  invoice; only a subscription made outside the console reaches paused;
  paying on Stripe's page leaves the subscription's own default card in
  place; an uncollectible invoice's page still takes payment.
- docs/testing.md: what a break record's wanted message must be, and how
  to drive a failed renewal in the Stripe sandbox.
- status/design-feedback.md: the data model delivers through a past_due
  grace period, the console suspends at once (#255).
- The archived design gains three reasons: why the immediate-cancel
  refusal comes first, the https-only redirect, and the paused badge's
  tone.
2026-10-26 15:45:27 -05:00
cgalo5758 715404500a Archive the runtime database user and the boot that serves first
serve-before-dependencies (#141) and runtime-database-user (#23) move to
the archive, and their deltas are synced into openspec/specs. The two
Purpose edits a delta cannot make are made here: temporal-connection's
Purpose says start connects in the background and retries for the life
of the process, and error-pages' names the 503.

What the two investigations learned beyond each decision moves into the
docs:

- go-conventions.md: a new section on code that reaches a service that
  answers after the listener opens (holders, per-request reads, the
  refused write, retryloop, viper writes before the listener).
- production-deployment.md: what a liveness probe requests, and why it
  must not wait on Temporal or the identity provider.
- building-an-integration.md: declared paths skip the session layer;
  a completed workflow id runs again, and the guarded first update.
- models/payments-billing.md: invariant 14 states the 503 and
  Discourse's failed rows; invariant 15 states the guarded update.
- operator-ux-conventions.md: an error answer carries no HX- header.
- database-management.md: why writers hold only the four DML
  privileges, identity columns and sequences, the qualified SQL the
  migration user runs, and sqlc alias lists for set-returning functions.
- database-locks.md: rule 9, a row lock on a table the writer cannot
  update is taken in a SECURITY DEFINER function.
- testing.md: privilege tests drop to console_runtime, race-only breaks
  as command records, the race run's -p 1, synctest for timer loops,
  and test workers off the console's task queue.

Both archived design.md files gain the options weighed and the drafts
the approved copy replaced.
2026-10-24 02:00:58 -04:00
cgalo5758 29520a8120 Set the Stripe key once at boot and read the boot mode everywhere
First section of #141. start assigns the stripe-go key through UseKey,
directly after it derives the key's mode and fingerprint and before any
goroutine starts; the Stripe integration's Startup no longer assigns it,
and nothing else does. UseKey assigns the trimmed key, so the SDK sends
the key whose mode and fingerprint boot derived, and an empty key leaves
the SDK's key as it was.

Every surface that asked a function for the key mode now reads the mode
boot derived: Checkout, the plan moves and the eager reconcile in
internal/server, the fulfillment entry points (SwitchPlan,
KeepCurrentPlan, CancelSubscription, the two reconciles and the
scheduled-change sweep), and the worker's billing activities. The Stripe
workflows' activities still read the SDK's key, which start set before
the worker existed. SDKKeyMode, Config.StripeKeyMode and
WorkerConfig.StripeKeyMode are gone. stripe-api-key is a secret, which
an operator override cannot replace, so the value boot reads is the one
Startup used to assign.

One behaviour changes until the next section lands: with no
temporal-host, the SDK now holds the key, so checkout sessions and plan
moves reach Stripe while the webhooks they cause are stored and never
processed. The next section refuses that configuration; the two are not
deployed apart.

The tests pass the boot mode, stripetest.KeyMode(), where they passed
the function. TestCheckoutThroughTheRouter lost its count of how often
the mode function was asked, since the function no longer exists, and
gained a row that posts a price mapped in live mode under the test key
and wants 422, which fails if the router hands Checkout an empty mode.
The eager reconcile's wiring is covered by no break.

Race baseline (go test -race -p 1 -count=1 over cmd, auth, server,
workflows and the three integrations, on bdd042bc after
reset-test-db.sh): 19 packages pass, no data race. An earlier run
without -p 1 failed two desiredstate relay tests on counts, because the
packages ran in parallel over one test database; the race gates now run
with -p 1 after reset-test-db.sh, as make test does.

Full suite on reset databases: 4373 pass, 15 skip, none failed. make lint passes. Breaks,
each caught:
- UseKey assigns an empty key
- Startup assigns stripego.Key from the configuration again
- the checkout handler compares against an empty mode
- SwitchPlan reads an empty mode
- SweepDueScheduledChanges passes an empty mode to the change it fires
- UseKey assigns the untrimmed key
- the router hands Checkout an empty mode
2026-10-23 13:39:27 -04:00
cgalo5758 f9c9b3225e Run the console on a runtime user and migrate on a second connection string
Fourth section of #23. db-dsn now names the console's runtime user, and
the new db-migrate-dsn (or db-migrate-dsn-file) names the user that runs
migrations. start opens both, migrates on the migration connection,
grants console_runtime to the runtime user there (WITH INHERIT TRUE, the
name quoted by the database), closes it, and serves on the runtime pool
alone. With db-migrate-dsn left out, start runs no migration and refuses
when the schema is behind its code, which is how a host runs migrations
as a separate step; migrate up, down and status use the migration string
and refuse without it, with no fallback to db-dsn.

Both pools are opened from one pgx.ParseConfig: the migration pool's
search_path is public and both pools' role is none, set in the startup
parameters after every key of either name is removed case-insensitively,
so neither a setting the database or role carries nor a key the string
repeats in another case moves a session off its login or its path.
PendingMigrations reads each stream's ledger by qualified name inside a
transaction pinned to search_path pg_catalog, pg_temp.

The test stack runs on two users: postgres-seed creates
member_console_runtime in new and existing volumes, bootstrap-stack.sh
writes MC_DB_DSN as that user and MC_DB_MIGRATE_DSN as member_console,
and the four scripts refuse a .env without the migration string, with
the command that regenerates it. Scripts and fixtures that need a
superuser on the application database (the reset, the screens'
timestamp pinning) use the migration string.

This worktree's stack was regenerated with that command and the console
booted as member_console_runtime: its sessions were the runtime user's
only, no migration-user session stayed open, and the runtime user could
not insert into core.pool_provisions.

Full suite on reset databases: 4277 pass, 15 skip, none failed. The
walkthroughs' names, results and skip messages match the previous
section's run. make screens lists three changed screens, all data:
regenerated ids on operator-person-detail and
operator-billing-invoice-detail (the demo snapshot was rebuilt) and a
domain's age on operator-domains (294d to 295d); the baseline was not
accepted. make lint passes.

Deliberate breaks, scripts/mutation-check -side new, all CAUGHT (36):
  dbRuntime runs the migrations on the runtime pool
  dbRuntime skips the boot grant
  dbRuntime leaves the migration pool open
  dbRuntime skips the pending check when db-migrate-dsn is unset
  dbRuntime migrates on db-dsn when db-migrate-dsn is unset
  GrantRuntimeRole without WITH INHERIT TRUE
  GrantRuntimeRole with %s in place of %I
  PendingMigrations takes a missing ledger as current
  PendingMigrations resolves the ledger without the public. qualifier
  the migration pool opened without search_path=public
  the migration pool opened without role=none
  the runtime pool opened without role=none
  ConnectMigration keeps the search_path and role the string carries
  dbMigration falls back to db-dsn
  ValidateStart accepts a malformed db-migrate-dsn
  db-migrate-dsn-file is not a database secret pair
  db-migrate-dsn-file is not a database secret pair (the migrate commands read db-dsn-file only)
  the bootstrap guard removed
  reset-test-db.sh guard removed
  seed-demo.sh guard removed
  reset-app-db.sh guard after its first psql
  startup parameters match their keys case-sensitively
  the migration pool opened without role=none (keys and options in the string)
  the runtime pool opened without role=none (keys and options in the string)
  the runtime pool replaces the search_path the string carries
  the string's spaces are written as plus signs
  ValidateStart reads db-migrate-dsn as set whatever it holds
  PendingMigrations lists only the versions the ledger shows when it is missing
  migrationDSN keeps whitespace
  migrate up accepts a whitespace-only db-migrate-dsn
  runtimeDSN accepts a whitespace-only db-dsn
  openDB runs the migrating boot for an unknown mode
  bootstrap-stack.sh trusts the exported MC_DB_MIGRATE_DSN
  reset-test-db.sh trusts the exported MC_DB_MIGRATE_DSN
  reset-app-db.sh trusts the exported MC_DB_MIGRATE_DSN
  seed-demo.sh trusts the exported MC_DB_MIGRATE_DSN
2026-10-23 08:01:14 -04:00
cgalo5758 db1bf7c287 State each spec's purpose, and refuse the archive's placeholder
When `openspec archive` creates a spec, it writes "TBD - created by
archiving change ..." as the spec's Purpose, and `openspec validate
--strict` accepts it. Twenty-five specs still carried it.

Each of the 25 now says what it defines, in one to three sentences
drawn from its own requirements, and each was checked against them.

The lint gains spec-purpose-placeholder: it reports a spec under
openspec/specs/ whose Purpose is the placeholder or empty, at the
placeholder's line, so an archive that creates a spec cannot land
until its Purpose is written. `--specs-dir` points it elsewhere or
turns it off. A table test covers the placeholder, an empty Purpose,
one followed directly by a requirement heading, trailing spaces,
Windows line endings, a written Purpose, a spec with no Purpose, and
a placeholder outside the section. docs/testing.md lists the rule.
2026-10-20 15:00:04 -04:00
cgalo5758 2375d6ee29 Correct four statements in the testing and Go convention docs
The page anatomy rule list left out actions-column, which the lint
emits. The fixtures paragraph said OrgType is the one raw INSERT, but
Draft and Lifecycle also write a product's lifecycle status and
Subscribe a subscription's commitment end, because no console act does
either. The stack script paragraph said the scripts refuse arguments
they do not take without saying which four. The read page bullet held
up operatorNames as a name resolver without saying that a failed read
still falls back to the caller's text for a missing name (#207).
2026-10-16 11:10:00 -05:00
cgalo5758 c8263bfe0e Turn on nilerr: a function that returns an error may not swallow one
docs/go-conventions.md says a failed read is never an answer, and
nothing checked it. nilerr refuses one form of that: a function that
returns an error catches one and returns nil. On the tree it found 15
such returns. 12 were the project lint's walkers, which the parent
commit replaced. The other three:

- CheckQuotaActivity answers a failed read of the sites entitlement as
  a quota of 0, which tells the member they have reached their site
  limit. That is #231; the return is pinned in .golangci.yml by its
  source line until #231 removes it.
- paymentMethodID in the invoice reconcile returned no id when the
  mapped payment method id did not parse. The column is UUID NOT NULL,
  so that cannot happen with the data the schema allows; the function
  now returns the error, so a row that breaks the rule is reported
  instead of read as unlinked. Not probed: no test can store a value
  the column refuses.
- The domain claim's connect probe answers a failed lookup of the
  operator's own target as ProbeStateError, the state that says the
  record could not be checked. That is the probe's answer, so the
  return is exempt with that reason.

With the pins, golangci-lint reports none. 2 breaks, each caught by
scripts/golangci-lint.sh: a read error returned as nil in the lint's
readTree, and the payment method id's parse error returned as nil.
The rule's page names nilerr as its check and says what it leaves to
review: a failed read turned into an empty list or a zero by a function
that returns a value. docs/testing.md lists nilerr among the linters.
make lint passes; the Stripe workflows and lint packages pass with the
database tests skipped (the test stack was down).
2026-10-15 22:11:26 -05:00
cgalo5758 061493920a Report a file the project lint cannot read instead of passing it
Six of the project lint's walkers (the page-template scan, the anatomy
rules, the Go copy pass, retired resource keys, raw error rendering
and walkthrough coverage) skipped a directory they could not list and
a file they could not read, and discarded the walk's result, so a file
the lint never read passed it. They now share readTree, which returns
the files a rule keeps and reports a walk error as an unreadable-file
violation naming the path. A directory that does not exist still holds
nothing to read, as before; walkthrough coverage keeps its own check
for a missing directory. pageBodyTemplates and eachGoString return the
violations beside their results.

TestReadTreeReportsWhatItCannotRead locks a file, locks a directory,
walks a directory that does not exist, and runs ruleRawErrorRender
over a locked file; it skips when run as root, which reads everything.
5 breaks, each caught (new side only; the test is new): a read error
returned as nil, a listing error skipped, the missing-directory check
removed, the violation filed under the walked directory instead of the
path, and the raw-error rule dropping readTree's violations.

On the working tree the project lint reports no unreadable file, the
lint package's tests (the verdict golden included) pass unchanged, and
make lint passes. docs/testing.md lists the new rule id.
2026-10-15 22:11:15 -05:00
cgalo5758 4dad0c48c7 Write down where a rule and an act live, output order, and how to prove a test
The code-organization rules the recent refactor worked to were written
in no tracked doc. docs/go-conventions.md gains them:
- "Where a rule and an act live": one function for a rule a page shows
  and an endpoint enforces (plans.Offer); one statement for a rule SQL
  and Go both need (core.superseded_ends); a many-row act in the
  package that owns its tables (entitlements.RemoveTier, ReorderTiers,
  PlanActError); a handler that parses, calls, maps and renders; a read
  page that loads, derives, builds and renders with one name resolver
  per request; and a failed read is never an answer. The handlers that
  still open a transaction or run SQL are #221, and the reads that
  still turn a failure into a value are #207 and #197.
- "Output order": a list query ends on a unique key (#225 lists the
  ones that do not), a loop that writes rows walks a stated order
  (#218), and Go output from a map ranges over sorted keys.
- No call to Stripe while a transaction is open. The transactions
  section now says new code uses the helpers and points to #186 for
  the BeginTx calls that remain.
docs/operator-ux-conventions.md's failed-collection rule links the
general one.

docs/testing.md:
- Characterize before a refactor, and what a refactor commit may
  change.
- The deliberate-break rule covers new tests and tests whose names or
  comments state a value, names all six outcomes the break tool
  reports, and says how to read a MISSED break.
- Tests of locks and of order are deterministic; a flaky test is a
  suspected defect; a database test passes twice without a reset and
  filters to its own rows.
- Golden files: defined order, rows of the test's own fixture, an
  expected change edited in by hand before the run, `-update` one
  package at a time.
- A helper joins the test kit with its first caller and a test.

CONTRIBUTING.md: the Go and test conventions point to their pages;
docs move with the code; a written rule names its check; a script
that resets or deletes data refuses unknown arguments; "Before you
push" runs `make test`, since without the stack the database and
Stripe tests skip; a commit message reads from the repository alone;
new source files carry the SPDX header; and a small fix rides along
only when listed, while a second sighting, a new rule or lock, or two
statements of one rule get their own issue.

docs/README.md: one page per rule, and the Go conventions entry lists
what the page now covers.
2026-10-15 20:56:59 -05:00
cgalo5758 925ed57ec6 Describe the test kit by what it covers, and point to go doc
docs/testing.md named world's builders one by one, and 21 of its 54
methods were missing from the list; dbtest, htmltest and the Stripe
mock had drifted the same way. The fixtures paragraph now describes
the builders by what they cover, and "The test kit" says that the page
names the helpers a new test reaches for first while
`go doc -all ./internal/testkit/<package>` (and ./internal/stripetest)
lists every exported name with its comment, so the page no longer has
to grow with each helper.
2026-10-15 20:44:21 -05:00
cgalo5758 e2f3b8aa44 Correct the docs that no longer match the code
docs/database-locks.md said recordObligationFailure records a failed
drain in a transaction that takes the rendezvous alone. It runs
MarkObligationFailed, which calls core.settle_obligation, and that
function locks the obligation row and then the pool row, the reverse
of the drain's order. The drain section now says so, and "Where the
paths differ" lists it with #196; the two entries above it gain #193
and #194.

docs/database-management.md described three schemas and three
migration sources. There are five of each: domains and discourse were
missing, with their ledgers and their place in the source order. Its
sqlc example ran `sqlc generate` in one directory, which leaves the
generated files without the SPDX headers `make sqlc-generate` restores;
the example now uses the make target and says why.

docs/testing.md:
- One list of the project lint's rules, by rule id, under the static
  checks; the "Lint and screens" entry, which repeated part of it,
  points there. The TL;DR's `make lint` comment no longer lists four
  of its five checks.
- The browser-test waits are described once, here; the copy in
  docs/go-conventions.md, which disagreed on which rod waits exist,
  is gone. rod's MustWait with a predicate, used by two walkthroughs,
  is named as allowed.
- MockBackend's description names the fields tests use and the three
  ways it fails a call, in place of three of its twenty fields.
- An example test that no longer exists is replaced by one that does,
  and a sentence fused with the next one in the screens entry is split.

test/AGENTS.md: the suite takes a few minutes, not 26 seconds; the
explicit-slug example runs from test/ like the rest of the page and
states the slug's rule; the stack scripts' refusal of unknown
arguments is described; and a link into one contributor's assistant
memory is removed.

The owner of a pinned lint exception or a test-only allowlist entry
is an issue; the comments in .golangci.yml, scripts/deadcode-testonly.txt
and docs/testing.md no longer offer a #166 step, since that issue is
closed and no entry names one.

golangci-lint, scripts/deadcode.sh and scripts/notebook-citations.sh
pass.
2026-10-15 20:38:17 -05:00
cgalo5758 96ba5067a6 Simplify AGENTS.md by removing deprecated context-mode rules and
updating checks section language
2026-10-15 20:25:36 -05:00
cgalo5758 208e10dd9d Replay recorded claim verification and push histories through one replay kit
Add internal/testkit/replay: Run and File replay a package's recorded
Temporal histories against the workflows it registers, and Record writes
them with stand-in activities. TEMPORAL_REPLAY_RECORD names a temporal CLI
binary (the recorder starts a private in-memory dev server from it) or a
running server's host:port; TEMPORAL_REPLAY_ONLY, a file-name prefix,
records the files it names again. FedWiki's replay test moves onto the kit
with its fourteen histories and every subtest name unchanged. Its two
variables FEDWIKI_REPLAY_RECORD and FEDWIKI_REPLAY_ONLY are replaced by
the two above, and its recorder skip message now reads
"TEMPORAL_REPLAY_RECORD not set".

The recorder now writes only the histories that do not exist yet, where
FedWiki's used to rewrite every file; a recorded history stands for runs a
release started, so replacing one takes TEMPORAL_REPLAY_ONLY.

Claim verification gets nine recorded histories (activates, polls,
check-now, expires, probe-error, mark-refused, record-fails, canceled,
rollover) and the push workflow seven (applied, rose, divergent,
conflict-then-applied, permanent, unlinked, gone), each replayed by
TestWorkflowHistoriesReplay in internal/workflows/domains and
internal/workflows/desiredstate. The kit has its own test over a two-step
workflow and a recorded history of it. The 17 histories were recorded at
the parent's code, from a private dev server started from the temporal CLI
(1.5.1); the commands they hold match an earlier independent recording
of the same scenarios, apart from the two clamped timers' durations. No
file names the recording machine or its user.

New tests: the kit's TestTarget, TestRecordsNeverReplaceAHistoryUnasked,
TestRunReplaysARecordedHistory (and its two-steps.json subtest),
TestFileRefusesAChangedWorkflow, TestEndingNamesWhatTheRunDid and
TestRecordTwoSteps (skips); the import guard's subtest for
internal/testkit/replay; TestWorkflowHistoriesReplay with its 9 and 7
subtests and TestRecordReplayHistories (skips) in the two workflow
packages. The kit is listed in docs/testing.md and in the test-only
deadcode list.

No output changes: no production file is edited. At the parent and at this
commit, the names and results of the section's packages are equal apart
from the tests above (FedWiki's 16 results pass or skip as before), the
Temporal surface listings (activities, workflows, workflow bodies, payload
types) are byte-equal, go run . lint prints the same line, the lint
snapshot has no added or removed key, and deadcode and golangci-lint are
clean.

Breaks: 9 breaks, each caught by the new tests; the 2 FedWiki breaks
(a request-driven create and a create started before lifecycle requests
that check the quota first) are caught by the old and the new test.

Not probed: Histories and Run failing a test (no test can watch its own
test fail; every production break above shows Run failing a subtest);
connect, runScenario and write against a server, which run only when
recording (the recording at the parent is their check). A replay cannot
see activity inputs, timer durations, the result, or the ending of a
history that ends canceled or failed; later tests pin those.
2026-10-11 11:50:42 -05:00
cgalo5758 c20ab7dc3c Page the recent-activity feed so every event lists on exactly one page
The landing feed read the newest 100 rows of each of its four sources and
merged only those, so an event past a source's 100th row listed on no page,
the Showing line counted the window ("of 101" for 102 events), and a page
past the window read as empty. Page p of size s now reads each source to
depth p*s in the order the feed merges them in, cuts the page from the
merge, and sums each source's count(*) OVER () for the total. A page number
past the end shows page 1, as before; the depth is capped at what a query's
LIMIT takes, so act_page=2147483647 does not wrap.

The rows a supersession makes redundant are now named once, in the
database, and the lists' queries leave them out, so each count is the count
of the rows shown. Migration 00025 adds core.transition_change_kind (the
kind of change a transition records: a resumption, an extension, else its
type, moved whole from the Go classification), core.superseded_ends (an end
and the typed row of the same act that states it, with by_extension marking
an extension's transfer) and core.extension_handoffs (the grant an
extension's transfer handed a position to), plus an index on (pool_id,
effective_at), the pairing key. The organization page's Tier changes list
leaves out every superseded end; the feed leaves out the ends an extension
states and the extension's successor grant, now over the whole history
instead of the 100-row window. supersededEnds, foldSupersessions and
feedPairing are deleted, and the transitions are cut by (effective_at,
transition_id) like the other sources. Both views read the transition
reason, which the reader role withholds, so the reader-role column file
lists the six view columns as withheld; the role's script is unchanged.

Behaviour that changes: an event past a source's 100th row lists on its
page; the total counts every event; an extension older than the newest 100
tier changes lists as its tier change, not as its successor grant; and when
the transitions read fails, the extension's grant no longer stands in for
the tier change (it drops with the rest of that source, the warning
unchanged).

How it was shown to change only that: TestRecentActivityPagesEveryEventOnce
fails at the parent in its first two subtests (page 11 lists the payment
alone "of 101", and the extension's successor grant "of 102") and passes
here; its other two subtests pass at the parent and guard the hiding inside
the transitions query and the transitions' order. Names and results of
internal/server, internal/entitlements, internal/billing, internal/migrate
and internal/db are equal to the parent's (1778 result lines at the parent,
1787 here) apart from: new TestRecentActivityPagesEveryEventOnce (four
subtests), TestTransitionChangeKind and TestSupersededEnds (nine subtests);
deleted TestLandingFeedCutsEachSourceAt100Rows (its fact is the first new
subtest), TestFoldSupersessions (four subtests; its cases are the first,
second and fourth new TestSupersededEnds subtests, stated clause by clause
in SQL instead of kept as a Go oracle) and
TestTransitionChange_RecognisesMachineReasonsByWriter (its eight cases are
in TestTransitionChangeKind, with eight more); and the failure subtest "a
failed transitions read drops the tier changes, lists the extension's grant
and says so", renamed "... drops the tier changes and says so" and wanting
the issued grant, the payment and the invoice. Both goldens
(operator_activity and org_enrollment) are byte-equal, and the read-count
rows and TestRecentActivityReadsPerEvent are unchanged.
TestTransitionChange_OneLabelRule now takes (type, kind) pairs; the
classification half is in TestTransitionChangeKind.

Rewritten assertions that are NARROWER: the old test also read pages 1 and
2 of the 50-row size on the 101-invoice feed; the new one reads page 11 and
the clamped pages at 10 a page, so those two pages of a 50-row size are no
longer read (a 25-row size and page 2 of a 30-row feed stay in
TestLandingPagesTheActivityFeed). wholeFeed in the golden tests now asks
for the whole timeline on one page: a 400-event limit missed the fixtures'
billing events once other tests had written newer rows to the shared
database.

test/e2e/plan-management TestMVPPlanManagement step 8.6 read the transition
audit through the by-pool list and wanted 5 rows (initiate 2, upgrade 1,
end 2); the list now leaves out the end the trial grant's upgrade
supersedes, so it returned 4. The step now counts the 5 rows written in
core.pool_provision_transitions directly, then wants 4 listed (initiate 2,
upgrade 1, end 1). Wider: the audit fact is kept and the list is asserted
besides. The end-to-end package is outside this commit's checks; a copy of
steps 8.2 to 8.6 run on the test database passes here and, at the parent,
fails on the list's count (5 listed).

Checks: 25 breaks (12 in Go and generated code, 13 in the migration, each
migration break probed with the test databases reset before and after),
each caught by the new tests. The Down of 00025 ran once on a scratch
database (down one version, up again) without error. sqlc-unused reports
unused=0 and the deleted names occur nowhere in internal or docs. The lint
snapshot is unchanged (0 added, 0 removed). Walkthroughs are equal at the
parent and here (16 pass, 6 skip); screens at the parent and here show the
same changes against the baseline, all in the masked regions or equal on
both sides, so operator-overview is unchanged at both widths.

EXPLAIN (ANALYZE, BUFFERS) on the test database, with enable_seqscan on and
off: the restored-names subplan runs once per returned grant (10 loops),
the two anti-joins build the redundant set once, and the invoice and
payment reads scan each table once. A tiny database does not show how the
plans scale, so the same queries ran on a rolled-back fixture of 146,000
transitions: the by-pool list probes the primary key and the effective-time
index in 0.08 ms; the feed's transitions read, which counts the whole
history for its total, takes about 150 to 200 ms there; with tier changes
sharing a few instants, the pairing probe uses the new (pool_id,
effective_at) index.

Unprobed: the by-pool list's pairing probe chose the pool_id index with an
effective_at filter instead of the new index (a pool's rows are few, so the
choice costs nothing; the new index is used where one instant spans many
pools); the statement count of a page after the first (the read-count rows
read page 1, and a later page runs the same four statements); the total
when a source read fails (the failure table asserts rows and warnings, not
the total); a grant-less provision or a provision-less extension in
core.extension_handoffs (no writer records one); the legacy transition type
'extend' in the views (no writer has written it since an early migration's
type check left it out; the function's test pins its kind); and a source
whose rows and count come from two snapshots, when an event is written
between two of the four statements.
2026-10-10 20:51:37 -05:00
cgalo5758 0ce17b598e Characterize the sweep per kind of due row, on a scratch database
TestSweepTrace runs SweepDueScheduledChanges on 12 kinds of due row and
writes one golden file per case under
internal/fulfillment/testdata/sweep_trace, in the format of the
reconcile goldens: the count and error the sweep returns, its
statements in order with each Stripe call placed among them, its log
lines, and the rows afterwards. The cases are a due period-end
cancellation, a cancellation Stripe refuses, a period-end downgrade a
day late, a period-end downgrade whose switch Stripe already made, a
plan switch deferred to the term boundary and the same switch Stripe
refuses, a deferred switch whose target price is recorded in live mode,
one whose item is, a cancellation whose second row finds its claim
gone, a quantity change, and a cancellation on a subscription already
marked canceled, and one on a subscription whose ended_at is set with
its status left active.

Each case gets its own scratch database, migrated with the core and
stripe streams (the stripe stream needs no stream before it), because
the sweep fires every due row of its database and on the shared
database the count and the trace would depend on what other tests left
due. newTraceEnv takes a scratch parameter for this; the two earlier
trace tests pass false. The helpers due and sweep land with their first
caller here.

No production file changes and no output changes. The set of test and
subtest names of internal/fulfillment is the parent's 110 plus
TestSweepTrace and its 12 subtests (123 names, all passing on both
sides, each side run after a reset of the test database). Each golden
was read against the fixture and the code before it was committed. The
new test and the two earlier trace tests pass three runs in a row, a
run without -update leaves the tree unchanged, and the 29 earlier
goldens are unchanged. docs/testing.md says the sweep's cases run on a
scratch database.

17 breaks of sweep.go, each caught by the new test (a new test has no
old side).

Defects the goldens pin:
- #177: the sweep claims a row before it calls Stripe, outside the
  transaction that writes the subscription's change. A refused
  deferred switch leaves the row applied with nothing done and counts
  nothing; a refused cancellation leaves it applied and counts one; a
  quantity change is claimed with nothing done.
- A due period-end cancellation is cancelled at Stripe without asking
  whether Stripe still cancels at the period end.
- The count is every nil return, so skips, lost claims and quantity
  changes count, against the function's comment "the number applied".
- A deferred switch moves the oldest current item, not the ladder's
  item; the case is built so the two agree.

Unprobed, with the reason:
- the three firing errors that print %!w(<nil>) (a mapping without a
  Stripe id, a subscription without a current item): no console path
  leaves either while a row is due;
- a failed due list or a failed claim other than no rows: both need a
  failing database;
- the plan switch without a target price: no console path writes one.
2026-10-10 18:52:39 -05:00
cgalo5758 9df43c1489 Characterize the subscription reconcile per status
TestReconcileTrace runs 21 cases of ReconcileSubscription, each on a
fresh fixture, and writes one golden file per case under
internal/fulfillment/testdata/reconcile_trace. A golden holds the
act's result, the statements it ran in order with the begin, commit or
rollback and each Stripe call placed among them, the log lines, and the
rows the fixture holds afterwards (subscription and its changes, items,
scheduled changes, provisions, ladder positions, transitions and the
entitlements). The cases cover first sight, replay, a quantity change
and a quantity of zero, a price swapped in place, an item replaced by
another and an item without an id, an add-on added and removed, a
product without an entitlement set, past due and active again,
incomplete, the canceled branch (back to the default plan, replayed,
and resuming a held grant), a refused fetch, a subscription without a
customer, an unmapped price, and a mapping recorded in the other key
mode. The trace helpers sit in trace_db_test.go: the code under test
runs on a counted handle, fixtures on another, and an unnamed id left
in a golden fails the test instead of making it flaky. A materialization
is folded into one line (its statements are pinned by the materialize
golden) and each run of conferral writes is sorted, because the
reconcile ranges over a map there. docs/testing.md gains one bullet.

The golden of grant_over_subscription pins a defect (#212): a reconcile
confers the subscription's tier again over the operator's grant that
replaced it, ending the grant. The three canceled cases
(canceled_to_default, canceled_replay and canceled_resumes_grant) pin
the order the ended branch runs its steps in as it is today (#180). The
14 goldens that show the status sync pin it running with no pool lock
taken before it (#193). A fix to any of the three flips those goldens on
purpose.

No production file changes and no output changes. The set of test and
subtest names of internal/fulfillment is the parent's 79 plus
TestReconcileTrace and its 21 subtests (101 names, all passing; the
parent's all pass too). Each golden was read against the fixture and
the code before it was committed; a second run without -update
leaves the tree unchanged and the test passes three runs in a row on a
reset database. Where the Stripe fetch is concerned, the goldens print
the SDK's own encoding of the expand parameters, expand[0] and
expand[1].

29 breaks of reconcile.go, each caught by the new test (a new test has
no old side).

Unprobed, with the reason:
- the error texts of the reconcile other than the three the cases pin:
  each sits on a failed statement that no fixture causes without a
  failing database;
- that the mapping read before the transaction runs on the pool and not
  in the transaction: the trace shows it before the begin, not which
  connection ran it; a failed read other than no rows is ignored today
  and cannot be caused;
- the order among statements of one name issued per row of a read
  ordered by creation time alone (ending an item, ending a surplus
  conferral): the trace shows their count, not which row went first;
- the order of the desired loop, sorted on purpose because Go's map
  order varies;
- materialization's own statements, folded (the rows show what it
  writes);
- two reconciles of one subscription at once: no case runs them; the
  lock statement is in every trace that reaches the transaction (all
  but fetch_refused and unreachable), but a trace cannot show it held;
- a quantity change moving the entitlement limit: the fixture's limit
  rule is not per unit, so the quantity golden shows the item and the
  provision at 3 and the limit at 1;
- ReconcileLatestSubscriptionForCustomer, unchanged and pinned by its
  own test.
2026-10-10 18:52:39 -05:00
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 25fb6e45af Build product mappings and a product without its set through the kit
world gains MapProduct and MapProductIn, which give a product a synced
Stripe mapping through the console's own upsert (Stripe id prod_<product id>,
no environment or the one asked for), and WithoutSet, which clears a
product's entitlement set through EditProduct the way the operator's edit
form saves a product with no set chosen. TestCatalogBuilders reads both back
through the console's queries. No builder writes a dead-lettered outbox
entry; a test that needs one makes it through the console's
MarkOutboxDeadLetter.

No production file changes and no existing builder writes anything new. Names
and results over ./internal/testkit/..., internal/entitlements,
internal/fulfillment, internal/server, internal/integrations/discourse/web,
internal/integrations/fedwiki/web and internal/integrations/stripe/workflows,
taken with the test database reset before each side, are equal at the parent
and here except the new test and its two subtests, which pass. The package
test/e2e/plan-management, which also builds through world, compiles under go
vet; it calls none of the new builders and no existing builder changed. 5
breaks, each caught by the new test; none dropped. Unprobed: MapProductIn(p,
false), which is the same statement with the flag false; the test reads the
live case.
2026-10-10 14:50:48 -05:00
cgalo5758 ae84dc569d Give tests the Stripe SDK key's mode as the console's configuration gives it
stripetest.KeyMode returns the mode of the key the Stripe SDK holds when
it is called: "test" once Install has set its key, "" when the SDK holds
no key, "live" under a live key. A test passes it where the console
passes its configured key mode, and it reads the key at the same moment
production does, so a test that never installs the mock keeps the mode
it has today. The docs say so beside the other stripetest helpers.

No production file changes. The package's tests run with the app down on
freshly reset test databases at the parent and at this commit; the names
and results are equal apart from the one new test, which passes. One
break (reading a fixed key instead of the SDK's key) is caught by the new
test; the test has four cases, none unprobed.
2026-10-10 14:50:48 -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 8cee42f2d3 Compare golden files and name a run's ids through one kit package
The server's golden helpers (the comparer, the -update flag and the
normaliser that turns a fixture's ids into names) move into
internal/testkit/golden, so any package's tests can compare output with a
file under testdata. Ordinals is new: it numbers the UUIDs and UTC times
an act makes by first appearance, for the ids a fixture cannot name.
Normalizer.Add replaces the one place a server test appended to the
normaliser's unexported pairs.

Eleven server test files use the package; the -update flag keeps its
name. docs/testing.md gains the kit row and a bullet, and the kit's import
guard and the dead-code list know the package.

No output changes. No production file changes, and no file under
internal/server/testdata is touched; every server golden test passes
without -update and git status is clean under testdata afterwards. The
names and results of ./internal/testkit/... and ./internal/server are
equal at the parent and at this commit (1452 and 1458 tests and
subtests), apart from the five new tests and the import guard's new
subtest for the package. No assertion changes, so no assertion map.

13 breaks: 12 caught by the new tests, and the one that renames the
normaliser's <today> pair is caught by the golden test at the parent and
at this commit.

Unprobed: that a name outranks a pair in the server's goldens (their ids
are UUIDs and their pairs a date and the suffix, which never start at one
position, so the order is invisible there; the kit test probes it); the
server copy of the comparer at the parent (a break that makes it pass
everything cannot fail the goldens it compares); a migration test that
defines its own update flag (a documented rule, not code).
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 eb4a7552bd Pin every route's pattern and answer through the router
No production file changes. authtest gains Cookie, which commits a session
and returns its cookie, for a handler wrapped in the session middleware,
such as the whole router, where Get and Post cannot put the session on the
request's context. docs/testing.md names it and the kit table's authtest
row says "signed-in sessions, their cookies, and in-process requests".

internal/server/new_handler_test.go (no database) builds the router from a
configured profile and a bare one (no Stripe key), with a fake route mount
and two UI mounts, and adds five tests. TestNewHandlerRouteTable writes one
block per registered pattern (the 97 literal HandleFunc patterns the lint
scan finds in internal/server and internal/auth, plus the two static-file
Handle registrations and the fake mount's three routes) to
testdata/routes.golden: the pattern the mux resolves a probe path to, and
the answer to an anonymous request, a cross-site request for a method that
changes state, and a signed-in member where the handler answers before any
read; then eight extra probes and the bare profile's checkout row. The
golden was written once with -update and read against the code: every
operator row answers 302 /login anonymously and 403 Forbidden to the
member, the cross-site writes answer the CSRF refusal except the fake
webhook (204), the two wrong methods and the bare checkout resolve to / and
the styled 404, and the body one byte over 1 MiB answers 413.
TestNewHandlerTrustsTheBaseURLOrigin, TestNewHandlerRefusedSignInIsStyled,
TestNewHandlerRefusesAMountTemplateOutsideItsKey and
TestNewHandlerPassesDepsToMounts cover the base URL's origin, the styled
refused sign-in, the key-prefix refusal and the Deps a mount is handed.

Shown to change no output: no production file is touched, and the names and
results of internal/server and internal/testkit equal the parent's apart
from the five new tests, which pass three runs in a row. The 47 breaks of
the plan, each applied to the new side after a green control run, are all
caught.

Left unprobed:
- The answers the golden records that code outside the router produces
  (each handler's own answer, the auth handlers, the operator role guard,
  each middleware's behaviour): each has its own tests, and no edit to the
  router changes them; the golden holds them so a change shows.
- middleware.Timeout(32s): a short value races fast handlers, and a probe of
  the real value waits 32 seconds.
- middleware.Logging and the order of RequestID before it: they write a log
  line no test reads.
- The http.Server fields: Start keeps them, and nothing builds the server in
  a test.
- The dashboard's root-only check: a later commit probes it.
- The real integrations' routes (FedWiki, the Stripe webhook, Discourse):
  they mount from cmd/start.go, and the fake mount stands in.
- The Config cmd/start.go builds (including StripeKeyMode and BaseURL) and
  the Start call with its exit path: no test builds the command's Config;
  these tests build their own.
2026-10-09 21:23:00 -05:00
cgalo5758 bf6ad44a7c Characterize the catalog's other per-row reads: set history, sets, the setup step and product prices
Four new tests pin what four catalog loaders show when a read that names or
counts per row succeeds, fails or has no querier: the actor each change of a
set's history names (a person's change names the person, the system's names
nobody, a failed person read and a missing identity querier name nobody), the
active rules each set in the sets list counts (a failed read counts 0), the
first unconfigured provider the setup checklist's integrations step names in
set order (a failed rule read skips every set), and the Stripe mapping each
price on a product shows (a failed read or no Stripe leaves every price not
mapped). Four rows join the read-count table, each measured at one statement
per row, as predicted: entitlement-set-history, entitlement-sets, setup and
product-prices. The table's page type gains the integration configs the setup
row needs.

world gains RuleChange, a person's rule-added commit through the batch commit,
which settles the carrying pools; Rule stays the system's unsettled path.
registerGoldenKeys takes a testing.TB so a read-count build can call it.

No production file changes, so no output changes: the goldens are untouched,
and over ./internal/server and ./internal/testkit/... the set of test and
subtest names and each result equals the parent's apart from the 18 new
names (4 tests, 10 subtests, 4 read-count subtests), none missing, none
changed, no failures; three runs in a row on a reset database are green.
19 breaks, each caught by the new test; the old side does not exist for a
test that is new. Unprobed, since no section of this step moves their code: the history's other
columns and paging, the sets list's failed-obligation count, the setup page's
other steps, the price rows' amounts and intervals.
2026-10-09 17:44:54 -05:00
cgalo5758 2a913ca635 Characterize the plan-ladders page, the ladder page's tiers and the reorder preview
Adds tests, one test-kit builder and goldens only; no production file
changes.

TestPlanLaddersGolden builds three ladders (Hosting with Basic and Shared,
Support with Shared, Empty with no tier), five products (a shared one, an
add-on, one with no category, a draft), three organization types (defaulting
to Hosting, to nothing and to the empty ladder) and an organization holding
Basic, and compares with files under testdata/operator_plan_ladders: the
topology's columns, rank rows, shared products, off-ladder strip and type
summary together with the ladder list (page); the ladder page's tiers with
their organization counts (detail); and the reorder preview's order for the
ladder's own order (preview). The page lists every ladder in the database, so
the files hold the fixture's ladders, products and types. The properties
subtest holds the whole grid to the ladders' own reads: rows ascend by rank,
every row has one cell per column, and every column, the fixture's or not,
counts what CountActiveAttachmentsByLadder returns and holds the tiers
ListTiersByLadderWithProducts returns. Each file was read against the fixture
before it was committed; a type whose default ladder has no rank-0 tier reads
as no default.

TestPlanLaddersReadFailures pins, per read (ladder listing, shared products,
ladder counts, tier reads, product list, rank-0 listing, organization types,
tier counts on the ladder page and in the preview), what the pages show when
that read fails.

world gains AddTier. The read-count table gains plan-ladders (4 reads per
ladder), plan-ladder-detail (1 per tier) and tier-reorder-preview (2 per
tier), each as the plan predicted; the table's request can now be a POST.

Names and results of ./internal/server and ./internal/testkit/... match the
parent apart from the new tests, which pass: TestPlanLaddersGolden and its
four subtests, TestPlanLaddersReadFailures and its nine, and the three
read-count rows. A second run without -update leaves the tree unchanged, and
the tests pass three runs in a row after a reset. Thirty-two edits to the
topology and the ladder code, each breaking one fact, fail the test that names
it.

Unprobed: each column's and list row's IsActive and description, the org-type
summary's display names (moved unchanged); the list's name filter and paging,
which no section of this step edits; the order of same-named products in the
shared index, which is unspecified today and which no section of this step
fixes; the reorder preview's classification path (rank 0 does not change in the
fixture); the topology's template rendering, which its own tests keep.
2026-10-09 17:44:40 -05:00
cgalo5758 0fe2305905 Characterize the products list: a golden of its rows and a table of its failed reads
Adds tests, two test-kit builders and goldens only; no production file
changes.

TestProductsListGolden builds thirteen products (a ladder tier, an off-ladder
product, an add-on, one with no set, one with no rules, one priced in the other
environment, an unlisted, an inactive, a draft, a retired, one needing an
unconfigured provider, and two sharing a set) and compares the rows the loader
builds, and the page rendered from them, with files under
testdata/operator_products, with Stripe configured and not. Each file was
read against the fixture before it was committed, and each failure file against
the fallback that read gives every row when it fails. The golden files show the
verdict, note, set name, category and visibility of every row, the provider leg
naming the unconfigured provider, and both set cells naming the shared set.

TestProductsListReadFailures pins, per read of the page (shapes, set names,
prices, price mappings, set rules, resource keys, and the page itself), the
rows the loader builds when that read fails, and that a failed page read says
"Failed to load products" and builds no row.

world gains EditProduct and Lifecycle. The read-count table already holds the
products row at 0 per row.

Names and results of ./internal/server and ./internal/testkit/... match the
parent apart from the new tests, which pass: TestProductsListGolden and its
two subtests, TestProductsListReadFailures and its seven. A second run without
-update leaves the tree unchanged, and the tests pass three runs in a row
after a reset. Nineteen edits to the list code, its readiness helper and its
template, each breaking one fact, fail the test that names it. One of them
(the price reader told "configured" when Stripe is not) changes no output in
that state, because the mapping read is skipped there and the map stays empty;
it is run in the configured state with "not configured" instead.

Unprobed: the view model's ProductID, Name, Description, IsActive,
LifecycleStatus, Lifecycle and EntitlementSetID fields as individual facts
(the golden compares them, and the other breaks show it compares every
field); the skipped mapping read when Stripe is not configured, since no
output depends on it; the order of the six reads; the row order, which the
list query fixes; that two products sharing a set pass its id to the set and
rule reads once (the ids a batch read receives change no output, and the page
reads the sets in one statement either way).
2026-10-09 17:44:07 -05:00
cgalo5758 f53186bb6b Characterize the four billing lists and the lookup's candidates, with their read failures and reads per row
Adds tests, three test-kit builders and goldens only; no production file
changes.

TestBillingListsGolden compares each list's data, in the default and the
all state, with a file under testdata/operator_billing for a fixture of two
organizations and four billing accounts (live, unrecorded, unmapped, test),
each with a subscription, an invoice and a payment. Each file was read
against the fixture before it was committed: the test-mapped row is hidden
by default with the line counting one, shown with the test badge in the all
state, the unmapped row reads not_mapped, and every row names its own
organization.

TestBillingListsKeepTheirFallbacksWhenAReadFails pins, per list, that a
failed organization read leaves the name empty and a failed mapping read
shows not mapped.

TestLookupInvoiceCandidatesNameTheirOrganizations pins the invoice
candidates' organization names and the organization id as their fallback.

The read-count table gains billing accounts, billing subscriptions, billing
invoices, billing payments and lookup rows. Each grow adds its rows on a new
organization named from a counter, and the lookup row is sent as an htmx
request. Measured per row: 2 for each billing list, 1 for the lookup, as
predicted.

world gains MapAccount, MapSubscription and MapPayment, which record no
environment, beside MapInvoice.

Names and results of ./internal/server and ./internal/testkit/... match the
parent apart from the new tests, which pass: TestBillingListsGolden and its
twelve subtests, TestBillingListsKeepTheirFallbacksWhenAReadFails and its
twelve, TestLookupInvoiceCandidatesNameTheirOrganizations and its two, and
the read-count subtests billing_accounts, billing_subscriptions,
billing_invoices, billing_payments and lookup. A second run without -update
leaves the tree unchanged, and the tests pass three runs in a row after a
reset. Thirty-two edits to the list code and the lookup, each breaking one
fact, fail the test that names it.

Unprobed: the goldens pin the loaders' other fields (dates, amounts, statuses,
account names, payment method, invoice numbers, Overdue, the pager and the
absence line) with no break, because no later section edits the code that fills
them; the breaks probe the fields the mapping and naming changes move.
2026-10-09 17:43:59 -05:00
cgalo5758 6e7d0aea47 Drive the billing environment filter through signed-in requests on the kit
TestBillingViewsFilterToTheKeyEnvironment now builds its rows through the
world kit (organization type, owner, organization, billing account,
invoices, payments, bare subscriptions and their Stripe mappings) and
drives the operator routes with a signed-in operator through authtest.
Each subtest is a method of billingViewsFixture and reads the parsed page
(the absence line, its switch, the invoice rows, the facet pills) instead
of searching the body for strings. The gocognit exception for the test is
deleted; its largest function now scores 10.

The kit gains BareSubscription (a subscription row and nothing else) and
the mapping builders MapInvoice, MapInvoiceIn, MapPaymentIn,
MapSubscriptionIn and MapAccountIn. seedPayment, seedSubscription and
seedAccountMapping are deleted with their last caller, with the unused
member session; seedInvoice stops returning the id.

No production code changes. Shown to change no output: names and results
of ./internal/server and ./internal/testkit/... are identical at the
parent and at this commit (1204 tests each, none failing). 17 breaks,
each caught by the new test; 15 of them also caught by the old test. The
payments and subscriptions noun breaks are new side only, because the old
check could never fail for them.

Assertion map rows: every row same or wider, none narrower. One general
weakening: the badge's exact markup (`>Test</span>`) is now read as
whitespace-collapsed text. Fixture differences: names and invoice numbers carry the world
suffix instead of the be<UnixNano> marker; the organization type comes
from OrgType; the organization has a default pool; the owner is a world person;
the signed-in operator is a person of its own. The parent and commit database
dumps agree apart from these differences.

Unprobed: the absence line's count, which TestAbsenceLineCountsUnderTheSearch
pins; the LIVE and NULL rows' lack of a badge in the all state, not asserted
before and not added.
2026-10-09 16:00:10 -05:00
cgalo5758 df3de19115 Check the operator invoice detail through the kit and the HTML query helper
TestGetOperatorInvoiceDetailPage builds its organization, billing account,
price, invoice and line through the test kit's world on the shared database
instead of raw INSERTs in a rolled-back transaction, signs in an operator
through authtest, and reads the page through htmltest. The kit gains
LineItem, which writes a line the way the invoice reconcile does. The
gocognit exception for the old function leaves .golangci.yml; the largest
new function is well under the limit.

No production file changes, so no output changes. The test and subtest
names are the same; the full result sets of ./internal/server and
./internal/testkit/... (1204 tests) are identical at the parent and here.
Eighteen breaks of the production code the page runs through were each
caught by the new test, and the seventeen that the old test can see were
each caught by the old test too; the Overdue badge's danger tone is proven
on the new test alone, because the old check (text-bg-danger anywhere in the
body) could never fail.

Assertion map: every old check is the same or wider, with no narrower row.
One general weakening, for markup that states no fact: the title's and the
id's utility classes are no longer asserted. The amount check reads the
"Amount due" cell alone; the old check passed on that cell or the line's
amount, so the line's own amount stays unasserted, as before. The old
retired-markup absences stay raw over the whole body.

Fixtures differ in ways no assertion reads: names carry the World's suffix;
the organization has a default pool; the owner is a world person and a
second person, the operator, is signed in; the product is published with an
active set; the price is the product's default, as CreatePrice makes a
product's first price; the rows commit and the truncate is gone. The parent
and commit database dumps agree apart from these differences (the parent's
taken before the 404 subtest, because reading the non-UUID id aborts the old
transaction).

Unprobed: the line's own amount, which neither test asserts alone.
2026-10-09 16:00:10 -05:00
cgalo5758 74000dae68 Build billing rows and grant, tier and subscription acts through the kit
Add world builders that write through the queries and functions the
console calls: BillingAccount, Invoice and Payment, IssueOpts.Note,
Extend (the Manage act's extension), ReorderTiers, RemoveTier and
EndSubscription (the reconcile's terminal branch). Subscribe now
remembers the pool it conferred on. No existing builder writes anything
new: IssueOpts.Note is empty by default and is then written as NULL, as
before.

Shown to change no output: only test-kit code and docs change; no
production file is touched. The tests of internal/testkit/..., internal/server,
internal/fulfillment and both integration web packages pass on a reset
database with the same names and results, and the lint snapshot is
identical to the parent's.

The two new tests (TestBillingBuilders, TestActBuilders) were checked with
15 breaks, each caught; none dropped. Each TestActBuilders subtest builds
its own World, and three more breaks (the floor, a subtest's failure
charged to the subtest, both payments read back) were each caught.
Unprobed: RemoveTier's refusal of a held tier (a Fatalf inside the
builder), EndSubscription's closing of scheduled changes (no fixture
schedules one on a subscription that ends), and an invoice's due date,
period and paid time (written as given).
2026-10-09 16:00:10 -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 c77c9d7300 Correct the documents that name moved code
The storefront and consumers rows name the files that now hold the plan,
add-on and entitlement code; the integration guide drops member_products.go
from the files that stay in core and says the files of the split import no
Stripe package; the resource-pools
dimension states what the schema admits and what the console refuses; the plan
ladders card gains a Member position row; the kit table says the fixture
builder covers plan fixtures.

No output changes: only Markdown under docs/ is touched, no Go, SQL or
template file. git grep for member_products.go in docs and scripts is empty,
make lint and the notebook-citation check pass, and the lint snapshot differs
from the previous one by removed 0, added 0.
2026-10-09 04:50:39 -05:00
cgalo5758 219f734e9a Document the test kit, the transaction runner and the surface constructors
docs/testing.md: the front-matter summary names the test kit; the TL;DR runs
make test, and one package on the same footing after a reset. Database-gated
tests open through dbtest: Open skips when TEST_DATABASE_URL is unset, never
migrates and refuses a database whose core ledger is behind; OpenE2E and DSN;
Scratch, UpTo and ApplyVersion for migration subjects; Q and AwaitLockWait.
The self-migrating table and the hand-list warning are gone. The fixtures
paragraph describes world (console functions, fails on any error, suffixed
names, no cleanup, any *sql.DB) and states the exemption for a test of the
schema itself. Type 4 names the server harness's set helpers (OperatorSet,
MemberSet, Render), htmltest selectors by role and by aria-*, hx-* and data-*
attributes, and authtest. Type 7 gives browsertest's rules: boolean waits
only, ClickAndLand for a navigating answer, AwaitSwap for a swap, Recover in
each walkthrough, AppDB never migrates and skips without its key, free
functions taking t, and phases as named functions. A table lists the kit's
packages and what each may import. A paragraph describes proving a converted
assertion with a deliberate break and scripts/mutation-check. The complexity
bullet says a closure passed to t.Run counts toward the enclosing function.
The lint bullets say the lint parses each template in the set its surface
builds in production and fails on a file no surface parses, and that forbidigo
refuses rod's load and stable waits. The stale internal/workflows/stripe path
becomes internal/integrations/stripe/workflows, and the notes that described
TEST_DATABASE_URL as MC_DB_DSN, the shared-database fixture pollution and the
source-order convention now describe the separate test databases and the
reset.

docs/go-conventions.md: a transaction opens through db.InTx or db.Begin with a
kind (entitlements.Materializing or RuleChange for rendezvous transactions),
its begin and commit errors unwrapped; a pool lock goes through LockPools or
LockPoolOfGrant, and the order lives in database-locks.md; test helpers take
testing.TB and fail on error; "Where the checks live" names the rod wait rule,
the waits it refuses and those that pass it.

docs/database-locks.md: the pool row's "Taken by" names LockPools and
LockPoolOfGrant and their callers; Revoke, Manage, tier removal, default
change and the rule change say LockPools; the intro of the order section names
the two kinds as what sets lock_timeout, says a plain transaction sets none,
and says which paths render the lock refusal; rule 3 says "through LockPools";
the #186 bullet keeps only the advisory-key half.

test/AGENTS.md and test/reset-test-db.sh: the reset is the only migrator and
dbtest.Open refuses a stale ledger; the walkthroughs run before and after a
change from the demo snapshot.

docs/building-an-integration.md, docs/design-system.md and
docs/operator-ux-conventions.md: template sets come from web.MemberTemplates
and web.OperatorTemplates with the integration's web.Mount; operator files are
named <key>_operator*.html; no ParseUIPartials and no per-set surfaceRoot or
routeURL registration; the helpIcon row names forms.Help and forms.HelpIcon.
The DB-backed tests bullet of docs/building-an-integration.md says integration
tests open through dbtest.Open, the reset migrates every registered stream, and
a migration's own test runs on a dbtest.Scratch database; its UI-assets
paragraph says the member set panics at boot through template.Must, an operator
handler returns the constructor's error, and the lint fails on a file neither
set parses.
2026-10-08 19:16:52 -05:00
cgalo5758 b75c17ef81 Document the static checks 2026-10-07 01:42:54 -05:00
cgalo5758 02cf4c6fc5 Fail the project lint on a template it cannot parse 2026-10-07 00:53:16 -05:00
cgalo5758 b1d8f18ae2 Retire the stale ?tab= lint rule 2026-10-07 00:22:26 -05:00
cgalo5758 28eb8455f9 Delete the request-decompression middleware nothing mounts 2026-10-07 00:22:26 -05:00
cgalo5758 0caed6e905 Harvest the exploration notebooks into the docs
A survey of the twenty notebooks on disk found general facts that no
tracked page stated; the maintainer struck the weak rows and the rest
are written into their owning pages in each page's own words. The
design-system page gains the hidden-navigation figures that replaced a
dangling survey pointer, the reason the anatomy is parts and lint, the
record-table grouping and single-tint rules, and when a surface earns
row editing. The first-contact process gains a code-verification phase,
the transcript lane for models without vision, the three kinds of
honesty-audit drift and the click-path guardrail. The operator
conventions gain guards-before-writes, error state over empty state,
copy that states the real effect, toasts that report counts and the
PostForm rule. The Stripe, FedWiki, identifiers, IA, testing, model and
environment pages each gain their facts, the integration guide stops
telling authors to hand-write a page title, issues.md logs the
placeholder security contact, milestones.md records the Codeberg terms
clause and CONTRIBUTING.md notes that the module path is the forge URL.

One claimed fact was checked against the code and not written: the
mass-mutation preview does not run the commit loop in a rolled-back
transaction.
2026-09-19 19:47:33 -05:00
cgalo5758 074702c8bc Seed ledger demo via shared grant act helpers
Extract ExtendGrantTx, RevokeGrantTx, and ExpireGrantTx into
internal/entitlements so the operator handlers, expiry activity, and
demo seed all run the same conferral path. The seed plays a five-grant
history on Carlos's org; pin-timestamps now uses dense-rank offsets
and transition list ordering ties break by transition_id.
2026-09-07 00:51:49 -05:00
cgalo5758 ee23d31c97 Complete anatomy sweep across all surfaces
Rebuild every operator, member, and integration template on the shared
page-anatomy parts, empty the lint allowlist, and move the confirm modal
into both shells. Replace back links with location trails and make row
identifiers the primary link. Make screen captures deterministic by
resetting the app database from a pinned demo snapshot. Send the logout
id_token_hint only while valid.
2026-08-31 18:11:09 -05:00
cgalo5758 408fa6f5a6 Add page anatomy parts and UI quality gate
- Add shared ui_*.html parts (pageHeader, sectionHeader, statusBadge,
  emptyState) parsed into every template set
- Add anatomy lint rules with a shrinking allowlist and screen-coverage
  check
- Add make screens capture harness with contact sheets and baseline diff
- Compose member and FedWiki regions server-side so pages arrive
  complete
- Rebuild Domains and Integrations on the parts as pilots
2026-08-30 04:05:31 -05:00
cgalo5758 60b4c93eb6 Remove hardcoded absolute repo paths
Use repo-relative paths in docs and exploration notes, and update the
chrome-devtools launch description in testing.md.
2026-08-29 21:19:17 -05:00
cgalo5758 c4bb1ba585 Harden domain claim expiry and carving
Sweep stranded pending claims at boot and on a Temporal schedule while
preserving evidence-based abandonment semantics.

Apply occupancy and name-policy checks to carves by operator-root owners
without affecting direct operator placements.
2026-07-25 04:10:34 -05:00
cgalo5758 c604487524 Archive migration changes and update specs
Document the consolidated core/integration schema model, per-stream
goose
ledgers, and revised M9 integration roadmap.
2026-07-05 22:17:41 -05:00
cgalo5758 5752dead03 Use per-stream migration ledgers
Run goose once per migration source with its own
goose_db_version_<name> table and native file numbering.

Remove positional namespace assembly, update rollback/status behavior,
and document the OpenSpec decision.
2026-07-05 21:42:55 -05:00
cgalo5758 cbd08b45aa Add project front-door documentation
Rewrite the README around purpose, architecture, quickstart, and
status. Add contributing/security policies plus a docs index with
audience front matter across existing docs.
2026-07-01 04:18:08 -05:00
cgalo5758 966a309bfd docs: add testing guide
Document the kinds of tests (pure unit, DB-gated integration, Stripe-mocked,
HTTP handler/render, route/template lint, live e2e, OpenSpec validation), what
each covers, how to run them, and the role of the test/ stack directory. Add a
Testing subsection to the README pointing at it.
2026-05-23 21:51:16 -05:00