Commit Graph
139 Commits
Author SHA1 Message Date
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 e68493ea16 Read both grants pages from one listing query
ListGrantsWithDelivery now takes optional organization, search, product,
state and paging arguments and serves the grants page and the
organization page's ledger. The grouped SELECT's columns and grouping
moved unchanged into a CTE named listed, whose WHERE now holds the
organization filter and the search filter (with the organizations join
the search needs); the outer statement filters on the state computed
once, counts with count(*) OVER() after that filter, orders by
created_at DESC and pages (a NULL limit or offset reads every row).
ListGrantsWithDeliveryPage, toDeliveryRows and loadGrantsListData's
unused success and error-message arguments are deleted. The generated
row is field for field the old page row. Product names are still
matched in Go, so % and _ in a search keep matching themselves there.

The two pages keep the pool difference: with an organization the waits
are evaluated on its active default pool, without one on every pool.

How the commit was shown to change no output:
- The row golden, run first on PostgreSQL against the merged query,
  passed with every file under testdata/grants_listing/ and
  testdata/org_enrollment/ unchanged. LIMIT NULL OFFSET NULL returns all
  rows, and EXPLAIN ANALYZE of the organization-only call and of the
  search, state and page call shows no CTE scan on listed (it is inlined).
- internal/server (1398 results) and internal/entitlements (180) give
  the same test and subtest names and results at the parent and at this
  commit; no test was added, renamed or deleted.
- 13 breaks, each caught by the old and the new test, plus 3 breaks
  caught on the old side only: the adapter's copies of the waits, the
  default-duplicate and the restored-after columns, which are deleted
  with the adapter. No break was dropped because the old test missed it.
- Test calls were re-pointed to the merged query with their assertions
  unchanged (every row of the assertion map is same). The comments of the
  query, the grants template, docs/models/entitlements.md and
  docs/operator-ux-conventions.md now name the one query.

Unprobed: the order of grants that share created_at (a tier removal
writes one default grant per vacated pool in one transaction). The
merged query's plan may order such a tie differently from either old
query, no fixture holds a tie, and the order is accepted until the
tie-break by grant id, as for positions. The zero UUID pool of an
organization with no active default pool: no fixture organization lacks
one. total_count on the organization call, which no caller reads. The
adapter's other 22 copied columns on the old side: the goldens read each,
three are probed, and the adapter is gone so no copy remains to break.
2026-10-10 03:36:06 -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 f66a80b26a Restore the rules in the model cards and the Stripe guide 2026-10-07 06:28:59 -05:00
cgalo5758 b75c17ef81 Document the static checks 2026-10-07 01:42:54 -05:00
cgalo5758 8dcda61e37 Describe the console's database locks and the order paths take them 2026-10-07 01:35:25 -05:00
cgalo5758 afb18aae07 Correct the model cards that disagree with the code 2026-10-07 01:19:45 -05:00
cgalo5758 02cf4c6fc5 Fail the project lint on a template it cannot parse 2026-10-07 00:53:16 -05:00
cgalo5758 ab2bd4ada6 Delete the materializer's dry run and rule overlays 2026-10-07 00:36:11 -05:00
cgalo5758 15ffabbdd6 Delete the rule-change dry run 2026-10-07 00:36:11 -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 75f0572dfb Add the gate's configuration and tools 2026-10-06 22:48:30 -05:00
cgalo5758 23d5bad024 Add integration delivery core: provider connections, desired-state
records, outbox, lifecycle requests, relay, push, read-back, and FedWiki
lifecycle workflows

- Add core.provider_connections table and unique provider connection per
  provider
- Add core.target_mappings table linking connections to resource keys
  and targets
- Add core.desired_state_records table storing desired entitlements per
  connection and recipient with generation and content hash
- Add core.observed_projections table for application-reported state per
  connection and recipient
- Add core.lifecycle_requests table recording lifecycle requests by
  instance and generation
- Add core.person_cut_offs table and setter function to record persons
  cut off everywhere
- Add core.desired_state_outbox table with triggers on entitlement,
  membership, organization, connection, mapping, and cut-off tables to
  write change notices
- Enforce constraints on recipients, generations, and statuses across
  tables
