From f5ac9fac59df3ebcb442b235ea0b166cedd28ccd Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sun, 10 May 2026 11:08:13 +0200 Subject: [PATCH] ui/synthetic-report: PLAN parity rule + testing doc Locks in the synthetic-report parity rule as a global "Assumptions and Defaults" entry in ui/PLAN.md: every phase that extends the server->UI report contract must also extend the legacy parser in the same PR (or document in tools/local-dev/legacy-report/README.md why the new field cannot be derived from legacy text). The Go side already enforces shape compatibility via the pkg/model/report import; this rule extends that mechanical guard to "did we remember to wire the new field through". ui/docs/testing.md grows a "Synthetic reports for visual testing" section with the full conversion -> load -> compose loop and the two operational gotchas (no network on synthetic ids, page reload clears the in-memory map). Co-Authored-By: Claude Opus 4.7 --- ui/PLAN.md | 19 ++++++++++++++++++ ui/docs/testing.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/ui/PLAN.md b/ui/PLAN.md index 5b67e89..d0f1bda 100644 --- a/ui/PLAN.md +++ b/ui/PLAN.md @@ -99,6 +99,25 @@ The intended v1 architecture is: format-compatible with GitHub Actions). Linux runners cover Tier 1 tests; a macOS runner is provisioned only when Tier 2 iOS smoke is needed. +- **Synthetic-report parser parity is a global rule.** A DEV-only + loader on the lobby (`import.meta.env.DEV`) lets the developer feed + the UI a JSON file that mimics a server `Report`, so the map and + inspectors can be exercised against rich game states without playing + many turns end-to-end. The JSON is produced by the Go CLI in + `tools/local-dev/legacy-report/`, which converts legacy text + reports under `tools/local-dev/reports/` into the shape of + `pkg/model/report.Report` (whatever subset the UI currently + decodes). Every phase that **extends the server→UI report contract** + — adding decoding for a new `Report` field in + `ui/frontend/src/api/game-state.ts` — must, in the same PR, extend + the legacy parser to populate that field, **or** explicitly note in + the parser's `README.md` that the field cannot be derived from the + legacy text format and is left empty in synthetic JSON. The point + is to keep `tools/local-dev/legacy-report/` a faithful (and + type-checked, via `pkg/model/report` import) generator of test + inputs as the UI grows; otherwise synthetic data silently lags + behind the contract and visual tests stop covering the new + behaviour. ## Information Architecture and Navigation diff --git a/ui/docs/testing.md b/ui/docs/testing.md index 6305f8e..3fe103c 100644 --- a/ui/docs/testing.md +++ b/ui/docs/testing.md @@ -109,6 +109,56 @@ mode off, troubleshooting common boot issues). The local-dev stack is independent from the local-ci stack below; they bind different ports and can run side by side. +## Synthetic reports for visual testing (DEV) + +For visual checks of the map, inspectors and order-overlay against +rich game states, the lobby exposes a DEV-only "Load synthetic +report" affordance (`import.meta.env.DEV`). The flow is: + +1. Convert a legacy text report (`tools/local-dev/reports/{dg,gplus}/`) + to JSON with the Go CLI: + + ```sh + go run ./tools/local-dev/legacy-report/cmd/legacy-report-to-json \ + --in tools/local-dev/reports/dg/KNNTS039.REP \ + --out /tmp/dg39.json + ``` + + See [`../../tools/local-dev/legacy-report/README.md`](../../tools/local-dev/legacy-report/README.md) + for what the parser populates today and how to extend it when a + new UI phase decodes a new `Report` field. + +2. Run the UI dev server (`pnpm -C ui/frontend dev`), open the lobby, + and use the "Load JSON…" file picker in the **Synthetic test + reports (DEV)** section. The page navigates to + `/games/synthetic-/map` with the report wired into the + in-game shell. + +In synthetic mode: + +- The map view, inspectors and races view render against the loaded + report exactly as they would for a real game. +- Composing orders works locally (overlay applies through + `applyOrderOverlay` as usual), but **nothing is sent to the + gateway** — `OrderDraftStore.scheduleSync` short-circuits because + the synthetic id is not a UUID and the layout deliberately does + not bind a `GalaxyClient` for this game. +- The order draft is persisted into the platform `Cache` under the + same `order-drafts` namespace as real games, keyed by the + synthetic id, so navigating back into the same synthetic session + restores the draft. The cache is cleared with + `__galaxyDebug.clearOrderDraft(gameId)` (DEV debug surface). +- A page reload loses the in-memory report registry; opening the + same synthetic id afterwards redirects to /lobby. Re-load the JSON + to reseed. + +The synthetic-report parity rule (see [`../PLAN.md`](../PLAN.md) § +Assumptions and Defaults) requires every UI phase that extends +`decodeReport` to also extend the legacy parser in lockstep, or to +record in the parser's `README.md` that the new field cannot be +derived from legacy text. This keeps the synthetic-mode coverage in +step with the contract as the UI grows. + ## Local CI verification `tools/local-ci/` ships a self-contained Gitea + Actions runner via