fix(db): own the two settings seeds, and repair the protocol-3 one-shot (phase 3, slice 4) #5

Merged
whitlocktech merged 2 commits from feature/own-settings-seeds into main 2026-08-12 03:01:34 +00:00
Member

The module-uo half of Phase 3 slice 4. Pairs with website#140 (merge this one first) and docs#139.

What moves

Core's schema.sql seeded two settings rows that name a game concept: game_account_signup — the row slice 3 deliberately kept the key of — and the one-shot migration marker uo_link_protocol_3_migrated. Both INSERT IGNORE statements move into this module's fragment. The keys are unchanged: they are live rows on every existing install, and renaming one silently resets an operator's choice to the default.

The second one was a live defect

Pre-extraction these two statements were adjacent in core's schema:

UPDATE uo_link_config SET protocol = 3
  WHERE id = 1 AND protocol < 3
    AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_3_migrated');
INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_3_migrated', '1');

Slice 1 moved the UPDATE here and left the INSERT in core. The two files do not run together — core's schema is replayed in full before any module fragment (MODULE_API.md §2.6) — so the marker existed before the guard ever read it, and the one-shot could never fire. An install carrying a protocol-2 row would have stayed pinned at 2 against a v3 sidecar: every REST call 409, the WS closed on ws.hello. That is exactly the failure the migration exists to prevent.

Latent rather than live, and only because edge has not cut over: it bites an install that first boots a post-slice-1 build while already holding a uo_link_config row.

Verification

Against a real MariaDB, all three states:

Start After boot
fresh database both rows written, nothing to migrate
protocol = 2, no marker moved to 3, marker written
protocol = 2 pinned back, marker present stays at 2 across a restart

The third is the half that makes it a one-shot rather than a re-bump — an operator who deliberately pins an older sidecar in Admin → Shard has to stay pinned.

Tests

server/test/schemaFragment.test.js (new, 8 tests) asserts the order, because that is the property that broke and nothing else can see it: both files were individually valid SQL and both replayed cleanly. It also restates the fragment rules core validates at load time — leading-verb allowlist, IF NOT EXISTS, grandfathered table prefixes — for the same reason manifest.test.js restates the manifest rules: a mistake should fail in this repo's CI, which can say what is wrong, rather than on an install, where the symptom is a module that is simply absent. Its statement splitter is a character walk, because a comment in this file contains quotes.

client/test/api.test.js (new, 6 tests) takes the five assertions that stayed behind in core's apiClient.test.js when the bindings moved in slice 1 — the atlas-vs-/shard path split, the query-string filtering, the slug encoding, the admin atlas methods — plus a new one pinning the seven admin URLs the shipped Android app calls by name. Its fake window.__rg carries the real react/react-dom/router rather than stubs: src/core.js compares its imported bindings against the published ones, so stubs make every run print the exact wording of a real defect.

367 server (+8) and 40 client (+6) tests pass; chunk builds clean, check:imports and check:externals both OK.


  • AI-assisted (Claude Code)
The module-uo half of Phase 3 slice 4. Pairs with **website#140** (merge this one first) and **docs#139**. ## What moves Core's `schema.sql` seeded two settings rows that name a game concept: `game_account_signup` — the row slice 3 deliberately kept the *key* of — and the one-shot migration marker `uo_link_protocol_3_migrated`. Both `INSERT IGNORE` statements move into this module's fragment. The keys are unchanged: they are live rows on every existing install, and renaming one silently resets an operator's choice to the default. ## The second one was a live defect Pre-extraction these two statements were adjacent in core's schema: ```sql UPDATE uo_link_config SET protocol = 3 WHERE id = 1 AND protocol < 3 AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_3_migrated'); INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_3_migrated', '1'); ``` Slice 1 moved the `UPDATE` here and left the `INSERT` in core. The two files do not run together — core's schema is replayed **in full** before any module fragment (MODULE_API.md §2.6) — so the marker existed before the guard ever read it, and **the one-shot could never fire**. An install carrying a protocol-2 row would have stayed pinned at 2 against a v3 sidecar: every REST call 409, the WS closed on `ws.hello`. That is exactly the failure the migration exists to prevent. Latent rather than live, and only because `edge` has not cut over: it bites an install that first boots a post-slice-1 build while already holding a `uo_link_config` row. ## Verification Against a real MariaDB, all three states: | Start | After boot | | --- | --- | | fresh database | both rows written, nothing to migrate | | `protocol = 2`, no marker | **moved to 3**, marker written | | `protocol = 2` pinned back, marker present | **stays at 2** across a restart | The third is the half that makes it a one-shot rather than a re-bump — an operator who deliberately pins an older sidecar in Admin → Shard has to stay pinned. ## Tests `server/test/schemaFragment.test.js` (new, 8 tests) asserts the **order**, because that is the property that broke and nothing else can see it: both files were individually valid SQL and both replayed cleanly. It also restates the fragment rules core validates at load time — leading-verb allowlist, `IF NOT EXISTS`, grandfathered table prefixes — for the same reason `manifest.test.js` restates the manifest rules: a mistake should fail in this repo's CI, which can say what is wrong, rather than on an install, where the symptom is a module that is simply absent. Its statement splitter is a character walk, because a comment in this file contains quotes. `client/test/api.test.js` (new, 6 tests) takes the five assertions that stayed behind in core's `apiClient.test.js` when the bindings moved in slice 1 — the atlas-vs-`/shard` path split, the query-string filtering, the slug encoding, the admin atlas methods — plus a new one pinning the seven admin URLs the shipped Android app calls by name. Its fake `window.__rg` carries the **real** react/react-dom/router rather than stubs: `src/core.js` compares its imported bindings against the published ones, so stubs make every run print the exact wording of a real defect. 367 server (+8) and 40 client (+6) tests pass; chunk builds clean, `check:imports` and `check:externals` both OK. --- - [x] AI-assisted (Claude Code)
wtclaude added 2 commits 2026-08-12 02:55:44 +00:00
Core seeded `game_account_signup` and `uo_link_protocol_3_migrated`, two keys
that name a game concept. That made core's schema declare a module's settings,
which is the structural half of what Phase 3 removes (MODULE_SYSTEM.md §2.7.1,
slice 4). Both INSERTs move here. The keys are deliberately unchanged: they are
live rows on every existing install and renaming one silently resets an
operator's choice to the default.

The marker is not just a tidy-up. It and the `UPDATE uo_link_config SET
protocol = 3` it makes one-shot were adjacent in core's schema.sql until slice 1
moved the UPDATE here and left the INSERT behind — and the two files do not run
together: core's schema is replayed in full before any module fragment. So the
marker existed before the UPDATE ever read it, the NOT EXISTS guard was false on
every boot of an upgraded install, and the migration could never fire. An
install carrying a protocol-2 row would have stayed pinned at 2 against a v3
sidecar, 409ing every REST call — the exact failure the migration prevents.
Latent rather than live: it bites only an install that first boots a
post-slice-1 build while already holding a uo_link_config row, and `edge` has
not cut over.

`schemaFragment.test.js` asserts the order, plus the fragment rules core
validates at load time (leading-verb allowlist, IF NOT EXISTS, grandfathered
table prefixes) — restated here for the same reason manifest.test.js restates
the manifest rules. Its statement splitter is a character walk, because a
comment in this file contains quotes.

Co-Authored-By: Claude <noreply@anthropic.com>
test(client): assert the URLs this module calls
All checks were successful
PR Checks / client-build (pull_request) Successful in 18s
PR Checks / server-tests (pull_request) Successful in 8m45s
d70e5e10d0
Five assertions that stayed behind in core's `apiClient.test.js` when the
bindings moved in slice 1 — the atlas-vs-shard path split, the query-string
filtering, the slug encoding, the admin atlas methods — plus a new one pinning
the seven admin URLs the shipped Android app calls by name. They were asserting
UO URLs from inside core's suite, which is the boundary Phase 3 removes, and
core's slice-4 deletion of those bindings would otherwise have deleted the
coverage with them.

The fake `window.__rg` carries the REAL react/react-dom/router rather than
stubs: `src/core.js` compares its imported bindings against the published ones
and logs a "bundled its own copy" error when they differ, so stubs make every
run of this file print the exact wording of a real defect.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit e468bbd3b9 into main 2026-08-12 03:01:34 +00:00
whitlocktech deleted branch feature/own-settings-seeds 2026-08-12 03:01:35 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-uo#5
No description provided.