- Implement desired_state_content SQL function computing record content
  per recipient rules
- Implement relay workflow to claim outbox rows, expand to records,
  recompute records, and dispatch pushes
- Implement push workflow to push records, handle transport answers, and
  record push results
- Implement read-back workflow to page through application state, update
  projections, and raise records as needed
- Add lifecycle request writers with idempotent recording, finishing,
  and acknowledgment
- Add FedWiki integration lifecycle workflows for create, set status,
  swap, delete, and reconcile
- Add FedWiki web handlers and forms for lifecycle requests, including
  create form with instance and generation
- Add tests for schema, triggers, relay, push, read-back, lifecycle
  requests, FedWiki workflows, and operator pages
- Add documentation updates for provider integration, entitlements,
  identity, organizations, lifecycle, and FedWiki integration
- Add benchmarks for delivery and push workflows
- Ensure schema-per-provider for in-tree integrations; connector state
  lives in generic core tables keyed by connection
- Secure functions and triggers with SECURITY DEFINER and restrict
  core_writer role privileges accordingly
- Handle Temporal client integration for workflow execution and
  scheduling
- Provide a recording transport for integration tests
- Close Issues 32 and 33 by implementing provider connections and
  desired-state records with lifecycle requests and relay mechanisms
2026-10-06 03:46:15 -05:00
cgalo5758 113c672e3b Resume a marked grant when the plan that replaced it ends, and archive the change
An operator can mark a grant to resume. When a plan replaces a marked
grant and that plan later ends (the subscription ends, or a granted plan
is revoked or expires), the grant delivers again from its own row before
the org-type floor runs (core migration 00023, core.grant_waits).

- Grants: a resumes_after_supersession column, off by default and never
  on a default grant; delivery states gain On hold, with a badge hover
  that names what the grant waits on.
- Operator: one Manage action per grant opens a card under its row
  (Note, Valid until, Resumes; Save, Cancel, Revoke). A changed Valid
  until extends the grant; otherwise the note and mark change in place.
  An extension now keeps its predecessor's valid_from.
- Member: a held rung reads Granted and is reached by cancelling; the
  cancel confirmation names each ladder's outcome; Cancel plan sits
  beside Keep plan while a downgrade is scheduled.
- Smaller fixes from the screens reviews: 24-hour times everywhere,
  search placeholders that name what they match, a checkbox label above
  its box, and the org-types walkthrough restoring the default it
  changes.
2026-10-01 18:16:40 -05:00
cgalo5758 56e65b669c Read invoices back from Stripe and archive the change
Stripe delivers webhook events in the endpoint's API version. Under a
version later than the 2025-02-24.acacia stripe-go v81 pins, the
payload-shaped invoice handlers projected every invoice with no
subscription link and no lines, and every payment with no payment
intent or method.

- Invoice events read only the invoice id and run ReconcileInvoice,
  which reads the invoice, its lines and its payment intent's charges
  back at the pinned version and converges the core rows under a
  per-invoice lock, in any event order. A payment row is one attempt,
  keyed by its Stripe charge (Stripe store migration 00005).
- Two clocks: created_at is when the console wrote a row; invoices gain
  issued_at and payments paid_at (core migration 00022, backfilled),
  and every invoice and payment list, the activity feed and the
  composite's latest invoice read those.
- An hourly and at-boot invoice sweep reads back invoices never read,
  open invoices once a day, and invoices of dead-lettered events; the
  provider page states when it last finished. The first run is the
  repair of invoices projected so far.
- The API version skew is logged once per version, and
  payment_method.attached links payments recorded before it.
- A restricted key now needs Invoices read and Charges read.
- scripts/db-reader-role.sql maintains console_reader, a login role
  that reads the console's tables without personal data, documented in
  docs/database-management.md and checked against a golden column list
  by TestConsoleReaderColumns. The demo snapshot keeps owners and
  privileges so the role's checks hold on it.
- The demo seed and test fixtures carry issue and paid times.
  status/design-feedback.md gains four upstream notes, one written when
  #60 was closed.

