Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0d664fa
ci(web): single-source toolchain versions, patch-level drift guard, 4…
claude Jul 25, 2026
761e70f
feat(test): seed-deterministic generation, regression foundation, vis…
claude Jul 25, 2026
48c6e45
fix(test): narrow PerfectAgent sample to stop the suite hanging in CI
claude Jul 25, 2026
4fd8479
feat(test): in-app test bridge, headless scene tests, Playwright func…
claude Jul 25, 2026
9c2cf69
refactor: extract MazeSession from GameState, cover the newly testabl…
claude Jul 25, 2026
0b3a5c9
fix(test): run one fixture instance per test case, fixing a paralleli…
claude Jul 25, 2026
68518c5
Merge branch 'claude/split-2-determinism-3a6h5m' into claude/split-3-…
claude Jul 25, 2026
8dabeec
Merge branch 'claude/split-1-web-toolchain-3a6h5m' into claude/split-…
claude Jul 25, 2026
1000510
docs(test): record the fixture-lifecycle requirement and how it was d…
claude Jul 25, 2026
9f1e935
docs: refresh unit-test count for the lifecycle guard test
claude Jul 25, 2026
13e49ae
chore: gitignore node_modules and Playwright run output
claude Jul 26, 2026
0e25490
Merge branch 'claude/split-1-web-toolchain-3a6h5m' into claude/split-…
claude Jul 26, 2026
1266a2c
Merge branch 'claude/split-2-determinism-3a6h5m' into claude/split-3-…
claude Jul 26, 2026
5646ef4
Merge remote-tracking branch 'origin/main' into claude/split-1-web-to…
claude Jul 26, 2026
e4aaeec
Merge branch 'claude/split-1-web-toolchain-3a6h5m' into claude/split-…
claude Jul 26, 2026
9083e08
Merge branch 'claude/split-2-determinism-3a6h5m' into claude/split-3-…
claude Jul 26, 2026
74166de
Merge main (squashes of #8/#9) into claude/split-3-testability-3a6h5m
rtkelly13 Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/editor-checksums.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
#
# To add an entry when bumping, see docs/WEB_EXPORT.md → "Updating the pinned editor".
ad76e72610187b13e83229e863928c32689b1ba5dda34f5210940d563b89e473 Godot_v4.7.1-stable_mono_web_export_win64.zip
# Official upstream Linux editor, used by the headless scene-test job (docs/TESTING.md).
# Not the patched fork — plain Godot is enough to run in-engine tests.
6ca7ff0459f1b806900be683c1b0837c607a9c16834c530dc68c81b9fc3ae1f6 Godot_v4.7.1-stable_mono_linux_x86_64.zip
b1f1b387dd45c6f3db35b336f58d40d6ea7ea0c8d7597d4fc26b493f4d12347c Godot_v4.7-stable_mono_web_export_win64.zip
72 changes: 72 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,78 @@ jobs:
- name: Test
run: dotnet test tests/ProceduralMaze.Tests.csproj -c Debug --nologo

# In-engine tests for the Godot scene/UI layer, which the NUnit suite cannot reach (it
# deliberately builds without the Godot SDK). Runs the official upstream Linux editor
# headless -- the patched Windows fork is only needed for the *web export*, not for this.
#
# Not gdUnit4Net: its Godot-runtime executor fails to start on Godot 4.7.1
# ("Failed to connect: Connection timeout"), while plain Godot runs the project fine.
# See docs/TESTING.md -> "Why not gdUnit4Net".
scene-tests:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
GODOT_VERSION: "4.7.1-stable"
GODOT_ASSET: "Godot_v4.7.1-stable_mono_linux_x86_64"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up .NET 9 SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"

- name: Cache Godot editor
id: godotcache
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/godot
key: godot-linux-${{ env.GODOT_VERSION }}

- name: Download Godot (checksum-pinned)
if: steps.godotcache.outputs.cache-hit != 'true'
run: |
mkdir -p "$RUNNER_TEMP/godot"
curl -sSL -o "$RUNNER_TEMP/godot/$GODOT_ASSET.zip" \
"https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION/$GODOT_ASSET.zip"

- name: Verify Godot checksum
run: |
# Same supply-chain guard as the patched web editor: never run an unverified binary.
expected=$(grep -v '^\s*#' .github/editor-checksums.txt | grep "$GODOT_ASSET.zip" | awk '{print $1}')
if [ -z "$expected" ]; then
echo "::error::No pinned checksum for $GODOT_ASSET.zip in .github/editor-checksums.txt"
exit 1
fi
actual=$(sha256sum "$RUNNER_TEMP/godot/$GODOT_ASSET.zip" | awk '{print $1}')
if [ "$expected" != "$actual" ]; then
echo "::error::Checksum mismatch for $GODOT_ASSET.zip (expected $expected, got $actual)"
exit 1
fi
echo "Checksum OK: $actual"

- name: Extract Godot
run: |
unzip -q -o "$RUNNER_TEMP/godot/$GODOT_ASSET.zip" -d "$RUNNER_TEMP/godot"
find "$RUNNER_TEMP/godot" -type f -name "Godot_v*_mono_linux.x86_64" -exec chmod +x {} \;

- name: Build with scene tests
run: dotnet build ProceduralGeneration3DMazes.csproj -c Debug --nologo -p:IncludeSceneTests=true

- name: Import project (headless)
run: |
GODOT=$(find "$RUNNER_TEMP/godot" -type f -name "Godot_v*_mono_linux.x86_64" | head -1)
# First open builds the resource cache; a cold project cannot run a scene.
"$GODOT" --headless --path . --import
continue-on-error: true

- name: Run scene tests (headless)
run: |
GODOT=$(find "$RUNNER_TEMP/godot" -type f -name "Godot_v*_mono_linux.x86_64" | head -1)
# The runner exits non-zero when any check fails; verified locally.
"$GODOT" --headless --path . res://tests/scene/scene_tests.tscn

# Keeps the visual-regression harness verified on every PR without needing a deployed
# build. The maze suite itself can only run post-deploy (see web-export.yml), so without
# this the harness would sit untested until someone needed it.
Expand Down
38 changes: 28 additions & 10 deletions .github/workflows/web-export.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@ jobs:
# Visual regression against the DEPLOYED build, after the smoke test proves it boots.
# Screenshotting a build that didn't start just yields a blank baseline.
#
# MAZE_SEEDING gates the maze suite: until the web build reads generation parameters from
# the query string, screenshots are nondeterministic, so the suite skips rather than
# reporting a false red. See docs/VISUAL_REGRESSION.md -> "Prerequisite: URL-parameter
# seeding", then set this to "1".
# MAZE_TEST_BRIDGE gates both browser suites. The in-app bridge
# (scripts/testing/TestBridge.cs) provides URL seeding and the state channel they need. It
# is unit- and scene-tested, but whether it survives the *patched* web export template is
# unproven until a deploy exists to check against -- so both suites skip rather than risk a
# false red. Flip to "1" after confirming window.__mazeTestApi === "1" on a deploy.
# See docs/TEST_BRIDGE.md -> "Enabling in CI".
visual-production:
needs: [production, smoke-production]
if: needs.production.outputs.url != ''
Expand All @@ -144,11 +146,18 @@ jobs:
npm ci
npx playwright install --with-deps chromium

- name: Functional tests (via the in-app test bridge)
working-directory: tests/visual
env:
MAZE_URL: ${{ needs.production.outputs.url }}
MAZE_TEST_BRIDGE: "0"
run: npx playwright test --project=functional

- name: Visual regression
working-directory: tests/visual
env:
MAZE_URL: ${{ needs.production.outputs.url }}
MAZE_SEEDING: "0"
MAZE_TEST_BRIDGE: "0"
run: npx playwright test --project=maze

- name: Upload visual diff on failure
Expand Down Expand Up @@ -261,10 +270,12 @@ jobs:
# Visual regression against the DEPLOYED build, after the smoke test proves it boots.
# Screenshotting a build that didn't start just yields a blank baseline.
#
# MAZE_SEEDING gates the maze suite: until the web build reads generation parameters from
# the query string, screenshots are nondeterministic, so the suite skips rather than
# reporting a false red. See docs/VISUAL_REGRESSION.md -> "Prerequisite: URL-parameter
# seeding", then set this to "1".
# MAZE_TEST_BRIDGE gates both browser suites. The in-app bridge
# (scripts/testing/TestBridge.cs) provides URL seeding and the state channel they need. It
# is unit- and scene-tested, but whether it survives the *patched* web export template is
# unproven until a deploy exists to check against -- so both suites skip rather than risk a
# false red. Flip to "1" after confirming window.__mazeTestApi === "1" on a deploy.
# See docs/TEST_BRIDGE.md -> "Enabling in CI".
visual-preview:
needs: [preview, smoke-preview]
if: needs.preview.outputs.url != ''
Expand All @@ -284,11 +295,18 @@ jobs:
npm ci
npx playwright install --with-deps chromium

- name: Functional tests (via the in-app test bridge)
working-directory: tests/visual
env:
MAZE_URL: ${{ needs.preview.outputs.url }}
MAZE_TEST_BRIDGE: "0"
run: npx playwright test --project=functional

- name: Visual regression
working-directory: tests/visual
env:
MAZE_URL: ${{ needs.preview.outputs.url }}
MAZE_SEEDING: "0"
MAZE_TEST_BRIDGE: "0"
run: npx playwright test --project=maze

- name: Upload visual diff on failure
Expand Down
35 changes: 25 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ var result = services.MazeGenerationFactory.GenerateMaze(settings);

| Class | Purpose |
|-------|---------|
| `GameState` | Godot autoload singleton, holds settings and current maze |
| `MazeSession` | **Godot-free** session state and operations — where behaviour belongs |
| `GameState` | Thin Godot autoload adapter forwarding to `MazeSession` |
| `ServiceContainer` | Manual DI container, instantiates all services |
| `MazeGenerationFactory` | Main entry point for maze generation |
| `MazeJumper` | Navigate through a generated maze |
Expand Down Expand Up @@ -107,7 +108,9 @@ In Godot 2D rendering:
- Benchmarks: `benchmarks/ProceduralMaze.Benchmarks.csproj`
- Godot scenes: `scenes/*.tscn`
- Maze logic: `scripts/maze/`
- Session state/behaviour (Godot-free): `scripts/session/`
- UI code: `scripts/ui/`
- Web test bridge: `scripts/testing/`

## Adding New Features

Expand All @@ -116,6 +119,27 @@ In Godot 2D rendering:
3. Add tests in `tests/`
4. Add UI in `scripts/ui/` and `scenes/`

## Testing

Four layers, each with a different cost. **Push tests down** — see
[docs/TESTING.md](./docs/TESTING.md) for which to use.

| Layer | Command | Needs |
|---|---|---|
| Unit + integration (551 tests, ~10s) | `cd tests && dotnet test` | .NET only |
| Scene / UI (in-engine) | `dotnet build -p:IncludeSceneTests=true` then `godot --headless --path . res://tests/scene/scene_tests.tscn` | Godot binary |
| Functional (browser) | `cd tests/visual && npx playwright test --project=functional` | deployed build |
| Visual | `cd tests/visual && npx playwright test --project=maze` | deployed build |

**Keep behaviour out of Godot types.** New logic belongs in a plain C# class that the unit
suite compiles (`scripts/session/`, `scripts/maze/`); Godot `Node` subclasses should be thin
adapters that forward to it — `GameState` → `MazeSession` is the pattern. Adding a file to the
test project's `<Compile Include>` list is a claim that it is Godot-free, and the build
enforces that claim. Only genuinely engine-bound code (drawing, input, node wiring, scene
lifecycle) should need scene tests. Browser tests need the in-app test bridge
([docs/TEST_BRIDGE.md](./docs/TEST_BRIDGE.md)) because a Godot web export is a single
`<canvas>` with no DOM for Playwright to query.

## Randomness & Determinism (read before touching generation)

Maze generation is **seed-deterministic**: the same `MazeGenerationSettings.Seed` plus the
Expand Down Expand Up @@ -239,15 +263,6 @@ cd benchmarks && dotnet run -c Release -- --filter "*ShortestPath*" -j short 2>&
- `DirectionsFlagParser.SplitDirectionsFromFlag` - Called frequently, allocates arrays
- Maze generation algorithms - Main user-facing performance


## 🛑 Repository Conventions & Workflow Policy

1. **Squash Merge Only**: All pull requests must be merged into `main` using **Squash and Merge** exclusively.
2. **Delete Branch on Merge**: Feature branches must be automatically deleted immediately upon merge into `main`.
3. **Linear History**: Maintain a strictly linear history. Rebase feature branches onto `main` before merging; no merge commits allowed.
4. **Direct Push Protection**: Non-force direct pushes to `main` are blocked; PR mechanism required (force pushes permitted when needed).


## 🛑 Repository Conventions & Workflow Policy

1. **Squash Merge Only**: All pull requests must be merged into `main` using **Squash and Merge** exclusively.
Expand Down
9 changes: 9 additions & 0 deletions ProceduralGeneration3DMazes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@
<Compile Remove="benchmarks\**\*.cs" />
</ItemGroup>

<!--
In-engine scene tests, added back after the blanket removal above (order matters in
MSBuild). Opt-in via -p:IncludeSceneTests=true so the runner never ships in a game
export; CI passes it for the headless scene-test job. See docs/TESTING.md.
-->
<ItemGroup Condition=" '$(IncludeSceneTests)' == 'true' ">
<Compile Include="tests\scene\*.cs" />
</ItemGroup>

<!--
Program.cs is the WASM entry point. It MUST NOT be compiled into the normal
(library) build, otherwise top-level statements trigger CS8805
Expand Down
3 changes: 3 additions & 0 deletions docs/REGRESSION_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
**Status:** Foundation landed (seeding + determinism guards). Golden-file suite designed
below, **not yet built**.

> This document covers the *generation* regression story. For which testing layer to use for
> what — unit, in-engine scene, browser functional, visual — see **[TESTING.md](./TESTING.md)**.

## Why the existing suite can't catch regressions

The 421 tests before this work were all *invariant* tests: is the maze valid, is every cell
Expand Down
Loading
Loading