Skip to content

feat(seeder): expand db-seed with demo files + multi-type comments - #1484

Merged
rumblefrog merged 1 commit into
mainfrom
feat/db-seed-demos-and-comments
May 28, 2026
Merged

rumblefrog merged 1 commit into
mainfrom
feat/db-seed-demos-and-comments

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Summary

./sbpp.sh db-seed previously stopped at empty :prefix_demos rows
and ban-only comments — the dev panel rendered with two visible-but-
empty surfaces on every fresh seed:

  1. "Review Demo" link on the banlist rendered as a dead anchor.
    :prefix_demos was untouched, so the page handler never emitted
    the affordance and the surface was unreachable for manual
    exploration / acceptance audits / screenshot work without
    hand-uploading a demo first.
  2. Moderation queues (admin submissions + protests) and the
    public banlist's per-row comment disclosure rendered empty
    threads — :prefix_comments only got type 'B' (ban-attached),
    so the C / S / P columns were structurally untestable.

This PR closes both gaps with one expansion of
Sbpp\Tests\Synthesizer:

  • Demo seeding — insertDemos() writes ~80 rows at medium scale
    (64 ban-attached, 16 submission-attached — 80/20 split mirroring
    the panel's own bias toward bans-with-evidence) and lands a
    paired ~1 KiB opaque text payload on disk under SB_DEMOS
    (web/demos/ on the dev bind-mount). Filenames are
    md5(seed|demtype|demid) so re-seeds round-trip identically and
    the truncate-time wipe can find them by re-reading the table.
  • Multi-type comments — insertComments() fans the comment
    budget across all four :prefix_comments.type values
    (B=60% / C=20% / S=10% / P=10%, medium → 120/40/20/20). Matches
    the overload contract in api_bans_add_comment's match($ctype)
    arms documented in AGENTS.md → "Add a comment-delete trash icon".
  • Idempotency hardening — snapshot-then-truncate-then-unlink
    ordering (closes a transient "rows point at deleted files"
    window); scope is the snapshotted set so manual panel uploads +
    .gitkeep survive every seed.
  • Paired host-side changes — .gitignore for web/demos/*
    (with the .gitkeep exception); sbpp.sh db-reset strips
    MD5-basename files from web/demos/ after the DB drop so
    post-volume orphans don't accumulate.

Issue #1483 was filed for the pre-existing tech-debt finding from
the adversarial review (bare letter codes for BanRemoval / LogType
column-typed fields in the seeder) — out of scope for this PR.

Test plan

Local default-seed run:

$ ./sbpp.sh db-seed
  demos                      80
  comments                  200
  ...
  • PHPStan clean (./sbpp.sh phpstan — 253/253, no errors)
  • Targeted PHPUnit (./sbpp.sh test tests/integration/ExportBundleWriterTest.php
    — the one test that touches :prefix_demos / :prefix_comments:
    3 tests / 426 assertions all green)
  • Exact counts — demos=80 (B=64, S=16); comments=200 (B=120,
    C=40, S=20, P=20). Confirmed via SELECT type, COUNT(*) FROM sb_comments GROUP BY type and SELECT demtype, COUNT(*) FROM sb_demos GROUP BY demtype.
  • FK orphan check — six LEFT-JOIN-NULL queries across
    demos.B/bans, demos.S/submissions, comments.B/C/S/P
    against bans/comms/submissions/protests: all return
    zero orphans.
  • On-disk files — exactly 80 MD5-named files under
    web/demos/ after a fresh seed; basenames match the
    :prefix_demos.filename column byte-for-byte.
  • Idempotency: filename stability — two consecutive seeds
    with the default seed produce identical filename sets
    (md5sum of sorted ls output: ce2189fe… both runs;
    diff returns empty).
  • Idempotency: manual file preservation — touch web/demos/manual-decoy.dem; re-seed; the decoy survives
    (not referenced by :prefix_demos, not in the snapshot).
  • .gitkeep preserved — .gitkeep is also not in
    :prefix_demos, so it survives every seed unchanged.
  • db-reset cleanup — confirmed find … -regex '.*/[0-9a-f]{32}$' -delete matches both shapes the write
    path produces (synth + panel UploadHandler with
    renameToHash: true) while leaving .gitkeep and any
    non-MD5 manual uploads (icon / mapimg flows preserve
    original filenames) in place.
  • AGENTS.md + docker/README.md updated with exact per-scale
    counts, demo payload caveat (not playable in the demoviewer),
    new wipe ordering contract, and db-reset cleanup behavior.
  • Refusal guard intact — Synthesizer still refuses any
    DB_NAME other than sourcebans per the file's docblock, so
    sourcebans_test / sourcebans_e2e are unaffected.

`./sbpp.sh db-seed` previously stopped at empty `:prefix_demos` rows and
ban-only comments. Two visible-but-empty surfaces on a freshly-seeded
dev DB:

1. **"Review Demo" link on the banlist** rendered as a dead anchor —
   `:prefix_demos` was untouched by the seeder, so the page handler
   never emitted the affordance and the surface was unreachable for
   manual exploration / acceptance audits / screenshot work without
   hand-uploading a demo first.
2. **Moderation queues** (admin submissions + protests) and the
   public banlist's per-row comment disclosure rendered with empty
   comment threads — `:prefix_comments` only ever got type `'B'`
   (ban-attached) rows, so the C / S / P columns of the table were
   structurally untestable.

This PR closes both gaps with one expansion of `Sbpp\Tests\Synthesizer`:

**Demo seeding.** `insertDemos()` writes ~80 rows at medium scale
(64 ban-attached, 16 submission-attached — 80/20 split that mirrors
the panel's own bias toward bans-with-evidence) and lands a paired
~1 KiB opaque text payload on disk under `SB_DEMOS` (`web/demos/` on
the dev bind-mount). Filenames are `md5(seed|demtype|demid)` so
re-seeds round-trip identically and the truncate-time wipe can find
them by re-reading the table. Picks use `array_rand` for exact
unique counts — rejection-sampling-with-collisions undershoots the
budget by ~6% which made the screenshot-flow rendering jittery
across runs. The synth payload is text (not a Source-engine binary),
so the "Review Demo" link round-trips end-to-end through
`getdemo.php` with `Content-Type: application/octet-stream` and a
download lands in the browser, but the SDK demoviewer won't replay
them — that would need a real demo binary and is out of scope for a
panel-chrome seeder.

**Multi-type comments.** `insertComments()` now fans the comment
budget across all four `:prefix_comments.type` values:

- `B` (ban) — 60% — `comments.bid` → `bans.bid`
- `C` (comm block) — 20% — `comments.bid` → `comms.bid`
- `S` (submission) — 10% — `comments.bid` → `submissions.subid`
- `P` (protest) — 10% — `comments.bid` → `protests.pid`

Medium scale → 200 comments → 120/40/20/20. The split matches the
overload contract in `api_bans_add_comment`'s `match($ctype)` arms
documented in AGENTS.md → "Add a comment-delete trash icon". The
parent-ID tracking arrays (`commsCids`, `submissionSubids`,
`protestPids`) populate during their respective `insert*()` passes
via `lastInsertId()` so the comment-insert pass has the FK pool ready.

**Idempotency contract.** The pre-existing truncate-then-reseed flow
got two refinements:

- **Snapshot filenames BEFORE truncate, unlink AFTER.** Previous
  pre-fix shape was wipe-files-then-truncate, which left a transient
  window where files were gone but DB rows still pointed at them
  (any panel page query during that window 404'd the "Review Demo"
  link). New order: snapshot → truncate → unlink, so rows + files
  drop visibly together from the panel's POV.
- **Wipe scope is the snapshotted set, not the directory.** Manual
  panel uploads (anyone who uploaded a demo against the dev stack)
  and `.gitkeep` survive untouched. The `db-reset` companion (see
  below) is the only thing that strips by-name across the whole
  directory, scoped to the MD5-basename shape.

**Paired surface changes:**

- `.gitignore` — `web/demos/*` + `!.gitkeep` exception. Pre-fix a
  fresh `db-seed` polluted `git status` with 80 untracked MD5-named
  files; the demo store is also where the panel's `UploadHandler`
  writes real ban evidence on production installs, so checking those
  in would leak operator evidence into the source tree.
- `sbpp.sh db-reset` — strips MD5-basename files from `web/demos/`
  after the DB volume drop. The `_demos` rows that pointed at them
  vanish with the volume, so the next `db-seed`'s referenced-only
  wipe reads zero rows from the fresh DB and skips them; without the
  reset-time strip those files would accumulate as permanent orphans
  across worktree lifetimes. `find … -regex '.*/[0-9a-f]{32}$'`
  matches both shapes the write path produces (synth + the panel's
  own `UploadHandler::handle()` with `renameToHash: true`).
- `AGENTS.md` + `docker/README.md` — exact per-scale counts, demo
  payload caveat (not playable in the demoviewer), the new wipe
  ordering contract, gitignore + `db-reset` cleanup behavior.

**Verification.** Default-seed run produces exactly 80 demos +
exactly 200 comments split B=120/C=40/S=20/P=20; FK joins return
zero orphans across all six pairs (`demos.B` → bans, `demos.S` →
submissions, `comments.B/C/S/P` → bans/comms/submissions/protests);
two consecutive seeds with the default seed produce identical
filename sets (`md5sum` of `ls` output matches byte-for-byte); a
manually-touched decoy file in `web/demos/` survives every seed.
PHPStan clean; the one PHPUnit test that touches `:prefix_demos` /
`:prefix_comments` (`ExportBundleWriterTest`, 426 assertions) passes
unchanged.

Follow-up: #1483 tracks the pre-existing tech debt where the seeder
writes bare letter codes (`'U'` / `'D'` / `'m'` / `'w'` / `'e'`) into
the column-typed string fields covered by `BanRemoval` / `LogType`
instead of routing through the enum's `->value` per AGENTS.md
"Backed enums for column-typed fields". Out of scope for this PR.
@rumblefrog
rumblefrog added this pull request to the merge queue May 28, 2026
Merged via the queue into main with commit 424d6d4 May 28, 2026
5 checks passed
@rumblefrog
rumblefrog deleted the feat/db-seed-demos-and-comments branch May 28, 2026 00:03
@github-actions github-actions Bot locked and limited conversation to collaborators May 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant