diff --git a/loadtest/README.md b/loadtest/README.md index 96f639c..8b071fa 100644 --- a/loadtest/README.md +++ b/loadtest/README.md @@ -11,7 +11,7 @@ and prints a trip-report summary. It stays in the repo for repeats. (each with a confirmed email identity) + `--guest` guest accounts and an active `sessions` row per account, then hands the plaintext bearer tokens to the driver. Token hashes match `backend/internal/session` (`hex(sha256(token))`), so the seeded - sessions resolve. Every row is tagged with the `lt:` marker for cleanup. + sessions resolve. Every row carries a distinctive display-name marker for cleanup. 2. **Drive** (edge protocol over h2c): assembles real 2–4 player games via the invitation flow (`invitation.create` → `invitation.accept`, no robots), then runs each player's turn loop — poll `game.state`, replay `game.history`, generate a legal @@ -52,7 +52,7 @@ resource baseline from the Grafana **Scrabble — Resources** dashboard ``` loadtest run [flags] seed, drive the ramp + hammer, print the report -loadtest cleanup [flags] delete everything the harness seeded (matched by the lt: marker) +loadtest cleanup [flags] delete everything the harness seeded (matched by the display-name marker) ``` Key `run` flags (env in parentheses): diff --git a/loadtest/internal/scenario/scenario.go b/loadtest/internal/scenario/scenario.go index 82a6716..e822722 100644 --- a/loadtest/internal/scenario/scenario.go +++ b/loadtest/internal/scenario/scenario.go @@ -201,7 +201,9 @@ func (d *Driver) secondaryOp(ctx context.Context, p seed.Account, g *Game, rng * c, _ := d.edge.CheckWord(ctx, p.Token, g.ID, []byte{0, 1, 2}) d.rec.Record("game.check_word", c, time.Since(t0)) case 3: - c, _ := d.edge.DraftSave(ctx, p.Token, g.ID, `{"rack_order":[],"board_tiles":[]}`) + // rack_order is an opaque string and board_tiles a (here empty) array, per the + // backend draft DTO; a malformed shape is rejected as bad_request. + c, _ := d.edge.DraftSave(ctx, p.Token, g.ID, `{"rack_order":"","board_tiles":[]}`) d.rec.Record("draft.save", c, time.Since(t0)) case 4: c, _ := d.edge.DraftGet(ctx, p.Token, g.ID) diff --git a/loadtest/internal/seed/seed.go b/loadtest/internal/seed/seed.go index f20014d..8da5417 100644 --- a/loadtest/internal/seed/seed.go +++ b/loadtest/internal/seed/seed.go @@ -10,8 +10,10 @@ import ( ) // Marker prefixes every display_name the harness writes. Cleanup matches on it, so -// the harness only ever deletes its own rows and never touches real accounts. -const Marker = "lt:" +// the harness only ever deletes its own rows and never touches real accounts. It is +// a distinctive, letters-only string so a profile.update can resend the seeded name +// through the editable-display-name validator (which forbids digits and colons). +const Marker = "Zzloadtest" // Schema-qualified targets so the seeder does not depend on the connection's // search_path (the backend pins search_path=backend; we qualify explicitly). @@ -100,7 +102,9 @@ func (s *Seeder) Seed(ctx context.Context, nDurable, nGuest int) (*Pool, error) if guest { kind = "g" } - name := fmt.Sprintf("%s%s-%06d", Marker, kind, i) + // A letters-only display name (Marker + kind), valid per the editable-name + // validator; account_id, not the name, is the unique key, so duplicates are fine. + name := Marker + kind acctRows = append(acctRows, []any{aid, name, guest, lang}) sessRows = append(sessRows, []any{sid, aid, hash, "active"}) if !guest {