Co-located C# tests: layout gate + singular `.Test.cs` sweep from main, then withdraw #485 nf-iocb ← Beads

closed priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/990 by tools/beads/import_github.py's reconcile pass.

Aedan's ask, verbatim (chat, 2026-08-17):

> "cleaning up https://github.com/cfsh/ninefold/pull/485 esp given all the recent designs. the prescription could be to start a fresh stack of PRs, and eventually just withdraw 485"

> "probes are in-scope too, instead of living in tools/probes, should be somewhere , maybe they need local folders, or maybe we can handle something like tonapse/playback_probe.json? give an option for local folders and pros / cons . Remember games will continue to expand to many scenes (look at explore)"

First of three chunks (Aedan: "split into separate chunks, this will be a lot of context for poor devs"): this issue is the C# chunk only — the layout gate, the .Test.cs sweep, and withdrawing #485. The Python test move is #995 (sequenced after this); the probe move is #996 (independent, parallel-safe).

The asks the original work answered, verbatim (from #485 / #494):

> "i am not sure the style guide for where we're putting tests atm, e.g. tools/{nflib,tests}/{foo.py,foo_test.py}. I propose we always put tests next to the relevant file, no tests directory (test_data directories allowed). Do u concur? Also should we codify this in a ninefold/STYLE_GUIDE.md?"

> "global globs are risky for these reasons, suppose we end up with SitMathTest as a real game class."

And the one review ask on #485, inline on shared/UserPathGuard.Tests.cs (comment id 3708955302), still unactioned and binding on this work:

> "change the suffix to singular .Test.cs to align with _test.py"

Context

#485 (branch chore/co-located-tests, head 16e7a59) implemented the co-location convention, a layout presubmit lane, and STYLE_GUIDE.md. It is now CONFLICTING and ~195 commits behind main (branched at 320d990, 2026-08-04): every PR it deferred around has since merged (#434 explore, #466/#470/#472 the design-001 ports), games/explore/tests/ grew from 3 test files to 9, and the owning session was archived — full handoff state is at <https://github.com/cfsh/ninefold/issues/494#issuecomment-5201025383>. Rebasing a 42-file sweep across that gap re-litigates every hunk; the *decisions* in it are the durable part (B12 — relish deleting; A4 — the mainline fix is a clean re-execution, not a heroic rebase).

Current scope on main, measured 2026-08-17:

Prescription

Build a fresh stack from today's main; #485 is a quarry, not a base (B10 — the prior art is on branch origin/chore/co-located-tests: the layout_problems() seam + 9 unit tests, the STYLE_GUIDE.md text, the .gitignore rule; mine them, do not rebase them).

The marker is .Test.cs — singular. This is decided, per Aedan's inline ask above; it is not an open question. All of #485's dot-not-suffix reasoning survives verbatim (a dot is not legal in a C# type name, so SitMath.Test.cs can never collide with a real class SitMathTest). Python stays _test.py — a dot is not an importable module name; the guide states the asymmetry is forced by the languages.

The stack (E3 — one cognitive level per PR; E4 — write it whole, split for review)
  1. PR 1 — the gate + the guide. STYLE_GUIDE.md (convention stated in present tense, C6; cite this issue from its transition section) and the layout lane in presubmit.py, registered in the SUITES dict beside python/dotnet/shellcheck (presubmit.py:236). Port the lane from #485's branch: over dotnet msbuild -getItem:Compile on all three projects (plus installer/NinefoldInstaller.csproj, outside the sln), assert (a) every *.Test.cs is compiled by the test project, (b) no *.Test.cs is compiled by a game project, (c) every production .cs is compiled by something. Not "exactly one project" — pure-logic sources are cross-linked into both deliberately; keep the unit test that pins this so nobody tightens it. Predicates accept both layouts (/tests/ dir OR .Test.cs) until PR 3 — one shared is_test_source() helper, never two call sites (B7: a convention nothing checks is how #485's world drifted in the first place). The lane also models Godot-runtime tests*.GodotTest.cs (per aedanpope: tests_godot/ControllerNavTest.cs is included, not exempt): compiled by the game project (they need the Godot runtime), never by the xUnit project — the inverse of checks (a)/(b) — and each must be referenced by a sibling *_test.tscn. The distinct marker keeps the classes glob-separable (ControllerNav.GodotTest.cs does not match *.Test.cs), so its tracked .cs.uid is untouched by the *.Test.cs.uid gitignore rule. Port the lane's unit tests; they join the python lane.
  2. PR 2 — the C# move. git mv every file in the six directories above to a co-located <Subject>.Test.cs; class names keep their Tests suffix (filename ≠ type name, per #485's §1.3 — leave unless Aedan says otherwise). Swap each tests\*.cs csproj row for a *.Test.cs glob row; drop the .Tests namespace segments; delete the tests/ dirs and their .gdignores; gitignore *.Test.cs.uid (Godot regenerates one per co-located test on import — production .cs.uid stays tracked). Two known #485 defects to NOT re-import if copying hunks: its Ninefold.csproj block comment claimed the exactly-one invariant the lane deliberately doesn't enforce, and its CLAUDE.md edit left a dangling Ninefold.Tonapse.Tests example — see #494's handoff comment, items F1/F2. Also move tests_godot/ into the sweep: tests_godot/ControllerNavTest.cs tests Tonapse (it loads res://games/tonapse/tonapse.tscn), so it becomes games/tonapse/ControllerNav.GodotTest.cs with controller_nav_test.tscn beside it; update the .tscn's ext_resource path, the scene path in run_godot_tests.sh (still one exec, still within shell policy), the namespace (Ninefold.Tonapse.GodotTestsNinefold.Tonapse), and the pointer in tools/tests/test_notes_adding_a_game.py:63; delete the tests_godot/ directory. Its .cs.uid stays tracked and it must NOT be .gdignored — Godot loads it as a scene script.
  3. PR 3 — delete the transition scaffolding, and make the old layout impossible to reintroduce. Remove the dual-layout acceptance from is_test_source(), and add the terminal rule: the lane fails on any tracked .cs under a tests/ directory (test_data/ exempt). Without this rule the lane checks only *wiring*, not *placement* — a correctly-wired new games/x/tests/ dir added later would classify as production, be compiled by something, and pass, putting the convention back to being enforced by nothing (B7). The Python side needs no extra rule: #995's reconciliation guard covers the _test.py side. Unit-test both directions: a tests/-dir .cs fails; a test_data/ file passes.
Test plan
Non-goals
Open question, with default
  1. SynthWaveformDump placement#485 moved it to shared/ (imports only Ninefold.Shared); its comment says it mirrors Tonapse's WaveformView. Default: shared/, matching what it imports.

Refs #485, #494, #782, #918, #995, #996, design 005 (piece 7/S13)

Dependencies

None.

Comments

No comments.

Add a comment