OpenSpec change stripe-invoice-reconcile is archived and its deltas
synced into openspec/specs/. Follow-ups are #150, #151 and #152.
2026-09-28 18:42:29 -05:00
cgalo5758 27fb02fbfa Show members who bills them before they pay, and archive the change
Stripe Checkout, receipts and the card statement carry the Stripe
account's public business name and statement descriptor, which Stripe
fills from the registered legal name until they are set. Where that
name differs from the deployment's, a member first meets it above the
card fields (#13). Each install has to notice this on its own, since a
fix in one account's settings reaches no other deployment.

- A Temporal workflow reads the public details at every boot and on
  request, with whatever permission the key holds: the account itself
  with Accounts Read, otherwise an invoice preview and the latest
  charge. The result is a JSON instance setting; the account id is
  never stored.
- The Stripe provider page gains a Public details section. Its Read
  again control, and the Environment check's Check now, move into the
  section header as a refresh action.
- The Products page and the checkout return name who bills the member
  and what the card statement says, only when a name would not be
  recognised against deployment-name. Every control that costs money,
  the switch preview's commit included, references the line through
  FormView.CommitDescribedBy.
- docs/stripe.md gains "What members see"; production-deployment.md
  points to it. golang.org/x/text becomes a direct dependency for the
  name comparison.

OpenSpec change merchant-disclosure is archived and its six deltas
synced into openspec/specs/.
2026-09-28 18:39:41 -05:00
cgalo5758 fba272fedb Charge upgrades now and defer downgrades to renewal
A paid-to-paid switch sent create_prorations, which charged nothing
now: the difference waited on the next renewal invoice, an immediate
cancellation lost it, and the confirmation's "charged now" was false.
A downgrade took effect at once, so moving to a cheaper paid tier cost
more access than cancelling (OpenSpec change plan-switch-timing,
unarchived: it awaits the maintainer's review, task 11.4).

- An upgrade is invoiced at once (always_invoice) and applies once paid
  (pending_if_incomplete). A declined card voids the invoice and leaves
  the plan as it was; a card that needs the bank's approval links to
  Stripe's invoice page. The preview pins its proration moment and the
  switch charges at it, so the amount charged is the amount shown.
- A downgrade takes effect at renewal through a subscription schedule
  and is recorded in the scheduled-change ledger.
- Every paid plan's card offers Cancel plan; a scheduled downgrade or
  cancellation shows on the card with Keep, which drops it at no
  charge. A paid rank-0 tier is a downgrade target, and a rung a grant
  holds moves up through Checkout with no move down.
- Reconcile keeps the ledger true to Stripe, and marks ended the
  subscription item a schedule's phase change replaced (core migration
  00021, subscription_items.ended_at). The sweeper reconciles a
  period-end move instead of firing it.
- The form library gains a commit that is a link out of the console.

The verify skill and the first-contact process now say the gap in
scripted screens is tracked as #118 and that a new issue waits for the
maintainer's approval.
2026-09-28 18:37:10 -05:00
cgalo5758 94334c0e64 Document CSRF removal in favor of cross-origin checks
The server no longer uses CSRF tokens, relying instead on the origin
check in `internal/middleware/csrf.go`. Update all documentation, code
comments, error handling, and specs to reflect the new protection model
and remove the `csrf-secret` configuration key.
2026-09-21 14:35:35 -05:00
cgalo5758 b245bdb0a9 Fix security audit findings from 2026-09-21 scan
Remediate six confirmed security issues: deployment-only config keys,
bounded provider responses, short-lived registration sessions, private
init file mode, FedWiki workflow authorization, and switch preview
gates.

- Add DeploymentOnly config key declaration; refuse runtime overrides
  for keys that decide where secrets are sent
- Create httplimit package; bound all provider response reads at 8 MiB
- Set fifteen-minute deadline on /register sessions
- Write mc-config.yaml with 0600 permissions
- Derive FedWiki workflow IDs from site IDs; re-authorize sites before
  mutating activities
- Apply switch authorization gates to the proration preview
2026-09-21 13:57:57 -05:00
cgalo5758 d24fa10b8d Remove milestone references from docs and comments 2026-09-21 02:58:07 -05:00
cgalo5758 bea0ffc024 Add Codex CLI harness to agent runner
Support Codex CLI as a second agent harness alongside opencode, with
per-harness credential mounting, model routing, and reasoning effort
handling.
2026-09-21 02:42:53 -05:00
cgalo5758 0c78d5f425 Move issue tracking to the forge tracker
Replace status/issues.md and status/milestones.md with the Gitea forge
tracker, add issue templates, and archive pre-forge issues as YAML.
2026-09-21 02:35:27 -05:00
cgalo5758 b7a0e15574 Record Stripe environment on mappings and verify ids under the current
key
2026-09-20 01:51:45 -05:00
cgalo5758 9114271cc6 Right-align every Actions column and hide its header
Spread the Rules table's treatment to every table that carries verbs
(record-table-actions, archived 2026-09-19): the table is align-middle,
the Actions header and cells are text-end, every button in an Actions
cell carries ms-1, and the header's word is for assistive technology
only (a visually-hidden span; Primer: row actions do not require a
visible column header). The two integrations tables and the member
domains list, which headed their controls with an empty cell, take the
hidden word too. A new anatomy-lint rule, actions-column, refuses a
table that drifts from any of it.

Decisions on the way: a row tint marks the row in play (the current
workspace, a staged change), never a record's status; the member catalog
lists only what can be bought, so a Listed product without an active,
synced, recurring price is left out instead of shown with "Not available
for purchase"; two Actions cells that carried text in a verb's place are
empty (the Placements column and the pending panel already say why); and
four record tables gain their width floor.

Specs: page-anatomy "Tables share one density" modified, ui-quality-gate
gains "Lint refuses an Actions column without its treatment",
member-product-discovery's Extras bucket and truthful-copy requirements
modified. docs/design-system.md §6 states the treatment and its reasons.
2026-09-19 22:31:45 -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 5829091881 Track the agent runner and its method page
The contained runner that drove the 2026-09 security audit, the README
review and four rounds of design ideation lived only inside the ignored
notebook. It moves to scripts/agent-runner/: the prepare, tools, audit,
ideation and teardown scripts, the compose and container files, the
allowlist proxy and the prompt templates, with the paths that assumed
the notebook fixed and findings written to a caller-named directory. Run
outputs, transcripts and the round-specific sheet scripts stay behind.

docs/agent-runner.md states the method: the disposable git archive
copy and the fail-closed proxy, how a task is shaped, union rather than
intersection of findings across models, separate adjudication of every
finding against the source, and the evidence a finding must carry.
2026-09-19 19:47:15 -05:00
cgalo5758 a5a4b9c591 Gate archives on notebook citations and add the harvest skill
A notebook's lessons survive only if they are written into a tracked
home before the decision lands, so landing a change now has three
pieces. The contract: a "Landing a change" section in MAINTAINING.md and
a fifth item in the UI definition of done. The labor: a harvest skill
that inventories the notebook, names each file's kind and routes
findings to the owning docs page, reasoning to the change's design.md,
problems to issues.md and reusable tools to a tracked home. The gate:
scripts/notebook-citations.sh, a git grep over what git would commit for
the notebook's directory shape, which make lint runs after the program's
own lint, and scripts/archive-gate.sh, which refuses an mv into the
archive or an openspec archive while the check reports hits.

The gate is one script that any harness calls with the command it is
about to run; Claude Code reaches it through a PreToolUse hook and
opencode through a plugin. Both checks live in scripts/ rather than the
lint subcommand because they are repository hygiene, not part of the
shipped program. The generated openspec-archive-change skill is not
edited; the post-archive reminder carries the harvest step instead.
2026-09-19 19:47:15 -05:00
cgalo5758 660485d9b8 Make status/explorations a local notebook git ignores
The exploration directory held 245 tracked files, and one week's
notebook had grown to 28M of captures, per-run CSS copies and model
logs, so every change ended with an argument about what to prune. It is
now a local lab notebook: git ignores it, its tracked files are
untracked, and no tracked file cites a path under it, because another
clone does not have it.

Every live citation is rewritten so the fact stands in the sentence:
the docs pages, AGENTS.md, four main specs, one Go comment, issues.md
and milestones.md. The operator IA page gains the landed IA revisions it
used to point at, and the UX rubric moves to docs/first-contact-rubric.md
because the process requires it. Archived changes, status/archive,
status/log and design/ keep their old paths as frozen history; the
design/ ones are logged as an upstream issue. MAINTAINING.md and the
status README describe the notebook and the harvest that replaces
pruning.
2026-09-19 19:46:52 -05:00
cgalo5758 f8a3478f2a Rebuild the entitlement set Rules surface as a staged batch
The Rules section is one record table grouped by kind, Limit then
Boolean, on fixed columns, edited in place: Edit opens a row's controls
in their columns, Add rule opens a dense row above the table, and every
change is staged into a tray that lists the deltas with Undo and applies
them as one rule-change act. The reduction policy is a column of the
rule beside its limit. History shows counts only. Group rows are a quiet
heading rather than a divider, the maintainer's pick from four rounds of
outside-model ideation.

Dense rows align to the top and render each error under its control in
every form family (design D16), replacing the below-row error block; the
forms library gains the batch form (rows plus one tray) and the RowField
dense and label-hidden options. Migration 00019 records the governing
reduction policy on effect rows.

Archive staged-rule-changes with its spec updates (entitlement-set-
management, entitlement-set-history, entitlements, form-library,
form-conventions, ui-quality-gate). Screens accepted 2026-09-19.
2026-09-19 19:46:09 -05:00
cgalo5758 3727ff31d8 Add entitlement set rule change ledger and preview flow
Add an append-only ledger of entitlement set rule changes with per-pool
effect rows, a preview-and-commit rule change flow, and an automatic
drain that settles deferred recomputations. Rules gain a tier reduction
policy, resource keys declare over-limit behavior, and the materializer
now lowers limits when a rule stops applying.
Add entitlement set rule change ledger and preview flow

Add an append-only ledger of entitlement set rule changes with a
preview-and-commit operator flow. Rule writes now go through an enclosed
`core.commit_rule_change` function that files an act row and one
obligation per carrying pool, with a drain workflow settling deferred
recomputations. The preview dry-runs the materializer with a rule
overlay and renders per-pool buckets, reduction-policy disclosures, and
provider over-limit consequences. Materializing transactions take a
shared advisory rendezvous that rule changes hold exclusively, enforced
by a possession assertion. Add History and Entitlement changes surfaces,
a rule-less warning on five product-selection surfaces, and a
`tier_reduction_policy` column that gates FedWiki parking.
2026-09-15 03:53:28 -05:00
cgalo5758 782ca8f326 Derive Stripe mode from API key and refine disabled controls
Derive Stripe test/live mode from the API key prefix at boot, failing on
unrecognized prefixes, and drop the separate `stripe-mode` config key.

Refine disabled controls to render through the shared `disabledControl`
part with the not-allowed cursor, and add a lint rule refusing
hand-rolled disabled buttons.

Adjust plan cards to offer no purchase control on free rungs, fix bound
checkbox Bool handling, and rename "Public/Private" to "Listed/Unlisted"
with enhanced readiness verdicts.
2026-09-13 16:59:12 -05:00
cgalo5758 f8b4b98c7c Document the Stripe webhook API version pin and payment_method.updated
The webhook endpoint must be created at 2025-02-24.acacia, the version stripe-go/v81 pins; Basil and later drop the invoice.subscription and invoice.payment_intent fields the invoice projection reads. The handler also acts on payment_method.updated, which the event list omitted.
2026-09-12 01:16:17 -05:00
cgalo5758 3283df19b1 Move account menu into mobile rail drawer
Below lg the top bar now holds the brand and one toggler; the account
items render in the drawer as a second, labelled list from a shared
partial, so both surfaces cannot drift. Desktop unchanged.
2026-09-09 22:57:26 -05:00
cgalo5758 ea4fee18b6 Fix five findings from security audit run 2
- Rotate the session token at the OIDC callback and restore the full
  lifetime; cap pre-auth sessions at 15 minutes and write no session
  for bare anonymous requests
- Treat db-dsn as a secret: accept db-dsn-file, log only host, port,
  database and user, and never echo a malformed DSN in an error
- Guard the logout callback with a state cookie so a forged visit
  cannot end a live session
- Collapse FedWiki site actions on a foreign tenant's domain to the
  not-found answer, as for a domain that does not exist
2026-09-09 20:53:31 -05:00