Skip to content

feat(test): test bridge, headless scene tests, functional suite, MazeSession split - #10

Merged
rtkelly13 merged 17 commits into
mainfrom
claude/split-3-testability-3a6h5m
Jul 27, 2026
Merged

feat(test): test bridge, headless scene tests, functional suite, MazeSession split#10
rtkelly13 merged 17 commits into
mainfrom
claude/split-3-testability-3a6h5m

Conversation

@rtkelly13

Copy link
Copy Markdown
Owner

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.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

Every design choice is a verified constraint, not a preference:

  • Fire-and-forget commands — Godot's docs never state a create_callback return value reaches JS, so results are observed via state rather than depending on unspecified behaviour. Callbacks must also take exactly one Array arg, which a single JSON entry point satisfies.
  • No 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.
  • Hand-rolled JSON — the web build is trimmed, and reflection-based serializers are exactly what trimming breaks, failing only in the browser.
  • Protocol split into 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 with Starting 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 GameState autoload's _Ready fires). 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.cs is a ~200-line in-engine runner: checks, a tally, a process exit code. Compiled only under -p:IncludeSceneTests=true so it never ships in a game export. 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 "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.

GameState was 266 lines whose entire Godot surface was : Node, _Ready, _ExitTree, one GD.Print and one Mathf.Clamp. It's now a thin adapter over MazeSession, which has no Godot dependency. Public API unchanged, so nothing in scripts/ui needed editing.

It found a real bug on its first run. GameState.LoadImportedMaze wraps dead ends then builds a graph, and that combination threw Nullable object must have a valueimporting any maze crashed the app. A dead-end cell has exactly one direction (the one you arrived from), so GraphBuilder'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 a KeyNotFoundException.

Verification (executed, not reasoned)

Correcting an earlier claim of mine: Godot code is buildable here — GodotSharp targets net8.0.

  • 550 tests pass (was 443).
  • Godot project builds clean, with and without -p:IncludeSceneTests=true (proving the runner is excluded from normal builds).
  • 7/7 scene tests pass in real Godot 4.7.1 headless, exit 0 — and the failure path was checked by deliberately breaking a check: 6/7, exit 1, so CI genuinely gates. Confirmed on real CI too.
  • Reverting only the GraphBuilder guard fails 10 tests while the plain-maze control still passes.
  • All 18 Playwright specs enumerate; the 4 harness self-tests pass.

⚠️ Not verified: whether the bridge behaves under the patched web export template. Both browser suites are gated on MAZE_TEST_BRIDGE=1 and 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) and docs/TEST_BRIDGE.md (contract, per-decision rationale, verification-status table, security notes).


Generated by Claude Code

claude added 5 commits July 25, 2026 10:45
….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
claude added 11 commits July 25, 2026 21:45
…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.
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
Base automatically changed from claude/split-2-determinism-3a6h5m to main July 27, 2026 22:30
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
rtkelly13 merged commit f093584 into main Jul 27, 2026
3 checks passed
@rtkelly13
rtkelly13 deleted the claude/split-3-testability-3a6h5m branch July 27, 2026 22:36
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants