Skip to content

bootstrap-admin does not print the farm code, so first-run login is blocked #589

Closed

Description

@mforce

Amendment (2026-08-24): this body's claim that no test asserts bootstrap-admin's stdout is
false — BootstrapAdminCommandTests.cs has a stdout harness and asserts on it. The real
diagnosis, and a correction to the "mirror provision-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, matching provision-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 BootstrapAdminOutcome already 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-admin printed, with no access to the database, the migrations, or the test fixtures.

Activity

  1. added
    bugSomething isn't working
    area:apiAPI/endpoint layer
    on Aug 24, 2026
  2. added
    sliceThin vertical work item
    epic-1.6Phase 1.6 — Multi-farm tenancy
    on Aug 24, 2026
  3. mforce commented on Aug 24, 2026

    @mforce
    OwnerAuthor

    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.cs has a stdout harness and five cases, and the first-run case asserts on stdout directly:

    var (exitCode, stdout, stderr) = await RunBootstrapCommandAsync(email);
    Assert.Contains(email, stdout);
    Assert.Contains("Temporary password:", stdout);
    var tempPassword = ExtractTemporaryPassword(stdout);

    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-Contains test 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:

    • The harness already exists — RunBootstrapCommandAsync captures (exitCode, stdout, stderr). The new test is an addition to a good existing fixture, not new scaffolding.
    • The mutation to prove the guard is real is unchanged: remove the farm code from the output and the new assertion must go red. But note the existing assertions will stay green under that mutation, which is exactly the point.

    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:

    fields
    FirstRunAdminOutcome bool WasAlreadyProvisioned, string? Email, Guid? AccountId, string? TemporaryPassword — all nullable, has a no-op path
    AccountProvisionOutcome Guid AccountId, string Slug, string OwnerEmail, string TemporaryPassword — all non-null, no no-op path

    So Slug must be added as string?, null on the AlreadyProvisioned() branch — not copied as the non-nullable string the other record uses. Field naming differs too (Email vs OwnerEmail); do not "harmonise" that as a drive-by.

    Where the slug comes from, with no extra query: FirstRunAdminService.cs:121-123 already reads the accounts table to confirm the default account exists —

    var accountExists = await db.Accounts.IgnoreQueryFilters().AnyAsync(a => a.Id == accountId, ct);

    That AnyAsync can select the slug instead, keeping it one round trip and reusing the existing Bootstrap.AccountMissing failure path for an absent row. The literal "default-farm" stays only in the migration; nothing new hardcodes it.

  4. added a commit that references this issue on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layerbugSomething isn't workingepic-1.6Phase 1.6 — Multi-farm tenancysliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions