This brief is internal. Enter the password to continue.
An audit of what testing exists across the eleven code repos today, a recommended toolchain with the alternatives that lost and why, and a phased plan to put it in place without stopping to retrofit 1,800 files.
Phase 0 shipped on August 7, 2026, the same day this was written. All 10 live repos now require a passing CI check before anything merges to main. Sections 01 and 07 below describe the state before that work and are kept as the original assessment.
For current status - which phases are done, what is next, what is waiting on you - see the Testing strategy rollout project in Linear. One scope correction since writing: tblmaster-player was a retired iteration of the player app and has been archived, so this is a 10-repo plan, not 11.
I read every repo rather than assuming. The headline is that one repo is done properly and the rest have effectively nothing, including the shared packages that every app depends on.
| Repo | Src files | Tests | Lint | Typecheck | CI gate |
|---|---|---|---|---|---|
| tblmaster-roomOperator tools | 448 | 15 | Yes | Yes | Full |
| tblmaster-platform11 shared packages + player app | 706 | 1 | App only | None | Migrations only |
| tblmaster-dirtystackpokerPublic site | 233 | 0 | None | None | None |
| tblmaster-admin | 194 | 0 | Yes | Yes | None |
| tblmaster-player | 73 | 0 | Yes | None | None |
| tournament-intelScrapers, runs nightly | 49 | 0 | None | None | None |
| tblmaster-dealer | 32 | 0 | Yes | Yes | None |
| mangrovepoker | 27 | 0 | Yes | None | None |
| tblmaster-displayIn-room TVs | 25 | 0 | Yes | Yes | None |
| tblmaster-auth | 18 | 2 | Yes | None | None |
| tblmaster-cards | 16 | 0 | None | None | None |
tblmaster-room is already correct. Vitest, fifteen tests on the genuinely tricky logic (seating, table breaks, clock computation, bust eligibility), plus a CI workflow that runs lint, typecheck, tests and a build on every pull request. It is the template for everything else, not a repo that needs work.
You do not need to choose a strategy from scratch. You need to propagate the one you already have.@tblmaster/entitlements, @tblmaster/tournament-clock, @tblmaster/validation, @tblmaster/supabase and the rest are pure TypeScript consumed by every app you ship.
This is the single highest-leverage gap. A bug here is a bug in the operator tools, the TVs, the dealer tablet, the public site and the phone app simultaneously.tblmaster-dealer and tblmaster-display are primed but empty. Both have Vitest installed and a test script wired up. Neither has a single test file, so npm test passes trivially.
Worth knowing: a green "tests passed" today means nothing in those two repos.tblmaster-cards and tblmaster-dirtystackpoker have no lint configuration at all, and dirtystackpoker is 233 files. Cards is already filed as TM-364.Your safety net today is TypeScript, ESLint in six of eleven repos, and the fact that a broken Next.js build fails the Vercel deploy. That catches type errors and syntax mistakes. It catches nothing about behaviour: a payout that splits wrong, a policy that leaks a row, a clock that drifts on a level change, a scraper that quietly returns zero.
Five layers. Each is cheap where it runs often and expensive only where it runs rarely. The reasoning for each choice is in Section 3.
| Layer | Tool | What it proves | Where it runs | Speed |
|---|---|---|---|---|
| Static | TypeScript + ESLint | The code is internally consistent and free of known bad patterns | Editor, commit, CI | Instant |
| Unit | Vitest + fast-check | Individual functions compute the right answer, including on inputs you did not think of | Push, CI | Seconds |
| Database | pgTAP | Policies deny what they should deny, functions return what they should return | CI | ~1 min |
| End to end | Playwright | A real browser can complete a real journey against a real deploy | CI on pull request | 2 to 5 min |
| Mobile | Maestro | The built app launches, signs in and reaches the main screens | Before a store submit | ~10 min |
Three of these you already own in some form. Vitest is installed in three repos, TypeScript and ESLint are configured in most, and GitHub Actions is running in two. The new tools are Playwright, pgTAP, Maestro and fast-check.
Four separate decisions. They are independent of each other, so you can accept one and reject another without the plan falling apart.
| Option | Case for | Case against | |
|---|---|---|---|
| Vitest | Runs TypeScript with no transform configuration. Very fast. Jest-compatible API, so every answer written for Jest in the last decade still applies. Built-in coverage and a --changed mode that only runs tests affected by your diff. |
Smaller ecosystem than Jest. React Native support needs care, which is why the mobile app is handled separately below. | Chosen |
| Jest | The industry default. Largest ecosystem. jest-expo is the officially supported preset for the React Native app. |
Slower. Needs Babel or SWC transform configuration to read your TypeScript. Its ESM support is still awkward in 2026. Adopting it means throwing away the 17 Vitest tests and two configs you already have. | No |
| node:test | Built into Node, zero dependencies. You already use it for the migration drift test. | No DOM environment, so it cannot test React components. Weak watch mode and no coverage UI. You would end up running two runners anyway. | Keep for scripts |
| Bun test | Extremely fast. | Bun is not the runtime for your Next or Expo apps, so it adds a second toolchain to maintain for a speed win you will not notice at this scale. | No |
Because it is already the de facto standard in this codebase and it is genuinely the better tool. The decision was effectively made when tblmaster-room was set up. Choosing anything else means a migration with no upside. Keep node:test for the standalone Node scripts in tblmaster-platform; there is no reason to convert a working test.
| Option | Case for | Case against | |
|---|---|---|---|
| Playwright | Auto-waiting, which removes most of the flakiness that makes people abandon end-to-end tests. A trace viewer that records the full timeline of a failed run so you can scrub through what happened without reproducing it. Codegen records your clicks into a test file. Handles multiple origins in one test, which matters because your auth is a separate app. | Tests are still slower and more brittle than unit tests. Needs a deployed or locally running app. | Chosen |
| Cypress | Excellent interactive runner. Very approachable. | Historically weak at cross-origin flows, which is exactly your auth handoff. Parallel runs in CI need their paid dashboard. Slower. | No |
| Selenium | Universal, ancient, works with anything. | No auto-waiting, so tests are flaky by default. Far more code for the same result. | No |
Two reasons specific to you. First, the trace viewer: as a solo operator you will almost never be watching when a test fails in CI, so the ability to replay the failure afterwards is worth more than a nice live runner. Second, cross-origin support: tblmaster-auth is a separate application, so any realistic sign-in journey crosses origins, and that is where Cypress fights you.
supabase test db| Option | Case for | Case against | |
|---|---|---|---|
| pgTAP | Runs inside Postgres, so it can assume a role and prove that RLS actually denies a row. Supported natively by the Supabase CLI. Tests functions, constraints and policies as behaviour rather than as SQL text. | You write assertions in SQL, which is a new muscle. Needs a database to run against, and there is a real obstacle to that today (Section 6). | Chosen |
| Client-side integration tests | Written in TypeScript with the Supabase JS client, so no new language. Tests the app and the database together. | Cannot cleanly impersonate roles, so it tests what your app happens to ask for rather than what the policy permits. It would miss exactly the class of bug that matters: a policy that is more permissive than intended. | Supplementary |
| The migration linter you have | Already built, already gating pull requests, catches the recurring SECURITY DEFINER regression. | It is static analysis on SQL text. It proves the shape is safe, never that the behaviour is correct. Keep it, but it is not a substitute. | Keep, not enough |
You named money and points correctness as a top risk, and that logic lives in the database, not the app. pgTAP is the only option on this list that can sit down as an authenticated user from another room and confirm it gets zero rows back. Nothing in the application layer can prove that.
| Option | Case for | Case against | |
|---|---|---|---|
| Maestro | Flows are short YAML files. Tolerant of timing by design, so it is much less flaky than the alternative. Works with Expo builds. Low enough friction that a solo operator will actually keep it running. | Lower ceiling. Not suited to complex conditional logic. | Chosen |
| Detox | More powerful, synchronises with the app's internals, better for intricate scenarios. | Notoriously heavy to set up and keep working across React Native upgrades. For one person maintaining it alongside everything else, it is a liability. | No |
| React Native unit tests | Fast, no device needed. | Requires jest-expo, which means a second test runner in the monorepo purely for the app. Most of the value is avoidable: put the logic in the shared packages, where plain Vitest tests it. |
Avoid by design |
The architecture already helps you here. player-universal consumes eleven shared packages that are plain TypeScript. Every piece of logic you move out of a screen and into a package becomes testable with the runner you already have, with no React Native machinery at all. Maestro then only has to answer a much simpler question before a store submit: does the built app launch, sign in and reach its main screens without crashing. That is the check that actually protects you from the bad-build-in-review scenario.
fast-check is property-based testing, and it is the right answer for the payout and points math specifically. Instead of writing "a $100 buy-in with 30 players and a 20 percent rake pays out X", you state an invariant that must always hold, such as "the payouts always sum to exactly the prize pool" or "no player ranked lower ever receives more than a player ranked higher". fast-check then generates thousands of random tournaments trying to break it, and when it finds a failure it automatically shrinks it to the smallest example that still fails.
It plugs directly into Vitest. It is the closest thing to an automated adversary you can get, and it is far more reliable at finding edge cases in numeric logic than either you or an AI reviewer guessing at them.
This is the part you said you were unclear on, so here it is concretely. There are four separate gates. They fire at different moments, they check different things, and critically, only one of them cannot be skipped.
Git hooks are a convenience, not a control. They live in your local checkout, they only exist on machines where you ran an install, and any of them can be skipped with git push --no-verify at two in the morning when you are certain it is fine.
Branch protection on GitHub is the actual enforcement. It is a setting on the repository that says "this pull request cannot be merged until the CI check reports success". It runs on GitHub's machines, not yours, and there is no flag that skips it. Since Vercel deploys from main, making the merge impossible is what makes the bad deploy impossible.
You asked whether this is something you call, something I do automatically, or something that runs before a push. All three, and Gate 4 is the one you probably have not seen before.
Claude Code supports a Stop hook: a command configured in .claude/settings.json that the harness runs automatically when I finish a task. Point it at the test command and I cannot hand work back to you claiming it is done without the tests having actually run. This is worth more in your setup than in a normal team's, because I am writing most of the code, and a rule written in CLAUDE.md is advice I might weigh against other advice, whereas a hook is machinery that simply executes.
You already have the soft version of this installed as the verification-before-completion skill. The hook is the version that does not depend on my judgement.
You asked whether this is one big run or something broken into chunks. It is neither, and the distinction matters more than the answer.
There are roughly 1,800 source files with 18 tests between them. Any plan that begins with "write tests for the existing code" ends with the plan being abandoned in week three. That is the single most common way this initiative fails.
The approach that works is a ratchet: turn the gates on immediately for new and changed code, and leave existing untested code alone until you happen to touch it. Concretely, you set coverage thresholds to whatever the numbers are today, and configure them so they can go up but never down. A pull request that lowers coverage fails. A pull request that leaves it alone passes. Over a few months the codebase becomes tested as a side effect of normal work, and you never spend a week doing nothing but writing tests for code that is already working fine.
Untested code that has been running in production for months is not the emergency. Untested code you changed this morning is. The ratchet puts the effort exactly where the risk is.
There is a single place worth a deliberate up-front pass, and it is not a repo. It is the eleven shared packages in tblmaster-platform.
They are the right chunk for four reasons that do not apply anywhere else:
Everything else follows the ratchet. Do not chunk by repo size, and specifically do not start with tblmaster-dirtystackpoker because it is the biggest untested thing. It is a marketing site. A bug there costs you an afternoon, not a tournament.
This is a real blocker I found rather than a theoretical concern, and it is worth surfacing now because it changes the order of work.
Your own supabase-advisor-gate workflow documents this: the migration history is applied incrementally against the live database and is not replayable from scratch, because early pre-baseline migrations assume the 20260323222726_remote_schema.sql baseline already exists.
That is exactly what the standard pgTAP-in-CI setup needs. It creates an empty Postgres, replays your migrations into it, and runs the tests. Today that would fail before reaching a single test.
The fix is to squash the migration history into a fresh baseline. You dump the current production schema, make that the new first migration, and archive everything before it. It is a contained, well-understood operation, and it is worth doing on its own merits: it also makes future preview branches work and removes a permanent piece of tribal knowledge from your setup.
Until that lands, database testing has one workable interim form: run pgTAP by hand against a scratch Supabase branch before a release rather than automatically on every pull request. Useful, but not a gate. I would treat the squash as a prerequisite for Phase 3 rather than trying to route around it.
Six phases, ordered by risk reduced per hour spent. Phase 0 delivers a genuine step change on its own and involves writing no tests at all, so it is worth doing even if you stop there.
Pure configuration. This is the biggest single improvement available and it requires no new tests, because it makes the checks you already have unskippable in the nine repos where they currently do not run at all.
tblmaster-room CI workflow into the other ten repos, adjusted per repo.tblmaster-cards and tblmaster-dirtystackpoker, which have none. Closes TM-364.typecheck script to the six repos missing one.main in all eleven repos with the CI check required.simple-git-hooks for pre-commit and pre-push, matching the pattern already in tblmaster-room.Stop hook so I run tests before handing work back.Expect this to surface existing lint and type errors in the repos that have never been checked, particularly dirtystackpoker at 233 files. Budget time for that cleanup, or start those two repos with the rules set to warn and tighten them in Phase 5.
Characterization tests that lock in today's behaviour, so future changes cannot silently alter it. This is the deliberate up-front chunk from Section 5.
entitlements, tournament-clock, validation, utils, then the rest.tblmaster-dealer and tblmaster-display already have Vitest wired and zero tests, so npm test currently passes on nothing. Both are live-tournament surfaces, which you named as a top risk.
tblmaster-room's existing suite into the settlement and payout paths.Blocked on the prerequisite in Section 6. This is the phase that addresses money and points correctness directly.
supabase test db into the platform CI workflow as a required check.Six or seven Playwright journeys, no more. Every end-to-end test you add is a permanent maintenance cost, so they have to earn their place.
tblmaster-auth and land in the room. Crosses origins.You asked for baseline first and bug hunting second. This is the second part, now that a baseline exists to catch what the hunt disturbs.
eas submit: launch, sign in, reach each main screen.Your instinct is right, but the reason it works is not quite the reason usually given, and that changes how you should set it up.
The value is mostly not that a second model has different weights. It is that a second agent has an independent context and an adversarial objective. An agent that just wrote a function is anchored on the mental model it used to write it, and will review the code against that same model. A fresh agent told to break the code has neither handicap.
Practical consequence: running a second Claude with a refute-first prompt captures most of the benefit. Using a different model on top of that adds a little more. Do not treat "must be a different vendor" as a requirement, because it is the smaller half of the effect.
.claude/agents/. Its definition can pin its own model, so a different model here is easy if you want it./code-review on the working diff before every pull request. Built in, free, on demand./code-review ultra runs a multi-agent cloud review of the branch. It is triggered by you and billed, and I cannot launch it on your behalf, so it is a deliberate step you take on the changes that warrant it rather than something that happens automatically.
The failure mode of adversarial review is volume. An agent told to find problems will always find problems, and if it returns twenty findings of which two are real, you will start skimming and then ignoring it. Prompt the adversary to verify each finding before reporting, and to return nothing when it has nothing. A reviewer that is usually silent is one you will still be reading in six months.
--no-verify, and once that becomes habit the local gates are gone. If the suite outgrows the budget, move work to CI rather than tolerating a slow push.tblmaster org is on the Team plan, which includes 3,000 minutes a month for private repositories. Eleven repos at roughly three minutes a run leaves substantial headroom.Do Phase 0. It is roughly a day, it involves writing no tests, and it takes you from nine repos where nothing is checked to eleven repos where nothing broken can reach main. Every later phase is an improvement on that foundation. Phase 0 is the foundation.