test: separate diagnostic apparatus from release gates

This commit is contained in:
Erik 2026-08-18 11:06:08 +02:00
parent 8f490240d4
commit c1a905004a
33 changed files with 527 additions and 24 deletions

View file

@ -10738,7 +10738,7 @@ user descended the spiral (`retail-spiral2.log`, 767 samples). **Retail's flood
and IDENTICAL in character to ours** — from the spiral cells it swings `num` 3→27 with gaze
and COLLAPSES to 3 cells at many poses (e.g. cam=015d eye(60.05,-28.51,-3.66) → just
{015d 015e 015f}; cam=014b → {014b 014c 014a}). Our flood does the same (3→43, headless
`FloodDepthFrom015E_VsRetail26` = max 43 vs retail 26). So retail does NOT keep the spiral
`Diagnostic_FloodDepthFrom015E_VsRetail26` = max 43 vs retail 26). So retail does NOT keep the spiral
where we drop it — the flood is EXONERATED as the cause. **The vanish must be DOWNSTREAM of
the flood** (unexplored): the steps in these spiral cells are STATIC objects (GfxObj
`0x010000DE` ×6/cell), drawn via the separate viewcone cull (`ViewconeCuller.SphereVisibleInCell`

View file

@ -104,7 +104,7 @@ inline when ported.
### BR-1 — The surface gate — ✅ RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1)
**Premise falsified before implementation (the BR-1 pre-check,
`ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
`Diagnostic_ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
every portal fill** — all four extraction paths skip `Stippling.NoPos`
positive sides (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
`PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, `GfxObjMesh.Build:71`),

View file

@ -99,6 +99,16 @@ expected until the linked defect is fixed. Diagnostic apparatus
(`Purpose=Diagnostic`) likewise reports separately and does not inflate the
contract-test pass count.
The current diagnostic apparatus lives in App and Core. It is retained for
investigation output, and several methods require installed DATs:
```powershell
dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release `
--filter 'Purpose=Diagnostic'
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Release `
--filter 'Purpose=Diagnostic'
```
## Updating dependencies
Do not edit lock files by hand. To make an intentional dependency change:

View file

@ -371,7 +371,7 @@ area files.
> (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
> `PrepareCellStructMeshData:1394`, `CellMesh.Build:44`,
> `GfxObjMesh.Build:71`), and the fills have no negative surface
> (`ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every
> (`Diagnostic_ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every
> fill). The equivalence pin (`StipplingSurfaceEquivalenceTests`, 2,607
> polys, 0 violations) proves our build-time skip ⇔ retail's draw-time
> `skipNoTexture` on this content. Consequences: the ledger rows

View file

@ -83,7 +83,7 @@ draw the purple lightning over the floor"):
3. **Striped floor z-fight-like artifact.** User's 2nd screenshot: regular
magenta bands across one floor region, "like something is fighting to draw
the purple over the floor." **NOT attributed.** Ruled out: not coincident dat
geometry (the `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep
geometry (the `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep
found only the legit z=12 under-hall floor quad-fan, nothing near the 6
corridor floor); not a striped texture (all corridor surfaces are plain
`Base1Image` stone 0x08000375/6/7/8). Leading guess: two draws of the same
@ -116,7 +116,7 @@ draw the purple lightning over the floor"):
stair geometry owner (`0x8A020182`'s ramp shell, vertical portals, ZERO
statics), CellBSP containment (partitions exactly at portal planes),
under-hall + corridor drawn-poly surface colors, DXT1 alpha histograms (0
transparent texels), and `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
transparent texels), and `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
(the stripe-geometry sweep — came back empty for the 6 floor).
- **`tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs`**
— production-matched portal-flood replays (approach/descent/gaze-sweep/walk +

View file

@ -48,7 +48,7 @@ NOT throw more lighting fixes at it (two already did nothing).
| Lighting **selection** | `SelectForCell` (all dynamic lights per cell, retail-exact) → **no visual change** |
| Light-set **camera-cap churn** (the OLD "confirmed" #176 theory) | visible-cell scoping shipped (`c500912b`); probe-proven ~285 through-floor lights dropped/frame — symptom unchanged. **REFUTED.** |
| **Membership / the "flap"** | `ACDREAM_PROBE_FLAP`: render cell = `0x8A020164` on **100%** of 188,732 frames across **526 distinct camera angles**, `res=None` always; `ACDREAM_PROBE_PVINPUT` flood is stable-per-angle (17/10/8…), never oscillates at a fixed view; 1 `[cell-transit]` (the spawn teleport) total |
| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=6 corridor floor (only the benign same-cell z=12 under-hall floor tiling in `0x011E`) |
| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=6 corridor floor (only the benign same-cell z=12 under-hall floor tiling in `0x011E`) |
| A2C / alpha-hole see-through | corridor floor surface `0x08000377` is **fully opaque** (`alpha0Texels=0`, `transl=0.00`) |
| Translucent under-surface blend | the one translucent colored surface `0x08000034` is `NoPos` (not drawn) |
| Flat (per-face) normals | corridor floor uses **smooth per-vertex dat normals** (center `(0,0,1)`, corners tilted ~27° — retail-style edge smoothing), NOT flat (`CellVertexNormals_SmoothOrFaceted_Dump`) |
@ -129,7 +129,7 @@ RenderDoc (do NOT assume — capture and read):
- `ACDREAM_PROBE_INDOOR_LIGHT=1``[indoor-light]` scoped-pool SET composition.
- `tools/cdb/issue176-floor-light.cdb` — retail light-setup trace.
- `Issue176177DungeonSeamInspectionTests` — dat truth (coplanar sweep, floor
surfaces, vertex normals); `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
surfaces, vertex normals); `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
seed = `{0164,0165,016E,017A}`+neighbors.
## Repro + launch protocol

View file

@ -26,6 +26,50 @@ The six-case reduction from the earlier 14,748-pass headline is understood:
seven launcher editor-focus theory rows were consolidated into one fact that
still executes all seven variants in a single Avalonia application session.
## Fresh reproducible syntax inventory
`tools/audit-test-inventory.ps1` now regenerates the complete attributed-method
manifest from tracked C# test sources with Roslyn:
```powershell
pwsh ./tools/audit-test-inventory.ps1
```
The generated JSON lives under ignored `artifacts/test-audit/`; it is not a
second 6.8 MB checked-in source of truth. The script and this reviewed ledger
are durable, while paths and line numbers regenerate after every batch. The
inventory at commit `8f490240` reports:
| Syntax signal | Current count |
|---|---:|
| Tracked/parsed C# test files | 1,256 / 1,256 |
| Attributed test methods (not expanded theory cases) | 11,455 |
| Methods containing at least one empty `return;` | 399 |
| Empty-return sites | 426 |
| Prerequisite-return candidates | 282 |
| Mechanical output-only candidates | 47 |
| Methods directly using `Thread.Sleep` / `Task.Delay` | 20 / 23 |
| Methods directly reading environment variables | 47 |
| Methods directly reading `.cs` source text | 63 |
The 282 prerequisite-return count is intentionally a review candidate set, not
a claim that all 399 methods are broken. It comprises 109 App, 125 Core, 29
Content, 12 Launcher.Core, three Core.Net, three Bake, and one Headless method.
The classifier includes external-asset, opt-in, and platform gates; behavioral
early exits inside callback tests remain outside that candidate set. Every
candidate still requires source inspection because `Directory.Exists`, for
example, can also be the behavior under test.
The output-only classifier follows same-file helper calls, but it cannot decide
whether an assertion merely establishes probe setup. The reviewed diagnostic
set is therefore 51 methods: the 47 mechanical candidates plus
`OptionsPanelLiveMountProbeTests.ProbeLiveMountShapes`,
`DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection`, and
the two already classified tower diagnostics. Relative to T-011's historical
51-method table, delete the removed PVS scaffold and add
`TowerAscentReplayTests.Diagnostic_TowerAscent_PerStepTable`; all other entries
remain. Those 51 methods expand to 70 discovered cases (28 App, 42 Core).
## Exact 77-skip reconciliation
The latest clean-gate TRX files account for every reported skip:
@ -83,11 +127,11 @@ identifiable in the lane report.
| T-004 warning mismatches | resolved in R2 | Clean rebuild is zero-warning with repository-wide warnings-as-errors. |
| T-005 unreachable panel stack | requires production reachability proof | Do not delete 52 meaningful tests until the dead presentation surface and any supported plugin compatibility promise are verified together. |
| T-006 misleading installed-DAT reason | resolved in batch B | Shared opt-in is now `ACDREAM_RUN_INSTALLED_DAT_TESTS=1` (legacy switch retained), the reason names the lane, and all nine owners carry `Lane=InstalledDat`. |
| T-007 271 silent passing gates | open | Re-run the syntax inventory against current source, then convert each to a hermetic fixture, explicit prerequisite skip, or non-default lane. |
| T-007 271 silent passing gates | active; current candidate set generated | Current syntax has 282 prerequisite-return candidates across seven projects. Inspect/classify from the generated manifest; the count changed with source growth and includes platform gates, so do not mechanically rewrite all 282. |
| T-008 incapable-of-failing diagnostics | batch A plus diagnostic lane work | Delete the literal wiring smoke test; repair the GPU contract tautology; later re-home output-only apparatus. |
| T-009 wall-clock double-click tests | open | Introduce a behavior-preserving injectable monotonic clock and deterministic fake time. |
| T-010 two useless cases | high-confidence cleanup batch A | Delete `SmokeTest.TestProject_IsWired` and `ChaseCameraTests.ImplementsICamera`; compilation already proves both claims. |
| T-011 51 output-only methods | open | Validate the historical 51-method list against current source; add stable oracles or move them outside default discovery. |
| T-011 51 output-only methods | classified in batch C | The reviewed current set is 51 methods / 70 cases. All carry `Purpose=Diagnostic`, preserving the apparatus while removing it from release pass totals. Contract-shaped names remain explicitly flagged until a stable oracle exists. |
| T-012 source-text freezes | requires semantic replacement map | Retain whole-tree dependency rules; remove exact-text freezes only when an equivalent semantic/behavioral guard is identified. |
| T-013 controller self-comparison | high-confidence cleanup batch A | Capture the first controller next to the first body and compare every retry with that reference. |
| T-014 seven load-sensitive tests | active; first mechanism reproduced | `PortalProjectionTests.ClipToRegion_FrameOwnedStore_ReusesExactResultArray` reported 6,904 B only while three App test hosts ran concurrently, then passed in an isolated run. Preserve both observations and remove ambient process-allocation sensitivity rather than loosening the zero-allocation oracle. |
@ -179,3 +223,31 @@ at 6,904 bytes while the Installed-DAT and discovery hosts ran beside it. The
same unmodified hermetic command then passed 5,505/5,505 in isolation. This is
evidence for removing cross-host allocation sensitivity; it is not grounds to
weaken the assertion or claim a clean concurrent result.
## Batch C diagnostic classification
Batch C assigns the reviewed 51-method diagnostic set to
`Purpose=Diagnostic`. No body, output, historical comment, or fixture is
removed. This resolves the misleading release accounting without pretending
that every investigation has a stable regression oracle.
The 15 contract-shaped methods called out by T-011 now have a `Diagnostic_`
prefix: four facility-hub scenarios, six stair/camera sweeps, two ambient-slot
inventories, production-emission replication, and two drawn-polygon reports.
Their current output and historical method references are preserved, while the
new names no longer imply an asserted oracle. Promoting them back into the
hermetic lane requires an asserted expected result; R3 will not invent those
values.
Batch C verification:
- App Release build: 0 warnings, 0 errors;
- Core Release build, serialized after App: 0 warnings, 0 errors;
- hermetic App: 5,479 passed / 0 skipped / 0 failed;
- hermetic Core: 4,753 passed / 0 skipped / 0 failed; and
- diagnostic discovery: 28 App + 42 Core = 70 cases from 51 methods.
An attempted parallel App/Core build collided on the shared
`AcDream.Plugin.Abstractions` intermediate DLL. The repository gate serializes
projects, so the authoritative verification is the two isolated clean builds
above; no code or assertion was changed in response to that tooling collision.

View file

@ -541,6 +541,7 @@ public class CornerFloodReplayTests
/// <summary>Scratch: the homogeneous reciprocal primitive in isolation, on the
/// synthetic fixture geometry that Build_AppliesReciprocalOtherPortalClip uses.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Scratch_ReciprocalPrimitive_SyntheticPair()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);

View file

@ -14,6 +14,7 @@ namespace AcDream.App.Tests.Rendering;
/// candidate setup's parts + bounds from the dat. The portal's setup is the
/// translucent swirl; lamp posts / creatures / signs identify by part shape.
/// </summary>
[Trait("Purpose", "Diagnostic")]
public class Issue131SetupProbeTests
{
private readonly ITestOutputHelper _out;

View file

@ -98,7 +98,8 @@ public class Issue176177FacilityHubFloodReplayTests
}
[Fact]
public void ScenarioB_StairDescent_RampCellRetention()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioB_StairDescent_RampCellRetention()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -140,7 +141,8 @@ public class Issue176177FacilityHubFloodReplayTests
}
[Fact]
public void ScenarioC_CorridorSeamGazeSweep_Bistability()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioC_CorridorSeamGazeSweep_Bistability()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -212,9 +214,10 @@ public class Issue176177FacilityHubFloodReplayTests
/// #177(a)/(c) at the stair transit.
/// </summary>
[Theory]
[Trait("Purpose", "Diagnostic")]
[InlineData(0x016Eu, 0x017Au, 85.00f, -4.3f)] // corridor seam, plane x=85
[InlineData(0x0182u, 0x0183u, 98.333f, -7.3f)] // stair-bottom transit, plane x=98.33
public void ScenarioE_RootLagWindow_ForwardChainRetention(
public void Diagnostic_ScenarioE_RootLagWindow_ForwardChainRetention(
uint rootLow, uint forwardLow, float planeX, float eyeZ)
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
@ -239,7 +242,8 @@ public class Issue176177FacilityHubFloodReplayTests
}
[Fact]
public void ScenarioD_CorridorWalk_PerStepChurn()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioD_CorridorWalk_PerStepChurn()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }

View file

@ -105,7 +105,8 @@ public class Issue177StairDescentCameraFloodTests
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
public void StairCellComposition_ShellVsStatics()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_StairCellComposition_ShellVsStatics()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -166,7 +167,8 @@ public class Issue177StairDescentCameraFloodTests
/// locate the knife-edge and which cell flips.
/// </summary>
[Fact]
public void RealStaircase_FineYawZoomSweep_FindKnifeEdge()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_RealStaircase_FineYawZoomSweep_FindKnifeEdge()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -228,7 +230,8 @@ public class Issue177StairDescentCameraFloodTests
/// caps well below retail's 26, our flood truncates the spiral (the vanish).
/// </summary>
[Fact]
public void FloodDepthFrom015E_VsRetail26()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_FloodDepthFrom015E_VsRetail26()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -349,7 +352,8 @@ public class Issue177StairDescentCameraFloodTests
/// contact leaving the eye on the surface vs free-space boom geometry.
/// </summary>
[Fact]
public void StaircaseSweep_EyeClearanceFromCeilingPortal()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_StaircaseSweep_EyeClearanceFromCeilingPortal()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -392,10 +396,11 @@ public class Issue177StairDescentCameraFloodTests
}
[Theory]
[Trait("Purpose", "Diagnostic")]
// spot label, player x, player cell, description
[InlineData(90.0f, 0x0178u, "TOP corridor 0178 (approach view)")]
[InlineData(100.0f, 0x0183u, "BOTTOM room 0183 (look-back view)")]
public void ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label)
public void Diagnostic_ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -451,7 +456,8 @@ public class Issue177StairDescentCameraFloodTests
}
[Fact]
public void Descent_RealCameraSweep_StairCellRetention()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_Descent_RealCameraSweep_StairCellRetention()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }

View file

@ -56,6 +56,7 @@ public class Issue181VisFlapReplayTests
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_FlappingCellViewRegion_SliverOrLarge()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();

View file

@ -32,6 +32,7 @@ namespace AcDream.App.Tests.Rendering;
/// numbers into hard pins.
/// </summary>
[Collection(CameraDiagnosticsCollection.Name)]
[Trait("Purpose", "Diagnostic")]
public class Issue181WallPressEquilibriumTests
{
private const uint FacilityHubLandblock = 0x8A020000u;

View file

@ -14,6 +14,7 @@ namespace AcDream.App.Tests.UI.Layout;
/// identify the two FA slots definitively: 0x1000026F (fellowship name field)
/// and 0x10000263 (swear-allegiance button). Env-gated like the other probes.
/// </summary>
[Trait("Purpose", "Diagnostic")]
public sealed class FaPanelSlotProbeTests
{
[Fact]

View file

@ -25,6 +25,7 @@ namespace AcDream.App.Tests.UI.Layout;
/// throwaway.
/// </para>
/// </summary>
[Trait("Purpose", "Diagnostic")]
public sealed class MapHousePanelSlotProbeTests
{
private const uint HostLayoutId = 0x2100006Eu;

View file

@ -19,6 +19,7 @@ namespace AcDream.App.Tests.UI.Layout;
public sealed class OptionsPanelLiveMountProbeTests
{
[Fact]
[Trait("Purpose", "Diagnostic")]
public void ProbeLiveMountShapes()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
@ -314,6 +315,7 @@ public sealed class OptionsPanelLiveMountProbeTests
/// FIXED 6-row + scrollbar popup in retail, user-gated during the vendor
/// campaign) as the contrast control.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void ProbeMenuPopupSizingAndTextStyle()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
@ -479,6 +481,7 @@ public sealed class OptionsPanelLiveMountProbeTests
/// to resolve in table 0x23000003 — sweep the plausible tables and key
/// spellings against the live DAT to find their real home.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void ProbeFilterLabelHome()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")

View file

@ -18,6 +18,7 @@ namespace AcDream.App.Tests.UI.Layout;
public sealed class PowerbarLayoutProbeTests
{
[Fact]
[Trait("Purpose", "Diagnostic")]
public void ProbePowerbarAuthoredStrings()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
@ -75,6 +76,7 @@ public sealed class PowerbarLayoutProbeTests
/// gmSecureTradeUI's element tree (lane A gave element ids 0x10000085-8B
/// but not the layout id) and dump it.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void ProbeSecureTradeLayout()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
@ -107,6 +109,7 @@ public sealed class PowerbarLayoutProbeTests
/// ID_SecureTrade_TotalItemsLabel's fragments + variable hashes so the
/// count text can use ResolveTemplate if token-free (AD-95).</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void ProbeTotalItemsTemplate()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")

View file

@ -75,6 +75,7 @@ namespace AcDream.App.Tests.UI;
/// <c>States</c> dictionary was not walked) remain open.
/// </para>
/// </summary>
[Trait("Purpose", "Diagnostic")]
public sealed class SpewBoxLayoutDumpDiagnostic
{
private readonly ITestOutputHelper _out;

View file

@ -127,7 +127,8 @@ public sealed class EnvCellSoundEmitterInventoryTests
}
[Fact]
public void PortalDat_SetupsWithAmbientSlotSoundTables()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_PortalDat_SetupsWithAmbientSlotSoundTables()
{
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null)
@ -184,7 +185,8 @@ public sealed class EnvCellSoundEmitterInventoryTests
}
[Fact]
public void SoundTables_WithAmbientSlots_ExistForWireBinding()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_SoundTables_WithAmbientSlots_ExistForWireBinding()
{
string? datDir = ConformanceDats.ResolveDatDir();
if (datDir is null)

View file

@ -21,6 +21,7 @@ public class CottageDoorwayCharacterizationTests
public CottageDoorwayCharacterizationTests(ITestOutputHelper output) => _out = output;
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Characterize_CottageNeighborhood_PrintStructure()
{
var datDir = ConformanceDats.ResolveDatDir();
@ -65,6 +66,7 @@ public class CottageDoorwayCharacterizationTests
public const uint OutdoorLandcell0031 = 0xA9B40031u;
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Characterize_Doorway_FindInteriorPoints()
{
var datDir = ConformanceDats.ResolveDatDir();

View file

@ -16,6 +16,7 @@ namespace AcDream.Core.Tests.Conformance;
/// landblock have a LandBlock (0xXXYYFFFF) terrain record at all, or only
/// LandBlockInfo + EnvCells? Output-only — no assertions.
/// </summary>
[Trait("Purpose", "Diagnostic")]
public sealed class DungeonLandblockDatProbeTests
{
private readonly ITestOutputHelper _out;

View file

@ -20,6 +20,7 @@ namespace AcDream.Core.Tests.Conformance;
/// these falloffs ARE what retail reads (modulo any load-time transform, settled
/// separately in the decomp). Output-only — no assertions; read the log.
/// </summary>
[Trait("Purpose", "Diagnostic")]
public sealed class HoltburgTorchFalloffProbeTests
{
private readonly ITestOutputHelper _out;

View file

@ -70,6 +70,7 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// vertical door-sized quads.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void DumpHoltburgBuildings_OrphanGeometry()
{
var datDir = ConformanceDats.ResolveDatDir();
@ -191,7 +192,8 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// must move.
/// </summary>
[Fact]
public void ReplicateProductionEmission_OnPortalFills()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ReplicateProductionEmission_OnPortalFills()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
@ -237,6 +239,7 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// which is exactly why retail shows doors but not the ramp.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void DumpPortalFillSurfaceTypes()
{
var datDir = ConformanceDats.ResolveDatDir();
@ -282,6 +285,7 @@ public sealed class Issue113DoorVanishDiagnosticTests
/// the phantom geometry, for type-model comparison against the door.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void DumpControls_HallAndCottage()
{
var datDir = ConformanceDats.ResolveDatDir();

View file

@ -287,6 +287,7 @@ public sealed class Issue113PhantomStairsDumpTests
/// sphere STRADDLES this plane (|dist| &lt; radius + 0.0002, pc:310090-310120).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Dump_Cell104_ExteriorPortalPlane_Vs_GapPoint()
{
var datDir = ConformanceDats.ResolveDatDir();
@ -559,6 +560,7 @@ public sealed class Issue113PhantomStairsDumpTests
/// printed top→bottom DESCENDING (north up).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void DumpAAB3_Watchtower_TopDownMap()
{
var datDir = ConformanceDats.ResolveDatDir();
@ -679,6 +681,7 @@ public sealed class Issue113PhantomStairsDumpTests
/// retail skips and our building-mesh extraction mishandles?
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void DumpHallModel_PolyFlagHistogram()
{
var datDir = ConformanceDats.ResolveDatDir();

View file

@ -13,6 +13,7 @@ namespace AcDream.Core.Tests.Conformance;
/// FindCellList result — so the P0→P1 divergence is precisely characterized
/// (real pick-criterion divergence vs a test/sphere artifact).
/// </summary>
[Trait("Purpose", "Diagnostic")]
public class ThresholdDivergenceDiagnosticTests
{
private readonly ITestOutputHelper _out;

View file

@ -129,6 +129,7 @@ public class CameraCornerSealReplayTests
/// shadow-query path is the half that failed).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_DispatchTrace_LeakPath_vs_Controls()
{
var datDir = ConformanceDats.ResolveDatDir();

View file

@ -237,6 +237,7 @@ public class DoorSetupGfxObjInspectionTests
// computes a different outdoor cell than 0xA9B40029
// Findings drive the fix direction.
[Fact]
[Trait("Purpose", "Diagnostic")]
public void HoltburgCottage_CellPortals_DatInspection()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
@ -271,6 +272,7 @@ public class DoorSetupGfxObjInspectionTests
/// building wall sub-piece) lives at that XY.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void HoltburgLandblockStatics_DatInspection()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")

View file

@ -280,6 +280,7 @@ public class Issue137CorridorSeamInspectionTests
/// hit point + normal.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void CorridorSeam_FindPolygonMatchingLiveHit()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")

View file

@ -33,6 +33,7 @@ public class Issue186ConnectorCellGeometryInspectionTests
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Dump_ConnectorCells_ShellAndCollision()
{
var datDir = DatDir();

View file

@ -25,6 +25,7 @@ namespace AcDream.Core.Tests.Physics;
/// wired). Kept as a reusable decoder for any future "why doesn't this
/// animate" question — swap the MotionTableId.
/// </summary>
[Trait("Purpose", "Diagnostic")]
public class Issue188FadingDoorMotionTableInspectionTests
{
private readonly ITestOutputHelper _out;

View file

@ -244,7 +244,8 @@ public class Issue176177DungeonSeamInspectionTests
/// light exposed it.
/// </summary>
[Fact]
public void CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs()
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -470,7 +471,8 @@ public class Issue176177DungeonSeamInspectionTests
[InlineData(0x8A02016Eu)] // corridor cells — the striped-floor screenshot area
[InlineData(0x8A02017Au)]
[InlineData(0x8A020165u)]
public void UnderHall_DrawnPolys_SurfaceColors(uint cellId)
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_UnderHall_DrawnPolys_SurfaceColors(uint cellId)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
@ -635,6 +637,7 @@ public class Issue176177DungeonSeamInspectionTests
[Theory]
[InlineData(0x8A02015Eu)] // #176 repro corridor
[InlineData(0x8A02016Eu)] // corridor with floor-portals
[Trait("Purpose", "Diagnostic")]
public void CellVertexNormals_SmoothOrFaceted_Dump(uint cellId)
{
var datDir = ResolveDatDir();

View file

@ -50,6 +50,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
[InlineData(0x00070146u)]
[InlineData(0x00070155u)]
[InlineData(0x00070157u)]
[Trait("Purpose", "Diagnostic")]
public void StaticObjects_SetupLightsCount_Dump(uint cellId)
{
var datDir = ResolveDatDir();

View file

@ -0,0 +1,374 @@
<#
.SYNOPSIS
Produces a syntax-based inventory of every attributed C# test method.
.DESCRIPTION
Parses tracked C# files under tests/ with the Roslyn assemblies bundled in
the active .NET SDK. The report is evidence for the R3 test-truth audit: it
records test attributes and traits, static skips, empty-return sites,
assertion/throw signals (including same-file helper calls), diagnostic
output signals, wall-clock waits, environment-variable dependencies, and
source-text reads.
This is a candidate generator, not a semantic proof. In particular, an
output-only candidate can call a failure-capable helper declared in another
file or base class. Every removal or lane change still requires inspection.
#>
[CmdletBinding()]
param(
[string]$OutputPath = 'artifacts/test-audit/test-inventory.json'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$repoRoot = [IO.Path]::GetFullPath((Split-Path -Parent $PSScriptRoot))
$resolvedOutput = if ([IO.Path]::IsPathRooted($OutputPath)) {
[IO.Path]::GetFullPath($OutputPath)
} else {
[IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath))
}
$dotnetCommand = Get-Command dotnet -ErrorAction Stop
$dotnetRoot = Split-Path -Parent $dotnetCommand.Source
$sdkVersion = (& dotnet --version).Trim()
$roslynDirectory = Join-Path $dotnetRoot "sdk/$sdkVersion/Roslyn/bincore"
$loadedRoslyn = @([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {
$_.GetName().Name -in @('Microsoft.CodeAnalysis', 'Microsoft.CodeAnalysis.CSharp')
})
if ($loadedRoslyn.Count -lt 2) {
foreach ($assemblyName in 'Microsoft.CodeAnalysis.dll', 'Microsoft.CodeAnalysis.CSharp.dll') {
$assemblyPath = Join-Path $roslynDirectory $assemblyName
if (-not (Test-Path -LiteralPath $assemblyPath -PathType Leaf)) {
throw "Roslyn assembly not found at '$assemblyPath'."
}
Add-Type -Path $assemblyPath
}
}
$roslynVersion = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree].Assembly.GetName().Version.ToString()
function Get-ShortAttributeName {
param([Parameter(Mandatory)]$Attribute)
$name = $Attribute.Name.ToString().Split('.')[-1]
if ($name.EndsWith('Attribute', [StringComparison]::Ordinal)) {
$name = $name.Substring(0, $name.Length - 'Attribute'.Length)
}
return $name
}
function Get-NodeLine {
param(
[Parameter(Mandatory)]$Tree,
[Parameter(Mandatory)]$Node
)
return $Tree.GetLineSpan($Node.Span).StartLinePosition.Line + 1
}
function Get-ContainingClassName {
param([Parameter(Mandatory)]$Method)
$typeNode = Get-ContainingTypeNode $Method
if ($null -ne $typeNode) {
return $typeNode.Identifier.Text
}
return '<global>'
}
function Get-ContainingTypeNode {
param([Parameter(Mandatory)]$Method)
$cursor = $Method.Parent
while ($null -ne $cursor) {
if ($cursor.GetType().Name -in @(
'ClassDeclarationSyntax',
'RecordDeclarationSyntax',
'StructDeclarationSyntax')) {
return $cursor
}
$cursor = $cursor.Parent
}
return $null
}
function Get-ReturnGateKind {
param([Parameter(Mandatory)][string]$Condition)
if ($Condition -match '(?i)(GetEnvironmentVariable|ACDREAM_)') {
return 'OptIn'
}
if ($Condition -match '(?i)(OperatingSystem\.)') {
return 'Platform'
}
if ($Condition -match '(?i)(datDir|datDirectory|Directory\.Exists|File\.Exists|TryOpen|TryBuildScenario|steps is null|csvPath|\.pak)') {
return 'ExternalAsset'
}
return $null
}
function Get-InvocationName {
param([Parameter(Mandatory)]$Invocation)
$expression = $Invocation.Expression
if ($expression.GetType().Name -eq 'MemberAccessExpressionSyntax') {
return $expression.Name.Identifier.Text
}
if ($expression.GetType().Name -eq 'IdentifierNameSyntax') {
return $expression.Identifier.Text
}
if ($expression.GetType().Name -eq 'GenericNameSyntax') {
return $expression.Identifier.Text
}
return $expression.ToString()
}
function Test-DirectFailureSignal {
param([Parameter(Mandatory)]$Method)
if (@($Method.DescendantNodes() | Where-Object {
$_.GetType().Name -in @('ThrowStatementSyntax', 'ThrowExpressionSyntax')
}).Count -gt 0) {
return $true
}
foreach ($invocation in @($Method.DescendantNodes() | Where-Object {
$_.GetType().Name -eq 'InvocationExpressionSyntax'
})) {
$expression = $invocation.Expression.ToString()
if ($expression -match '(^|\.)Assert(\.|$)' -or
$expression -match '(^|\.)(Should|Shouldly)(\.|$)' -or
$expression -match '(^|\.)Verify($|\.)') {
return $true
}
}
return $false
}
function Test-DirectOutputSignal {
param([Parameter(Mandatory)]$Method)
foreach ($invocation in @($Method.DescendantNodes() | Where-Object {
$_.GetType().Name -eq 'InvocationExpressionSyntax'
})) {
$expression = $invocation.Expression.ToString()
if ($expression -match '(^|\.)(_?out|output|Console|Debug|Trace)\.(Write|WriteLine)$' -or
$expression -match '(^|\.)(WriteDiagnostic|Dump|DumpSteps|Print)(\.|$)') {
return $true
}
}
return $false
}
function Test-RecursiveSignal {
param(
[Parameter(Mandatory)][string]$MethodKey,
[Parameter(Mandatory)][hashtable]$MethodMap,
[Parameter(Mandatory)][hashtable]$DirectSignalMap,
[Parameter(Mandatory)][hashtable]$CallMap,
[Parameter(Mandatory)][hashtable]$Visited
)
if ($Visited.ContainsKey($MethodKey)) {
return $false
}
$Visited[$MethodKey] = $true
if ($DirectSignalMap[$MethodKey]) {
return $true
}
foreach ($calledName in @($CallMap[$MethodKey])) {
foreach ($candidateKey in @($MethodMap.Keys | Where-Object {
$_.EndsWith("::$calledName", [StringComparison]::Ordinal)
})) {
if (Test-RecursiveSignal $candidateKey $MethodMap $DirectSignalMap $CallMap $Visited) {
return $true
}
}
}
return $false
}
$trackedFiles = @(& git -C $repoRoot ls-files tests | Where-Object {
$_.EndsWith('.cs', [StringComparison]::OrdinalIgnoreCase)
})
if ($LASTEXITCODE -ne 0) {
throw 'git ls-files failed while enumerating test sources.'
}
$records = [Collections.Generic.List[object]]::new()
$parsedFileCount = 0
foreach ($relativePath in $trackedFiles) {
$absolutePath = Join-Path $repoRoot $relativePath
if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) {
continue
}
$source = [IO.File]::ReadAllText($absolutePath)
$tree = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($source)
$root = $tree.GetRoot()
$parsedFileCount++
$allMethods = @($root.DescendantNodes() | Where-Object {
$_.GetType().Name -eq 'MethodDeclarationSyntax'
})
$methodMap = @{}
$failureMap = @{}
$outputMap = @{}
$callMap = @{}
foreach ($method in $allMethods) {
$className = Get-ContainingClassName $method
$key = "$className::$($method.Identifier.Text)"
$methodMap[$key] = $method
$failureMap[$key] = Test-DirectFailureSignal $method
$outputMap[$key] = Test-DirectOutputSignal $method
$callMap[$key] = @($method.DescendantNodes() |
Where-Object { $_.GetType().Name -eq 'InvocationExpressionSyntax' } |
ForEach-Object { Get-InvocationName $_ } |
Sort-Object -Unique)
}
foreach ($method in $allMethods) {
$methodAttributes = @($method.AttributeLists | ForEach-Object { $_.Attributes })
$attributeNames = @($methodAttributes | ForEach-Object {
Get-ShortAttributeName $_
})
$testAttributes = @($attributeNames | Where-Object {
$_ -in @('Fact', 'Theory', 'AvaloniaFact', 'InstalledDatFact')
})
if ($testAttributes.Count -eq 0) {
continue
}
$className = Get-ContainingClassName $method
$key = "$className::$($method.Identifier.Text)"
$hasFailureSignal = Test-RecursiveSignal `
$key $methodMap $failureMap $callMap @{}
$hasOutputSignal = Test-RecursiveSignal `
$key $methodMap $outputMap $callMap @{}
$emptyReturns = [Collections.Generic.List[object]]::new()
foreach ($returnNode in @($method.DescendantNodes() | Where-Object {
$_.GetType().Name -eq 'ReturnStatementSyntax' -and
$null -eq $_.Expression
})) {
$condition = '<unconditional>'
$cursor = $returnNode.Parent
while ($null -ne $cursor -and $cursor -ne $method) {
if ($cursor.GetType().Name -eq 'IfStatementSyntax') {
$condition = $cursor.Condition.ToString()
break
}
$cursor = $cursor.Parent
}
$emptyReturns.Add([ordered]@{
Line = Get-NodeLine $tree $returnNode
Condition = $condition
GateKind = Get-ReturnGateKind $condition
})
}
$traits = [Collections.Generic.List[string]]::new()
$staticSkip = $null
$typeNode = Get-ContainingTypeNode $method
$traitAttributes = @($methodAttributes)
if ($null -ne $typeNode) {
$traitAttributes += @($typeNode.AttributeLists | ForEach-Object {
$_.Attributes
})
}
foreach ($attribute in $traitAttributes) {
$attributeName = Get-ShortAttributeName $attribute
$arguments = if ($null -eq $attribute.ArgumentList) {
''
} else {
$attribute.ArgumentList.ToString()
}
if ($attributeName -eq 'Trait') {
$traits.Add($arguments.Trim('(', ')').Replace('"', ''))
}
}
foreach ($attribute in $methodAttributes) {
$arguments = if ($null -eq $attribute.ArgumentList) {
''
} else {
$attribute.ArgumentList.ToString()
}
if ($arguments -match '(^|[,\s])Skip\s*=') {
$staticSkip = $arguments
}
}
$bodyText = $method.ToString()
$environmentVariables = @([regex]::Matches(
$bodyText,
'GetEnvironmentVariable\s*\(\s*"([A-Za-z0-9_]+)"') |
ForEach-Object { $_.Groups[1].Value } |
Sort-Object -Unique)
$records.Add([ordered]@{
Path = $relativePath.Replace('\', '/')
Line = Get-NodeLine $tree $method
Class = $className
Method = $method.Identifier.Text
Attributes = $testAttributes
Traits = @($traits)
StaticSkip = $staticSkip
EmptyReturns = @($emptyReturns)
PrerequisiteReturnCandidate = @($emptyReturns | Where-Object {
$null -ne $_.GateKind
}).Count -gt 0
HasFailureSignal = $hasFailureSignal
HasOutputSignal = $hasOutputSignal
OutputOnlyCandidate = $hasOutputSignal -and -not $hasFailureSignal
EnvironmentVariables = $environmentVariables
HasThreadSleep = $bodyText -match '\bThread\.Sleep\s*\('
HasTaskDelay = $bodyText -match '\bTask\.Delay\s*\('
ReadsSourceText = $bodyText -match '(ReadAllText|ReadAllLines)\s*\(' -and
$bodyText -match '\.cs'
})
}
}
$orderedRecords = @($records | Sort-Object Path, Line, Method)
$workingTreeStatus = @(& git -C $repoRoot status --short --untracked-files=no)
$summary = [ordered]@{
GeneratedUtc = [DateTime]::UtcNow.ToString('O')
RepositoryCommit = (& git -C $repoRoot rev-parse HEAD).Trim()
WorkingTreeDirty = $workingTreeStatus.Count -gt 0
DotnetSdkVersion = $sdkVersion
RoslynAssemblyVersion = $roslynVersion
TrackedCSharpFiles = $trackedFiles.Count
ParsedCSharpFiles = $parsedFileCount
AttributedTestMethods = $orderedRecords.Count
StaticSkipMethods = @($orderedRecords | Where-Object { $null -ne $_.StaticSkip }).Count
EmptyReturnMethods = @($orderedRecords | Where-Object { $_.EmptyReturns.Count -gt 0 }).Count
EmptyReturnSites = @($orderedRecords | ForEach-Object { $_.EmptyReturns }).Count
PrerequisiteReturnCandidates = @($orderedRecords | Where-Object {
$_.PrerequisiteReturnCandidate
}).Count
OutputOnlyCandidates = @($orderedRecords | Where-Object { $_.OutputOnlyCandidate }).Count
DiagnosticMethods = @($orderedRecords | Where-Object {
$_.Traits -contains 'Purpose, Diagnostic'
}).Count
KnownFailureMethods = @($orderedRecords | Where-Object {
$_.Traits -contains 'Status, KnownFailure'
}).Count
ThreadSleepMethods = @($orderedRecords | Where-Object { $_.HasThreadSleep }).Count
TaskDelayMethods = @($orderedRecords | Where-Object { $_.HasTaskDelay }).Count
DirectEnvironmentVariableMethods = @($orderedRecords | Where-Object {
$_.EnvironmentVariables.Count -gt 0
}).Count
SourceTextReadMethods = @($orderedRecords | Where-Object { $_.ReadsSourceText }).Count
}
$report = [ordered]@{
Summary = $summary
Tests = $orderedRecords
}
$outputDirectory = Split-Path -Parent $resolvedOutput
[IO.Directory]::CreateDirectory($outputDirectory) | Out-Null
$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $resolvedOutput -Encoding utf8
$summary | Format-List
Write-Output "Inventory: $resolvedOutput"