3 Commits
Author SHA1 Message Date
cgalo5758 b0df28647f Move organizations to a new default as obligations an act owns
A default change saved the new default outside any lock, then moved
every organization of the type inside the request, from a list read
after the save. At size the request ran out of time. A failure part way
left the rest unmoved, with "Details are in the server logs." as the
only answer and committing again as the only re-run (#187).

Migration 00028 gives the default change work it can own and resume:

- core.plan_act_reached_org_types records which types an act reaches.
  core.plan_act_owns_org_type states once that the newest act to reach
  a type owns it, so a newer act takes over what an older one has not
  moved.
- core.plan_act_obligations holds one row per organization the act must
  move. core.plan_act_orgs_without_pool records an organization with no
  default pool, which is logged and counted, not owed.
- core.plan_act_listing_frontier states once which organizations the
  listing has not reached: the writer, the listing-done read, the
  poller's read and the card's count all call it, and PostgreSQL
  inlines it into each.
- Four enclosed writers list, settle, record a failure (failed after
  five attempts) and requeue. core.record_plan_act now writes the
  reached types and refuses a reorder detail that disagrees with the
  ladder. core.grants.act_id names the act a grant came from.

The commit's first transaction runs on the runner under the exclusive
rendezvous, inside a 5 s deadline. It locks the type and refuses a
reserved type, a candidate that is already the default ("Already the
default.") and a ladder with no rank 0. It then saves the default and
records the act. With no disposition given, it rolls back when an
organization holds the outgoing default, and the 422 preview is rebuilt
after the rollback through the capped read. A listing then writes the
obligations in batches of 500, in org type and organization id order.

Each move runs in its own transaction under the shared rendezvous. It
locks the pool, locks the obligation together with its type's owner, and
reads the current default and its rank 0. It then migrates or
grandfathers the organization, settles it as already at the default, or,
under an act with no disposition, leaves it untouched with a Warn line.
A failure is recorded on the obligation alone with its step, in a plain
transaction that takes no pool row. The request tries up to 50
obligations within 2 s. Its lock and transaction timeouts are 500 ms and
2 s, and a move they cut records no attempt. It then starts
PlanActDrainWorkflow for the rest; PollPlanActs starts a drain for any
act still owing work. The toast counts what is not yet reached and what
has no default pool, and leaves out any count it could not read.

The org type's card shows each open act's lines: organizations not yet
reached, and those not moved, each with its reason, with Retry. They
are read in two statements in one read-only repeatable-read
transaction, and "Pending plan changes could not be read. Reference:
{request id}." replaces them when that read fails. Retry is an act of
its own: it requeues the failed obligations on the types the act still
owns, and a retried organization is recorded under the operator who
pressed Retry. It answers a lock wait with the collision sentence and
any other failure with "Failed to retry. Reference: {request id}.".

Along the way:

- The default change's sentences ending "Details are in the server
  logs.", in six places, now end "Reference: {request id}.", logged
  through the request's logger.
- The select's label becomes "New default", so the form library's
  refusal reads "Choose a new default from the list." and its required
  message "Choose a new default.".

The change's specs now carry that label for org-types and form-library.
The design states the frontier function and that the request's cap
counts obligations tried, not settled. docs/database-locks.md,
docs/plan-management.md and the two docs quoting the label follow the
code.

Goldens:

- Behaviour changes in contention, default-already-at, default-migrate,
  default-none-migrate, default-needs-disposition, default-begin-fails,
  default-initiate-fails, default-migrate-restore-fails,
  default-grandfather, default-change-twice, default-current-refused,
  default-save-fails, the four removal-against-default files and the
  four tier-change-during-conferral files.
- The three default form goldens move with the label. routes.golden
  gains the Retry route's probes.
- default-list-fails is retired: no listing runs outside the act any
  more. New: default-listing-fails, default-superseded,
  default-request-cap, default-untouched-outgoing, default-retry,
  default-retry-nothing, default-preview-capped and
  default-gate-true-preview-empty.
- The other 40 plan-act goldens gain only the reached-type,
  without-pool and obligation sections and the grants' act column.

The full suite passes.
2026-10-19 19:54:16 -04:00
cgalo5758 ab1c0dbb69 Write the list search's hidden sibling inputs in sorted order
An embedded list's search form re-submits its sibling lists' state as
hidden inputs. listSearchForm added them by ranging over a map, and a
GET form submits its inputs in document order, so with two or more
siblings the parameters of the URL the search pushes came out in a
different order from one render to the next. They are now added in the
sorted order of their keys, so the order is the same on every render.
With one sibling, or none, the HTML is unchanged.

TestListSearchFormCarriesSiblingInputsInSortedOrder renders the form
20 times with three siblings and checks each render for key order. It
failed in 3 of 3 runs at the parent and passed in 3 of 3 at this
commit. The new file
internal/server/testdata/forms/per-request/list-search-three-siblings.html
pins the three-sibling HTML as the other per-request files do, and the
comment in forms_golden_test.go that explained why the list search was
rendered with one sibling is removed. No existing golden file changes.
No assertion of TestFormsHTML changed; it gains one case.

How it was shown to change no other output: the names and results of
./internal/server (the whole package, on the test databases) are equal
at the parent and at this commit apart from the two added lines, the
new test and the new TestFormsHTML/per-request/list-search-three-siblings
case, none failing. One break (the sorted keys walked backward) is
caught by the new test. The demo-state walkthroughs give 16 pass and
6 skip at the parent and at this commit.

Not probed: that a browser submits the inputs in document order. The
HTML specification says it does, and no test here drives a browser's
form submission.
2026-10-11 21:47:51 -05:00
cgalo5758 bc4a933a3d Pin the HTML of every registered form
TestFormsHTML renders each declaration in every binding the library
renders it in and compares the text, through golden.Compare, with a file
under internal/server/testdata/forms. A form that is not a rows form is
rendered unbound, on the edit side and on the create side, unbound with
values, and as a record (every field bound to a value its rules accept,
three cells per field, an instance and a message); a search is then
rendered boosted and with a target, a select and an out-of-band select,
and any other form is rendered refused (an empty POST parsed through the
declaration) with the refusals of a 600-character value in every field.
A rows form is rendered unbound, then with an open row, a staged delta
and the tray (or the row's own commit), each row field and one row
action. Runtime options are a fixed "Choose one", Alpha and Beta.

The subtests are grouped in three. registered/<name> holds the 19 forms
forms.All returns in the internal/server test binary; the files
number 26 in all. per-request/<case> holds the declarations built per
request: the settings form, the org type card's two copies, the rules
form, and the list search embedded (one sibling parameter) and
standalone. library-defaults holds a table's "Field" and "Value"
headings, a textarea's three rows and the htmx verb of GET, PUT, PATCH,
DELETE and POST on a form that is not a search.

TestFormsGoldenFilesAreAllRendered fails for any file under
testdata/forms that TestFormsHTML does not write, so a form that leaves
the registry leaves no stale file. TestPerRequestFormsSatisfyTheInvariants
runs CheckInvariants over the org type card's two copies and the rules
form's copy; the settings form has its own test, and the list search
builds its declaration inside listSearchForm and never returns it, so no
test can pass it to CheckInvariants and it stays unchecked.

Shown to change no output: only a test file and its goldens are added.
The names and results of ./internal/forms ./internal/server
./internal/integrations/... (2113 at the parent, 2144 here, none
failing, one skip on each side), taken with the test database reset and
the app down at the parent and at this commit, are equal apart from the
new tests and their 28 subtests.
The registered forms' files in name order followed by the defaults' file
are byte-equal to the output of a standalone program that renders the
same bindings, run at 0abbfac7; no file
under internal/forms, internal/server or internal/embeds that is not a
test or a golden has changed since. A separate render of the per-request
declarations, unbound only, equals the unbound section of the org type
card's files and of the rules form's file apart from the runtime options
it did not bind, and equals the list search's two files exactly. The
other bindings and the settings file were read against their
declarations, not compared with another render. Three runs in a row
produce the same files, with no -update.

22 breaks in the form library's render.go and the three server files of
the per-request cases, each edited once and each caught by the new
tests. The old side has no counterpart: every break is a new-side
record, since no old test names these. None dropped.

Pinned defects, so a later fix flips them by name: the list search's
hidden sibling inputs come out in map order with two or more siblings
(rendered here with one, so the file does not vary from run to run);
the library defaults no registered form reaches, which every earlier
test missed.

Not probed: the templates' own markup, because the files hold it for a
later template change to show while the breaks probe the view the
library builds; fieldAttrs, buildRows, RowField and resolveRowsFocus,
whose output is in the files; the list search's per-request declaration
against the invariants, for the reason above; the stale-file check,
which guards the test's own files. A dense checkbox's label position is
the one render break this net misses, since no registered form has a
dense checkbox; TestFormPartCheckboxLabelSitsAboveTheBox catches it.
2026-10-11 15:55:41 -05:00