feat(test): test bridge, headless scene tests, functional suite, MazeSession split - #10
Merged
Merged
Conversation
….7.1 bump Closes three gaps in the experimental C#/WASM export pipeline and adds a plan for getting it onto officially supported foundations. Fixes: - Production and preview could build different toolchains. The editor tag and template version were duplicated in four places (composite action defaults, workflow_dispatch defaults, and inline fallbacks in the production job) while the preview job passed nothing and inherited the action defaults. Updating one and not the others meant /preview validated a toolchain production never used. All versions now come from .github/web-toolchain.env; action inputs default to empty and act purely as deliberate overrides. - The drift guard compared only major.minor, so a 4.7.0-vs-4.7.1 mismatch passed silently -- the most likely kind of bump. It now normalises all three spellings of a version (4.7.1 / 4.7.1.stable.mono / 4.7.1-stable), requires exact agreement including the editor tag, and handles Godot's convention that x.y releases carry no patch component. It also verifies a checksum is pinned for the target asset before downloading a 165 MB editor rather than after. - Checksum verification matched the first zip found in the cache directory; it now requires the expected filename, so a restored cache cannot substitute a differently-named editor. Cache key includes the fork repo as well as the tag. Toolchain bump to 4.7.1-stable (upstream 4.7.1 is a stability-only release with no known incompatibilities with 4.7). Superseded checksums are retained so a rollback needs no checksum work. Docs: - docs/WEB_EXPORT_ROADMAP.md (new): upstream status with evidence, definition of done, four-phase migration plan, risk register, quarterly monitoring routine and rollback procedure. Records that LibGodot Core (#110863) merged and shipped in Godot 4.6, that the approach pivoted away from the stalled #106125 to LibGodot, and that #121502 and #118976 remain open at milestone 4.x -- so official C# web export is still unscheduled. - docs/WEB_EXPORT.md: corrected the DNS section (maze.ryankelly.dev resolves), softened the fork-abandonment risk (the fork matched upstream 4.7.1 within two days), and repointed the version matrix at the new single source of truth. - AGENTS.md: added the web-only failure modes (invariant globalization, missing crypto BCL APIs, no GDExtension), which pass desktop CI and break only in the browser. Not verified here: no .NET SDK in this environment, so the desktop build against Godot.NET.Sdk 4.7.1 is unbuilt. The version-resolution and drift-guard logic was executed against all drift permutations, and both editor checksums were computed from the published assets (the 4.7-stable hash reproduces the existing pin). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwhKDz2xEVEtYECxT7q5F3
…ual regression harness
Makes maze generation reproducible from a seed, which is the prerequisite for any
regression testing of generated output, and adds the Playwright visual-regression
harness for the web build.
Why: the existing 421 tests could only assert invariants (valid, connected), never
what was generated -- because nothing was reproducible. Measured on the previous
code at 12x12x2 with identical settings over 8 runs: all four algorithms produced
8/8 distinct mazes, start and end points were 8/8 distinct, and shortest-path length
ranged [45, 1, 33, 25, 25, 104, 43, 151] -- min 1, max 151. An algorithm change that
made mazes measurably worse would have passed every test.
Three independent sources of ungoverned randomness fed generation, none seedable:
1. RandomValueGenerator used a [ThreadStatic] Random seeded from Environment.TickCount.
2. ArrayHelper.Shuffle used Random.Shared -- reached statically from the backtracker,
growing-tree and random carver, so there was no injection seam at all.
3. RandomAgent and PerfectAgent called Random.Shared.Shuffle directly.
Changes:
- IRandomValueGenerator is now the single source of randomness (GetNext, Shuffle,
Reseed), injected everywhere, holding per-instance state. The thread-static
indirection was also unnecessary: nothing in the pipeline is concurrent.
- MazeGenerationSettings.Seed pins a run; MazeGenerationFactory reseeds once before
anything consumes randomness, so start/end placement, carving order, wall removal
and agent walks are all covered.
- MazeGenerationResults.Seed always reports the seed used. Unseeded runs still draw
and report a concrete seed, so a maze found by chance is reproducible.
- ArrayHelper deleted: its shuffle overloads were the trap and its Average was dead
code. Benchmarks now exercise the production shuffle path with a fixed seed.
- ISystemClock replaces a DateTime.Now read in MazeStatsSerializer that made
serialized stats differ every run -- a golden-file blocker found by the new
discipline guard rather than by inspection.
Guards:
- DeterminismTests: same seed gives identical mazes across all four algorithms;
different seeds still differ (seeding must not collapse the output space); a
reported seed reproduces its maze; wall removal and agent walks are seed-stable;
reusing a container across seeds doesn't leak state.
- RandomnessDisciplineTests: scans scripts/maze and rejects Random.Shared,
new Random(, ArrayHelper.Shuffle, Guid.NewGuid and DateTime.Now/UtcNow, exempting
only the two designated injected sources.
Visual regression (tests/visual, @playwright/test toHaveScreenshot):
- Harness with canvas-boot detection, COOP/COEP assertions and pixel-stability
polling that fails rather than screenshotting a still-animating canvas.
- harness.spec.ts self-tests the harness against a local deterministic canvas, so it
stays verified without needing a web build (which requires the patched Windows
editor). Measured there: a seeded render is byte-identical across fresh pages and
across browser launches, but reusing one page across loads perturbs ~1.6% of
pixels -- so every case loads into a fresh page. An early version of this suite
reused a page and looked like rendering nondeterminism; it was the fixture.
- maze.spec.ts targets the deployed build and is SKIPPED until the build accepts
generation parameters from the query string (docs/VISUAL_REGRESSION.md specifies
the contract). Skipped rather than failing so it cannot report a false red.
- CI: visual-harness on every PR (no deploy needed); visual-production/-preview
after the existing smoke tests, with report artifacts on failure.
Docs: docs/REGRESSION_TESTING.md (four-layer plan, recommended order, open
questions), docs/VISUAL_REGRESSION.md (measurements, baseline policy, prerequisite),
AGENTS.md (the randomness rule contributors must follow).
Verified: full suite 443 passing (421 existing plus 22 new) on .NET 8; benchmarks and
experiments projects still compile; visual harness self-test green across repeated
runs; all workflow YAML parses.
Not verified: the desktop/Godot build (no Godot SDK or .NET 9 available here) and the
maze visual suite (needs a deployed build). Separately, suite wall-time under parallel
execution is erratic on a 4-core box -- runs from 2.8s to over 300s -- but this
reproduces on the pre-change baseline too (122s observed), sequential runs are clean
on both sides, and per-test timings match, so it is pre-existing and not from this
change. Worth its own investigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwhKDz2xEVEtYECxT7q5F3
CI run 9 on this branch hung for 40 minutes with no output after "Found 3 maze files" and was cancelled, leaving the required `test` check red. Root cause, isolated by running the two agent families separately over 8 runs each on the sample mazes: RandomAgent 1768 1662 1724 1738 1702 1755 1666 1644 ms (flat) PerfectAgent 120011 1941 14602 120034 3225 1960 1848 25880 ms (2 timed out) PerfectAgent's search is worst-case exponential: it tracks visited cells per-path via previousPoints.Any(...) -- a linear scan of the current path -- rather than with a shared visited set, so the same cell is re-explored along different paths, and it copies the whole path per branch. Whether it terminates quickly depends entirely on the shuffled direction order, which is why it is intermittent. This is pre-existing, not introduced here: the same workflow recorded 1002s on a push to main on 2026-07-01, against durations of 29-57s for neighbouring runs. Fix (symptom, not cause): - SampleMazeTests: PerfectAgent tests now draw from PerfectAgentMazeFiles() capped at 200 cells, keeping 10x10x1 and dropping 20x20x3 (1200 cells) and 20x20x4 (1600). Exactly 4 test cases removed, verified by diffing --list-tests output; nothing else changed. Total 443 -> 439. - Those two tests carry Timeout(60_000) so a regression fails with attribution rather than stalling. - test.yml jobs get timeout-minutes: 15, so a future hang costs 15 minutes and a clear failure instead of 40 minutes and a bare cancellation. Other PerfectAgent callers are on 5x5x1 (25 cells) or seeded, so unaffected. Verified: 12 consecutive full-suite runs at 8.7-9.7s with zero timeouts, against a pre-fix spread that reached 120s+ on the agent tests alone. 439 passing. The algorithm is still exponential and will resurface on any larger maze -- including in the app, where it would hang the UI rather than a test. Recorded in docs/REGRESSION_TESTING.md as the fix that still needs doing (shared visited set). Also observed once, on the pre-narrowing tree: a single test failure that did not reproduce across 16 subsequent runs, so it is unidentified rather than diagnosed. Noted here rather than left silent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwhKDz2xEVEtYECxT7q5F3
…tional suite Adds functional (behavioural) testing, not just visual, across the two layers that had no coverage at all: the Godot scene/UI layer and the deployed browser build. WHY A BRIDGE WAS NEEDED A Godot web export renders everything into a single <canvas>. Menus, buttons and labels are drawn inside it, so Playwright's locator model -- getByRole, getByText, toBeVisible -- sees nothing but one element. Playwright can already *drive* the app with no changes (real key and mouse events reach the engine); what was missing was any way to *observe* it. Without a hook the only possible assertions are "a canvas exists" and pixel comparison. scripts/testing/TestBridge.cs supplies that, opt-in via ?test=1 (or any ?seed=): window.__mazeTestApi "1" when live, absent when disabled window.__mazeState JSON string of app state, republished on change window.__mazeCommand function(jsonString), fire-and-forget Design choices are constraints, not preferences, and each is documented: - Fire-and-forget commands. Godot's docs never state that a create_callback return value reaches the JS caller and no example returns one, so results are observed via state rather than depending on unspecified behaviour. Callbacks must also take exactly one Array argument, which a single JSON entry point satisfies. - No JavaScriptBridge.Eval. The docs say eval "may be disabled in custom export templates" and this project uses a patched template, so GetInterface plus property assignment avoids that path entirely. - Hand-rolled JSON. The web build is trimmed; a reflection-based serializer is exactly what trimming breaks, and it would fail only in the browser. - Protocol split into TestBridgeProtocol (Godot-free) so the NUnit suite can cover the fiddly parsing -- 54 new tests. The Node itself only runs inside the engine. - Also seeds from the URL, which was the outstanding prerequisite for the visual suite. SCENE TESTS, AND WHY NOT gdUnit4Net gdUnit4Net was tried first. On Godot 4.7.1 with gdUnit4.api 5.1.0-rc5 and gdUnit4.test.adapter 3.1.1: the project restores and builds with no conflict, and logic-only [TestCase] tests pass -- but every [RequireGodotRuntime] test fails with "Starting GodotRuntimeExecutor failed. The operation has timed out. Failed to connect: Connection timeout". Setting GodotProjectDir and pre-importing did not help. Isolated the cause: Godot 4.7.1 runs this project headless and executes its C# fine (the GameState autoload's _Ready fires). So the blocker is gdUnit4's own executor, not Godot and not this project -- consistent with its stated support stopping at Godot 4.4.1, last release June 2025. So tests/scene/SceneTestRunner.cs is a ~200-line in-engine runner: a list of checks, a tally, and a process exit code. Compiled only under -p:IncludeSceneTests=true so it never ships in a game export. Delete it in favour of gdUnit4Net once that supports 4.7+; the checks port over almost verbatim. This is the first real coverage of scripts/ui/ (~4300 lines). The nearest thing before was a test reading menu.tscn as *text* and asserting it contained the string "ComparisonButton" -- proving a node name appears in a file, not that it is a Button or that the scene instantiates. VERIFIED BY EXECUTION Correcting an earlier claim of mine: Godot code *is* buildable here. GodotSharp 4.7.1 targets net8.0, so the available .NET 8 SDK compiles it, and the official Godot 4.7.1 Linux build runs it headless. So this is executed, not reasoned: - 493 NUnit tests pass (was 439; +54 for the bridge wire format). - Godot project builds clean, with and without -p:IncludeSceneTests=true, which also proves the runner is excluded from a normal build. - 7/7 scene tests pass in real Godot 4.7.1 headless, exit code 0. Failure path checked too by deliberately breaking a check: 6/7, exit code 1, so CI genuinely gates. - Playwright: all 18 tests across 3 files enumerate (TypeScript compiles); the 4 harness self-tests still pass. - benchmarks and experiments still build; all workflow YAML parses. NOT VERIFIED: whether the bridge behaves under the *patched* web export template. That needs a deploy, and it is the one thing unit and scene tests cannot answer. Both browser suites are therefore gated on MAZE_TEST_BRIDGE=1 and currently skip rather than risk a false red -- see docs/TEST_BRIDGE.md "Enabling in CI" for the three checks to run on a /preview before flipping it. CI: new scene-tests job on every PR, downloading the official upstream Linux editor checksum-pinned in .github/editor-checksums.txt exactly like the patched Windows one. The fork is only needed for the web export. The functional suite runs post-deploy alongside the visual one. Docs: docs/TESTING.md (layer map, push-tests-down rationale, gdUnit4Net findings, known gaps), docs/TEST_BRIDGE.md (contract, per-decision rationale, verification status, security notes), plus VISUAL_REGRESSION.md, REGRESSION_TESTING.md and AGENTS.md updated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwhKDz2xEVEtYECxT7q5F3
…e behaviour Segregates application behaviour from Godot so it can be integration-tested without the engine, and covers the code that segregation exposes. 493 -> 550 tests. WHY: the codebase was already nearly segregated; the test project just didn't include it. Measured Godot-coupling density in scripts/ui/: AnimationController.cs 254 lines, 0 Godot refs (no `using Godot;`) ImportExportResult.cs 54 lines, 0 MazeImportExport.cs 363 lines, 9 (FileAccess, OS only) PathVisualizationSettings.cs 206 lines, 27 (all one type: Color) MazeMain.cs 1473 lines, 156 (Input. x28, GetNode x24, AddChild x13) GraphViewRenderer.cs 631 lines, 73 (genuine drawing) And GameState -- the app's state hub, 266 lines -- had an *entire* Godot surface of `: Node`, `_Ready`, `_ExitTree`, one GD.Print and one Mathf.Clamp. Nearly all logic, none of it testable, because the NUnit suite builds without the Godot SDK. CHANGES - scripts/session/MazeSession.cs (new): all session state and operations -- settings, current maze, level navigation, alternative paths, animation state, generation and import -- with no Godot dependency. Takes an optional ServiceContainer so tests can inject one. Mathf.Clamp becomes Math.Clamp. - GameState becomes a thin adapter forwarding to it. Its public API is unchanged, so nothing in scripts/ui or scripts/testing needed editing. - PathVisualizationSettings deliberately stays on the node: it is presentation config built on Godot's Color, and its DecisionDetailLevel enum lives in the same file, so moving it would reintroduce the dependency the split removes. GameState resets those presentation flags itself after delegating the session reset. - Test project now compiles MazeSession, AnimationController, ImportExportResult and GraphLayoutType. Adding a file to that list is a claim that it is Godot-free, and the build enforces the claim. TESTS - AnimationControllerTests (30 tests): a 254-line playback state machine that had zero coverage -- not because it was hard to test but because of where it lived. Time is a Update(deltaTime) parameter, so no clock or waiting is involved. Covers transport controls, stepping bounds, time-driven advance, speed clamping, notifications, and pins two real behaviours: a large frame delta advances only one step, and replay after finishing rewinds. - MazeSessionTests (24 tests): whole flows, previously impossible -- generate then navigate then regenerate, import adopting its own size, import clearing state left from the previous maze, animation driven through session state. A REAL BUG, FOUND ON THE FIRST RUN GameState.LoadImportedMaze applies dead-end wrapping and then builds a graph. That combination threw "Nullable object must have a value", so **importing any maze crashed the app**. GraphBuilder.GetGraphEdges walks a corridor until it reaches a junction or the start/end. A dead-end cell has exactly one direction -- the one you arrived from -- so it is neither a junction (needs more than two) nor a terminus, and the walk dereferenced a null Direction?. Dead-end wrapping *creates* such cells by hiding passages. Isolated it: GetGraph on a plain maze passes; after DoDeadEndWrapping it throws, on generated and imported mazes alike. Several existing tests did wrapping, and several did graph building -- none did both, which is how it survived. Fixed by dropping the edge when a corridor dead-ends, rather than pointing it at the dead-end cell: the graph's node set is junctions plus start/end, and consumers look edges up with graph.Nodes[edge.Point], so an edge to a non-node would trade one crash for a KeyNotFoundException. GraphBuilderDeadEndTests covers it, including that every edge still targets a known node and that wrapped mazes remain solvable. VERIFIED BY EXECUTION - 550 NUnit tests pass (was 493). - Reverting only the GraphBuilder guard fails 10 tests while the plain-maze control still passes, so the regression tests are genuinely coupled to the fix. - Godot project builds clean; 7/7 headless scene tests still pass in Godot 4.7.1; benchmarks and experiments build. Docs: docs/TESTING.md gains the coupling measurements, the three-tier plan, the MazeSession rationale and the bug write-up; AGENTS.md records the rule that behaviour belongs in plain C# with Node subclasses as thin adapters. Next in this direction, deliberately not done here: abstract FileAccess/OS in MazeImportExport so import/export round-trips become testable, then pull orchestration out of MazeMain incrementally. GraphViewRenderer's drawing should stay where it is -- render code's contract is the pixels, which is what the visual suite is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwhKDz2xEVEtYECxT7q5F3
…sm data race
`[assembly: Parallelizable(ParallelScope.All)]` was combined with NUnit's default
SingleInstance lifecycle, so test cases in a fixture ran concurrently against one
shared instance. Every field assigned in `[SetUp]` was therefore a data race.
It surfaced as two NullReferenceExceptions in RandomValueTests on CI, on a commit
whose other runs of the same job were green — re-running the identical commit
passed, confirming a race rather than an environment difference. The mechanism:
_mazePointFactory = new Mock<IMazePointFactory>(); // (a) bare mock
_mazePointFactory.Setup(x => x.MakePoint(...)).Returns(...); // (b) configures it
_randomPoint = new RandomPointGenerator(_random,
_mazePointFactory.Object); // (c) re-reads the field
Another test's (a) landing between this test's (b) and (c) makes (c) capture an
unconfigured mock. Moq then returns default(MazePoint) — null — and the test dies
dereferencing `point.X`, nowhere near the actual cause.
Measured with a trace harness (NUnit's per-test console capture reorders output and
hides this): with SingleInstance, four concurrent `[SetUp]` bodies share one fixture
instance; with InstancePerTestCase, four concurrent tests get four distinct
instances. Parallelism is unchanged, the unsafe sharing is gone.
Also drops MovementHelperTests' `[NonParallelizable]`, which was a local workaround
for this same root cause, and adds a guard test so removing the attribute fails
immediately by name instead of resurfacing as an occasional unexplained flake.
…testability-3a6h5m
…2-determinism-3a6h5m
This branch and `main` had no Node or Playwright ignore rules, so nothing stopped an `npm ci` in tests/visual/ from being committed. That is exactly how 170 files / 17.7 MiB of node_modules got into the GitHub-Actions branch (#7), which is also based on `main`. Later branches in this stack do carry `tests/visual/`-anchored rules, added with the visual-regression harness. These are deliberately repo-wide rather than anchored: the trap is a node_modules appearing somewhere the anchored pattern doesn't cover. Baseline screenshots under *-snapshots/ stay tracked; only regenerated run output is ignored.
…2-determinism-3a6h5m # Conflicts: # .gitignore
…testability-3a6h5m
…2-determinism-3a6h5m
…testability-3a6h5m
AGENTS.md keeps this branch's testing/session sections and re-adds the Repository Conventions section from main (deduplicated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rtkelly13
added a commit
that referenced
this pull request
Jul 27, 2026
…a6h5m Both this branch and main had extracted the composite action's inline PowerShell into script files; this branch's extraction (17 scripts, mechanically lifted and diffed) supersedes main's 8, so its versions are kept and main's now-unreferenced download-editor.ps1 is removed. Re-adds main-only .gitignore entries and the AGENTS.md Repository Conventions section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 3 of 4 — split out of #6. Based on #9; review that first.
Adds functional (behavioural) testing across the two layers that had no coverage at all: the Godot scene/UI layer and the deployed browser build. Then segregates behaviour from Godot so most of it can be tested without the engine.
Why a bridge was needed
A Godot web export renders everything into a single
<canvas>. Playwright's locator model —getByRole,getByText,toBeVisible— sees nothing but one element. Playwright can already drive the app (real key/mouse events reach the engine); what was missing was any way to observe it.scripts/testing/TestBridge.cssupplies that, opt-in via?test=1(or any?seed=):Every design choice is a verified constraint, not a preference:
create_callbackreturn value reaches JS, so results are observed via state rather than depending on unspecified behaviour. Callbacks must also take exactly oneArrayarg, which a single JSON entry point satisfies.Eval— the docs say it "may be disabled in custom export templates", and this project uses a patched one.GetInterface+ property assignment avoids that path.TestBridgeProtocol(Godot-free) so the NUnit suite covers the fiddly parsing — 54 tests.It also implements the URL seeding the visual suite was waiting on.
Scene tests, and why not gdUnit4Net
gdUnit4Net was tried first. On Godot 4.7.1 with
gdUnit4.api 5.1.0-rc5: builds and restores cleanly, logic-only tests pass, but every[RequireGodotRuntime]test fails withStarting GodotRuntimeExecutor failed. The operation has timed out.Setting<GodotProjectDir>and pre-importing didn't help.Isolated the cause: Godot 4.7.1 runs this project headless and executes its C# fine (the
GameStateautoload's_Readyfires). So the blocker is gdUnit4's own executor — consistent with its stated support stopping at Godot 4.4.1, last release June 2025.So
tests/scene/SceneTestRunner.csis a ~200-line in-engine runner: checks, a tally, a process exit code. Compiled only under-p:IncludeSceneTests=trueso it never ships in a game export. This is the first real coverage ofscripts/ui/(~4300 lines) — the nearest thing before was a test readingmenu.tscnas text and asserting it contained"ComparisonButton".Segregating behaviour from Godot
Measured coupling showed the codebase was already nearly segregated — the test project just didn't include it.
AnimationController(254 lines, zero Godot refs) compiles into the Godot-free suite unchanged, and had zero tests.GameStatewas 266 lines whose entire Godot surface was: Node,_Ready,_ExitTree, oneGD.Printand oneMathf.Clamp. It's now a thin adapter overMazeSession, which has no Godot dependency. Public API unchanged, so nothing inscripts/uineeded editing.It found a real bug on its first run.
GameState.LoadImportedMazewraps dead ends then builds a graph, and that combination threwNullable object must have a value— importing any maze crashed the app. A dead-end cell has exactly one direction (the one you arrived from), soGraphBuilder's corridor walk treated it as neither junction nor terminus and dereferenced null. Several existing tests did wrapping; several did graph building; none did both.Fixed by dropping the edge rather than pointing it at the dead-end cell — the node set is junctions + start/end and consumers use
graph.Nodes[edge.Point], so inventing a node target would trade a null-deref for aKeyNotFoundException.Verification (executed, not reasoned)
Correcting an earlier claim of mine: Godot code is buildable here —
GodotSharptargetsnet8.0.-p:IncludeSceneTests=true(proving the runner is excluded from normal builds).GraphBuilderguard fails 10 tests while the plain-maze control still passes.MAZE_TEST_BRIDGE=1and currently skip rather than risk a false red. #6 makes the smoke test answer this automatically on every deploy.Docs
docs/TESTING.md(layer map, coupling measurements, gdUnit4Net findings, known gaps) anddocs/TEST_BRIDGE.md(contract, per-decision rationale, verification-status table, security notes).Generated by Claude Code