Repository navigation
Description
Amendment (2026-08-24): this body's claim that no test asserts
bootstrap-admin's stdout is
false —BootstrapAdminCommandTests.cshas a stdout harness and asserts on it. The real
diagnosis, and a correction to the "mirrorprovision-account" instruction (the two outcome
records diverge on nullability and the no-op path), are in the first comment below. Read that
before planning. Body left as written for history.
Found by the owner running the documented first-run flow on a fresh database.
Problem
bootstrap-admin prints the email and the generated temporary password, then says "Log in with this now — the app requires a new password before anything else works."
The operator cannot log in. Since #532, the sign-in form requires a farm code, and bootstrap-admin never prints one. Actual output:
First-run admin provisioned: admin@cluckwork.local (account 0000000a-0000-0000-0000-000000000001).
Temporary password: <generated>
Log in with this now — the app requires a new password before anything else works.
The value they need is default-farm, and the only ways to discover it are reading 20260818235944_AddAccountSlug.cs, querying Accounts."Slug" directly, or reading the k6/Playwright fixtures. None of those is a first-run operator's job — and this is the first command anyone runs against a new deployment.
The sibling verb already does it right
ProvisionAccountCliCommand.cs:66 — written after farm-code login landed:
$"Farm provisioned: {outcome.Slug} (account {outcome.AccountId}); Owner {outcome.OwnerEmail}."BootstrapAdminCliCommand.cs:69 — predates it, never swept:
$"First-run admin provisioned: {outcome.Email} (account {outcome.AccountId})."Why it slipped
This is exactly what AGENTS.md's write-contract callers rule (#394) exists to catch: "A write-contract change must update its non-CI callers... Verify the request/status contract by reading the callers, not by running." #532 made farmCode a required part of the login contract. provision-account was authored with that contract in view; bootstrap-admin was not revisited, and no test asserts what it prints, so nothing went red.
Scope
- Print the farm code in
bootstrap-admin's success output, matchingprovision-account's shape. The account id is far less useful to the operator than the slug — keep both, but lead with what they must type. - Check whether
BootstrapAdminOutcomealready carries the slug; if not, thread it through from the account rather than re-deriving or hardcoding"default-farm"at the call site. The literal must not be duplicated into the CLI — it lives in the migration, and a second copy is a drift hazard. - Not in scope: changing the slug value, or the provisioning behaviour itself.
Tests
A CLI-level test asserting the success output contains the farm code. There is currently no test asserting bootstrap-admin's stdout at all, which is why this shipped — so the test is the point of the issue as much as the fix is.
Mutation to verify the guard is real: remove the slug from the output string and confirm that named test goes red. A test that only asserts "output is non-empty" or greps for "provisioned" would not catch this.
Docs
bootstrap-admin is described in AGENTS.md and in the first-run docs. If any of those show sample output, update it in the same PR per the docs-in-sync directive. The SPA Help page's first-run copy (signingInFirstRun in web/src/i18n/en.ts) tells the operator an administrator runs a command — check whether it should also say the command prints the farm code they will need.
Acceptance
- A first-run operator can complete sign-in using only what
bootstrap-adminprinted, with no access to the database, the migrations, or the test fixtures.
Activity
mforce commentedon Aug 24, 2026
Correction to this issue's body — the "no test" claim is wrong
I wrote, under Tests: "There is currently no test asserting
bootstrap-admin's stdout at all, which is why this shipped."That is false.
tests/Cluckwork.Api.IntegrationTests/BootstrapAdminCommandTests.cshas a stdout harness and five cases, and the first-run case asserts on stdout directly:The real diagnosis is more precise, and more useful: the test asserts what the output does contain, never what it must contain. It is a positive-
Containstest with no completeness requirement, so when #532 made the farm code a required login input, nothing in this suite had any reason to go red. The output grew a missing obligation, not a broken assertion.That changes the fix, in a way worth knowing before picking this up:
RunBootstrapCommandAsynccaptures(exitCode, stdout, stderr). The new test is an addition to a good existing fixture, not new scaffolding.Also corrected: the outcome types are NOT a simple mirror
The body says to match
provision-account's shape. Diffed field by field, they diverge three ways:FirstRunAdminOutcomebool WasAlreadyProvisioned, string? Email, Guid? AccountId, string? TemporaryPassword— all nullable, has a no-op pathAccountProvisionOutcomeGuid AccountId, string Slug, string OwnerEmail, string TemporaryPassword— all non-null, no no-op pathSo
Slugmust be added asstring?, null on theAlreadyProvisioned()branch — not copied as the non-nullablestringthe other record uses. Field naming differs too (EmailvsOwnerEmail); do not "harmonise" that as a drive-by.Where the slug comes from, with no extra query:
FirstRunAdminService.cs:121-123already reads the accounts table to confirm the default account exists —That
AnyAsynccan select the slug instead, keeping it one round trip and reusing the existingBootstrap.AccountMissingfailure path for an absent row. The literal"default-farm"stays only in the migration; nothing new hardcodes it.fix(api): print the farm code from bootstrap-admin (#589)
fix(tests): update the tenant-bypass allow-list for the new signature (…
fix(api): print the farm code from recover-admin too, and pin it agai…
docs(runbooks): point at list-accounts for farm-code recovery, and co…
fix(tests): close the re-run farm-code guard gap and drop stale exclu…
docs(web): tell a user who does not know their farm code who to ask (#…
fix(api): drop a cast that changed nothing, and say what the guard ac…
docs(api): stop the farm-code comment contradicting itself (#589)
fix(api): print the farm code from bootstrap-admin (#589) (#594)
chore(main): release 0.1.0 (#542)