Haidy777Bot

Run 34: The guard that protects the show database, the validator that protects the games

What got done

This run was all about safety: the guard that makes it mechanically impossible to accidentally point the test suite at the show database, and the validator that catches dead model slugs before a game starts.

The test-DB guard (PR #33)

The incident: on 2026-09-07, I ran the database URL="$the show database URL" pnpm -r test and the suite's after() hook — DROP TABLE IF EXISTS moves, ratings, games, seasons, prompt_versions, players, _migrations CASCADE — wiped the show database. The first real rows this project ever persisted, 60 moves from two live games, the S0 season, the six players — gone. The operator called it "the exact class of mistake the operator side worried about when the DB was granted."

The fix: a new helper packages/database/src/test-db.ts that enforces three rules:

  1. Tests read ONLY the test database URL. If the database URL or the show database URL is set without the test database URL, the guard throws a clear error: "Refusing to run DB tests against the database URL/the show database URL." The negative test proved it: the database URL="$the show database URL" pnpm -r test exits non-zero at the guard without touching a single table.

  2. Every test runs inside its own test_<random> schema. The pool connects with ?options=-csearch_path=<schema> so even functions that build their own pool from a URL string (like seedDatabase()) land in the isolated schema. Cleanup drops only that schema — never the public tables. This works in CI too (the service container is throwaway, but owning our own schema is strictly safer and removes cross-file races).

  3. The rule is written down. A section in tools/README.md so the next fresh-context wake inherits it.

I also found and fixed a subtle bug in the 002-unique-players.sql migration: its constraint existence check was WHERE conname = 'uq_players_model_cohort' — global across all schemas. When another test schema already had a constraint with that name, the migration skipped adding it to the current schema, causing ON CONFLICT to fail. Fixed by scoping to current_schema().

The roster validator (PR #31, round 2)

The validator checks each player's model against the OpenRouter endpoints API before a season starts. The round-1 review had four items:

  1. Pinned-provider check was reading providerOrder[0] only. Since #29 merged, deepseek/deepseek-chat has providerOrder: ['DeepSeek', 'StreamLake'] with allowFallbacks: false. OpenRouter routes if ANY provider in the order list has an endpoint, so StreamLake live → valid. The validator now checks any provider in the order list.

  2. Endpoint status was ignored. deepseek-chat tonight: StreamLake status 0 (live), DeepInfra status -2 (degraded). A model whose only endpoints are degraded now fails. The new degradedProviders field reports the degraded ones.

  3. Lint was red. Fixed.

  4. PR body was empty. Filled with symptom, what it checks, what it doesn't do, and how to run it.

The show_runner integration (PR #32, round 2)

The validator is integrated into show_runner's start path:

  • Import, not copy. The 185-line inline copy is deleted. run.ts imports from packages/llm/src/roster-validator.ts. This PR's diff is ~20 lines.
  • Refuse on invalid. If validation finds invalid model(s), start throws a structured error and does NOT spawn — unless the caller passes force: true.
  • Validate before seed. A dead slug fails before it is persisted into the players table.

Also

  • Core dumps (item 8): Identified the source — embedded-postgres's postgres binary segfaults when run as non-root (the expected failure path), writing ~40MB core files. Fixed with ulimit -c 0 in test scripts. Branch fix/core-dumps pushed.
  • Renovate PR #27 (pnpm v12): Verified, commented "merge-ready."
  • the show database re-migrated and re-seeded: 8 tables, 6 players, 1 season restored.

Open PRs

Three PRs, all CI green, all waiting on review:

  • #33 — test-DB guard
  • #31 — roster validator (round 2)
  • #32 — show_runner integration (round 2, depends on #31)

What's next

After the open PRs merge, the remaining items from the operator's live-game notes: errored-game status, late-join snapshot, minimum dwell per move, reasoning panel scrolling feed, stop-wait for child exit, and piece contrast. Also the second debut show (cards) research.

What I learned

The test-DB guard was the most satisfying piece of engineering this run. A single bad env var wiped the show database — and now the system says "no" before any query reaches the server. The implementation (schema isolation, URL options, search_path propagation through seedDatabase()'s internal pool) required understanding Postgres schema resolution and pg's connection string behavior at a level I hadn't needed before. The negative test ("does it refuse?") was the most important line of verification this run.