# QA: First-time language setup Accepted together with features/language_setup.feature. ## Automated, no server (task test) 1. Use case over in-memory fakes: "Apply for every pending site", "The owner may change a suggestion", "Retrying is safe", "A site with no articles still needs confirming", refused rows. Assert sentinels with errors.Is. Assert that the whole apply is one Transactor call and that site-row locks are taken in ascending site id order. 2. HTTP: NewHandler over the use case, a fresh chi.NewRouter mirroring api/router.go under /v1/language, behind require(role.KeySettingsManageSites), validator.InitValidator(), httptest. Cover 200, 400, 401, 403, 404, 409 and 422 with exact plain-text bodies and the JSON key set. setup_in_progress is simulated by a fake whose apply returns the sentinel. 3. Regression: slice 1 tests ("Create racing a confirmation", "Assigning a language is idempotent") untouched. ## Automated, SQLite file (task test) 4. Fixture: the four sites above with 8, 3, 5 and 0 articles, one soft-deleted. POST applies; assert ids, titles, bodies, slugs, is_online, story ids and timestamps unchanged; soft-deleted articles are assigned too; no pending site remains; a second POST changes nothing (hash of all rows). A site created after migration with no row is listed and can be confirmed. 5. Atomicity: inject a failing write on the second site; assert no row is confirmed and no article is assigned; the next POST completes. 6. Concurrency: goroutines creating articles during setup; every final lang equals the confirmed tag; bound the SQLITE_BUSY retries. Two concurrent POSTs: one 200, the other 200 or 409 setup_in_progress, never a deadlock. ## Automated, real databases (task test:mysql, task test:postgres) 7. One targeted run of steps 4 to 6 on MySQL 8.4, MariaDB 11 and PostgreSQL, sharing the run planned in the site_languages QA. Check lock ordering for deadlocks. Record NOT RUN if the daemon is down, or use the podman shim. ## Automated (Vitest + Vue Test Utils, task test:web) 8. One spec per row of "Setup states". Assert the copy, the disabled confirm button until every row has a language, that Journal and Archive are seeded and Notes is not, the single POST body, and the 409 / 500 transitions. 9. Mock set: GET with Journal, Notes, Archive (8, 3, 5 articles); GET with only Notes; GET with only Gallery; GET with nothing pending; GET with a substituted suggestion; POST 200 { assigned: 16 }; POST 409; POST 500. 10. Route guard: a reader is redirected; an owner reaches the page; the back link goes to /settings/sites/:key/languages, and to Settings, Sites for an unknown :key. 11. The Languages page banner links to /settings/sites/:key/languages/setup when needs_setup is true. 12. task test:web, the type check, bun lint, and a diff check for comments. ## Visual (task visual:check -- localization-site-languages, hard gate at 10 %) 13. Run once at the end of the slice (group L2, 7 states) with the board fixtures Journal, Notes, Archive and Gallery (SETUP in locale-fixtures.js: 8, 3 and 5 articles). Desktop 1440 and mobile 390, light and dark. 14. Not checked visually (no frame): nothing-pending, unknown :key, substituted seed. ## Assumptions recorded for gaps without a frame - Nothing pending: the Done alert without a count. - Substituted seed: the Languages "broader" wording. - Unknown or complete :key: the global list is shown and the back link falls back to Settings, Sites. - Setup route: one global page (Option 1), reached from a per-site banner. ## Fix round additions - Skipped list and stable lock loop: TestLanguageSetupSkippedChoices, TestLanguageSetupLocksUntilStable (test/localization_fix_http_test.go). - Setup activity events: TestLanguageActivity. - Two simultaneous setups on SQLite: TestLanguageConcurrencyOnSQLite. ## Manual - Keyboard order and focus return after "Try again" and "Check again". - Real two-owner run: start setup in two browsers and confirm the second sees "Setup is already running". - A copy of a real database with articles: run setup, check the articles list and one public read still match, then restart and confirm nothing changes.