Repository navigation
feat(uitest): add DEBUG-only board:showcase route for store captures - #1107
Conversation
Adds a shared `-uitest-route board:showcase` key (GameAppKit.UITestLaunchArg) both apps resolve to ONE fixed, pre-built mid-game board, for zero-click App Store 03-board marketing captures (fork A of the owner ruling). Minesweeper: MinesweeperShowcaseSession builds a deterministic snapshot via real MinesweeperEngine reveal/flag calls, mounted through MinesweeperGameViewModel(seeded:) instead of a live session restore — that seam already exists for snapshot/preview fixtures and is what keeps `elapsedSeconds` from ticking, which a live actor session cannot guarantee (`.restore` normalizes to `.paused`, which mounts a blocking overlay, and reaching `.playing` again needs an async `resume()` the loader's synchronous seam can't cheaply offer). MinesweeperGameViewModel.reveal(row:col:) / toggleFlag(row:col:) now also guard on `isSeeded` (only `refresh()` did before, so a seeded showcase VM was silently mutable by any tap). The 3 flagged mines are chosen only among cells FORCED by the basic single-constraint count rule: a revealed neighbor whose displayed number equals its own count of non-revealed neighbors, which pins every one of those neighbors (the flag included) as a certain mine — never a guess, and stronger than mere 8-adjacency to a revealed cell (a "2" with 3 non-revealed neighbors doesn't pin any single one of them). Seed and first-click cell were found by a brute-force search for a combination with >=3 such forced mines; `MinesweeperShowcaseSessionTests` computes each flag's witness fresh from the built board (no hard-coded coordinates) and fails if any flag lacks one. Sudoku: SudokuShowcaseBoard builds a fixed GameSessionSnapshot (givens + correct fills + exactly one conflicting entry + legal pencil notes) served through UITestShowcasePersistence, a DEBUG-only PersistenceProtocol wrapper that intercepts the fixed puzzleId and no-ops every write for it — mirrors UITestSeededCompletedDailyPersistence's shape and injection point. BoardLoaderView.mountLoaded gets a small DEBUG-only frozen-clock branch (UITestFrozenMonotonicClock) so the board's elapsed timer never advances after mount; every other puzzleId is byte-for-byte unchanged. Sudoku goes through the live GameSession actor (unlike Minesweeper's seeded snapshot) because it rides the existing PersistenceProtocol interception point BoardLoaderView already has — Minesweeper's fresh-board loader has no such seam upstream of it; both builders' headers cross-reference this asymmetry. All new/changed production code is `#if DEBUG` gated. Verified with `strings` against both apps' Debug and Release iphonesimulator builds: Debug shows the new symbols, Release shows zero. Two-capture hash comparisons on a throwaway simulator (status-bar override, warmed launches) confirm byte-identical repeat captures for Minesweeper's app-state content; Sudoku's board/notes/ error-highlight/timer content is always pixel-identical, but its glass digit-pad region shows a recurring sub-pixel compositor jitter (max channel delta <=2/255, same bbox every time) regardless of warm-up depth — a known rendering-layer concern for the PR-2 capture pipeline, not an app-state determinism defect in this PR. part of #1054 part of #1014 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fresh-context acceptance (T6) — commit
|
|
PM merge record, head Review: one substantive code review plus a fresh-context acceptance (ACCEPT). The review found two real defects, both fixed here:
PM ruling on that second one: adjacency to a revealed number is a proxy, not deducibility. Every flag must be forced by the basic count rule, and the test computes the witness per flag from the board rather than hard-coding it, so a later seed or builder edit cannot quietly reintroduce a guess. Final positions: flags (4,7) and (4,8) witnessed by (5,8)=2; flag (5,4) witnessed by (6,5)=2; seed Release exclusion, verified. Case-insensitive PM ref check: the only non-comment mention outside a Noted in the PR body: Sudoku reaches the showcase through a live session with a frozen clock while Minesweeper uses a seeded snapshot, because MS #1054 stays open; PR-2 carries the capture task, the fit-height compositor, the gate updates and the regenerated frames, and the Mac slot is still unresolved. |
What
PR-1 of the #1054 owner ruling (Fork A): a DEBUG-only
-uitest-route board:showcaselaunch key in both apps that pushes one fixed, pre-built, honest mid-game board with zero taps, so the App Store03-boardmarketing frame can be captured from a live simulator where Liquid Glass actually renders. PR-2 (capture task, fit-height compositor rule, gates, frames, docs) follows.part of #1054 · part of #1014
How
GameAppKit.UITestLaunchArg.showcaseBoardRouteKey(inside the existing#if DEBUGregion) — the single key both resolvers match.MinesweeperShowcaseSession.build()(whole file#if DEBUG) drives a realMinesweeperEngine— seed0xC0FF_EE15_5C0B_0008, first reveal at (8,4) — then flags 3 mines that are forced by the basic count rule: each flag has a revealed witness neighbour whose number equals its count of non-revealed neighbours. Mounted through the existingMinesweeperGameViewModel(seeded:)seam inMinesweeperFreshBoardLoaderViewwhen the route's sentinel seed arrives (#if DEBUGblock).reveal/toggleFlagnow honourisSeeded(they did not; the init's doc comment claimed they did), so the fixture cannot be mutated by a stray touch.SudokuShowcaseBoard(whole file#if DEBUG) builds a fixedGameSessionSnapshot— givens + correct entries + exactly one genuinely conflicting digit (sorecomputeErrors()flags it) + legal pencil candidates — served byUITestShowcasePersistence, aPersistenceProtocolwrapper mirroringUITestSeededCompletedDailyPersistencethat intercepts the fixed puzzleId and no-ops every write for it.BoardLoaderView.mountLoadedgets a#if DEBUGfrozen-clock branch for that id only; the#elsebranch is the original line.Accepted asymmetry (not a defect)
Sudoku uses a live
GameSession+ frozen clock; Minesweeper uses the seeded snapshot.MinesweeperSession.restorenormalises.playingto.paused(mounting the pause overlay) and returning to.playingneeds the actor's asyncresume(), which the synchronous fresh-board loader cannot host without a refactor; Sudoku's loader is already async, so a frozen clock is the minimal change there. Both file headers cross-reference this.Proof the route cannot ship
Claim, stated precisely: the
board:showcaseliteral, the showcase types and the-uitest-*keys are absent from the Release binaries, and so is theresolveShowcasePersistencesymbol — its Release body is the identity function (UITestShowcasePersistence.swift:135,#else return persistence) and the optimizer strips it. Scan is case-insensitive (rg -i) over every executable in each.app(main binary + GoogleMobileAds + UserMessagingPlatform), on Release builds made on the final commit (binary mtime 23:44/23:45 > commit 20:24:51):| app | Release
rg -i 'showcase|uitest'(all executables) | Releaserg -i resolveShowcasePersistence| Release positive controlrg 'Persistence'(same binary) | Debug<App>.debug.dylibrg -i 'showcase|uitest'|board:showcaseliteral in Debug ||---|---|---|---|---|---|
| Minesweeper | 0 | 0 | 55 | 80 | 2 |
| Sudoku | 0 | 0 | 57 | 96 | 2 |
The Debug column (positive control) shows the detector finds the symbols when they exist; the
Persistencecolumn shows the same detector on the same Release binary finds ordinary symbols. Every earlier run (developer, Leader, T6) also usedgrep -i; T6 additionally rebuilt Release on the final commit after finding the developer's first Release build predated the last edits.Honesty guards (tests)
reveal/toggleFlagare bit-identical no-ops.Verification
swift testMinesweeperKit (filterMinesweeperAppCompositionTests|MinesweeperShowcase): 20 tests green; SudokuKit (filterSudokuUITestRouteTests|SudokuShowcase): 9 green; GameAppKitUITestLaunchArgTests: 8 green.swiftlint --stricton every touched file: 0 violations.__Snapshots__change in the diff.simctl status_bar override→ identical sha256. Sudoku: board/notes/error/timer identical across captures; an intermittent ≤2/255 sub-pixel jitter confined to the glass number pad (86 px, bbox (431,2392)-(868,2746)) is a compositor effect, characterised and handled in PR-2's capture policy, not here.🤖 Generated with Claude Code