test: replace input and physics source freezes

This commit is contained in:
Erik 2026-08-18 15:31:57 +02:00
parent 0ad2ee1cdf
commit caa5eb8b2b
6 changed files with 313 additions and 238 deletions

View file

@ -4,6 +4,7 @@ using System.Reflection.Emit;
namespace AcDream.App.Tests.Architecture;
internal readonly record struct CompiledCall(int Offset, MethodBase Target);
internal readonly record struct CompiledInstruction(int Offset, OpCode OpCode);
/// <summary>
/// Reads compiled call/new-object edges from a method body. Architecture tests
@ -70,6 +71,70 @@ internal static class CompiledCallGraph
return literals;
}
/// <summary>
/// Reads the compiled instruction sequence without exposing operands. This
/// is sufficient for architecture tests that need to distinguish an
/// immediate return from fall-through without freezing source formatting.
/// </summary>
public static IReadOnlyList<CompiledInstruction> ReadInstructions(
MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
var instructions = new List<CompiledInstruction>();
for (int cursor = 0; cursor < il.Length;)
{
int instructionOffset = cursor;
OpCode opCode = ReadOpCode(il, ref cursor);
instructions.Add(new CompiledInstruction(instructionOffset, opCode));
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return instructions;
}
/// <summary>
/// Reads types named by compiled type operands such as casts, boxing, and
/// <c>isinst</c>. This lets tests retain an exact type boundary without
/// depending on the source expression used to spell it.
/// </summary>
public static IReadOnlyList<Type> ReadTypeReferences(MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
Type[]? declaringArguments = method.DeclaringType?.IsGenericType == true
? method.DeclaringType.GetGenericArguments()
: null;
Type[]? methodArguments = method.IsGenericMethod
? method.GetGenericArguments()
: null;
var types = new List<Type>();
for (int cursor = 0; cursor < il.Length;)
{
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode.OperandType == OperandType.InlineType)
{
int token = BitConverter.ToInt32(il, cursor);
Type? type = method.Module.ResolveType(
token,
declaringArguments,
methodArguments);
if (type is not null)
types.Add(type);
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return types;
}
public static int IndexOf(
IReadOnlyList<CompiledCall> calls,
Type declaringType,

View file

@ -1,10 +1,18 @@
using System.Reflection;
using AcDream.App.Input;
using AcDream.App.Tests.Architecture;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
namespace AcDream.App.Tests.Input;
/// <summary>
/// C3c-F2 (2026-08-02): source pin for the production player-mode auto-entry
/// precondition. <c>PlayerModeAutoEntry</c> is a ONE-SHOT that disarms before
/// invoking its callback, and the production callback completes the world
/// reveal, so an attempt made before the Runtime first-entry conductor has
/// C3c-F2 (2026-08-02): compiled architecture check for the production
/// player-mode auto-entry precondition. <c>PlayerModeAutoEntry</c> is a
/// ONE-SHOT that disarms before invoking its callback, and the production
/// callback completes the world reveal, so an attempt made before the Runtime
/// first-entry conductor has
/// committed permanently seals the reveal with the player never in world —
/// the second link of the connected-gate login wedge
/// (logs/connected-world-gate-20260802-130455: one "not committed yet" line,
@ -14,45 +22,61 @@ namespace AcDream.App.Tests.Input;
/// The guard's own one-shot/latch behavior is covered behaviorally by
/// AcDream.Core.Tests.Input.AutoEnterPlayerModeTests; only the production
/// context's dependency graph (a ~15-dependency PlayerModeController) has no
/// focused harness, hence the source pin.
/// focused harness, hence the metadata check.
/// </summary>
public sealed class C3cF2AutoEntryWiringTests
{
[Fact]
public void ProductionAutoEntryRequiresTheRuntimePublishedController()
{
string source = ReadSource("Input", "PlayerModeAutoEntry.cs");
MethodInfo readiness = typeof(LivePlayerModeAutoEntryContext)
.GetProperty(
"IsPlayerControllerReady",
BindingFlags.Instance | BindingFlags.Public)!
.GetMethod!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(readiness);
Assert.DoesNotContain(
"public bool IsPlayerControllerReady => true;",
source,
StringComparison.Ordinal);
Assert.Contains(
"IsRuntimePublished: true",
source,
StringComparison.Ordinal);
calls,
call => call.Target.DeclaringType == typeof(PlayerMovementController)
&& call.Target.Name == "get_IsRuntimePublished");
Assert.Contains(
"record.PhysicsHost is EntityPhysicsHost",
source,
StringComparison.Ordinal);
calls,
call => call.Target.DeclaringType == typeof(LiveEntityRuntime)
&& call.Target.Name == nameof(LiveEntityRuntime.TryGetRecord));
Assert.Contains(
calls,
call => call.Target.DeclaringType == typeof(LiveEntityRecord)
&& call.Target.Name == "get_PhysicsHost");
Assert.Contains(
typeof(EntityPhysicsHost),
CompiledCallGraph.ReadTypeReferences(readiness));
}
[Fact]
public void PlayerPresentationAttach_DrainsMatchedAnimationsBeforeStartupMotionSuffix()
{
string source = ReadSource("Input", "PlayerModeController.cs");
int attach = source.IndexOf(
"controller.Motion.DefaultSink =",
StringComparison.Ordinal);
int animationDrain = source.IndexOf(
"sequencer.Manager.HandleEnterWorld();",
attach,
StringComparison.Ordinal);
int unmatchedMotionDrain = source.IndexOf(
"controller.Motion.HandleExitWorld();",
animationDrain,
StringComparison.Ordinal);
MethodInfo enter = typeof(PlayerModeController).GetMethod(
"BuildControllerAndCamera",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new MissingMethodException(
typeof(PlayerModeController).FullName,
"BuildControllerAndCamera");
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(enter);
int attach = CompiledCallGraph.IndexOf(
calls,
typeof(MotionInterpreter),
"set_DefaultSink");
int animationDrain = CompiledCallGraph.IndexOf(
calls,
typeof(MotionTableManager),
nameof(MotionTableManager.HandleEnterWorld),
attach + 1);
int unmatchedMotionDrain = CompiledCallGraph.IndexOf(
calls,
typeof(MotionInterpreter),
nameof(MotionInterpreter.HandleExitWorld),
animationDrain + 1);
Assert.True(attach >= 0, "The player animation sink was not attached.");
Assert.True(
@ -62,24 +86,4 @@ public sealed class C3cF2AutoEntryWiringTests
unmatchedMotionDrain > animationDrain,
"Only the unmatched pre-attach interpreter suffix may drain last.");
}
private static string ReadSource(params string[] relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
{
return File.ReadAllText(Path.Combine(
directory.FullName,
"src",
"AcDream.App",
Path.Combine(relativePath)));
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -1,13 +1,15 @@
using System.Text.RegularExpressions;
using AcDream.App.Tests.Architecture;
using AcDream.App.Physics;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Physics;
/// <summary>
/// C3c-F1 (2026-08-02): source pins for the App-side halves of the
/// movement-owner application seams whose production graphs have no
/// C3c-F1 (2026-08-02): compiled architecture checks for the App-side halves
/// of the movement-owner application seams whose production graphs have no
/// focused harness (the inbound network-update controller's dependency
/// set is composition-only). The Runtime lifecycle matrices carry the
/// behavioral coverage; these pins keep the App wiring routed through the
/// behavioral coverage; these checks keep the App wiring routed through the
/// owner's typed entries instead of the throwing direct mutations that
/// crashed the connected lifecycle gate twice
/// (logs/connected-world-gate-20260802-122749 — SetCharacterSkills;
@ -18,36 +20,17 @@ public sealed class C3cF1ProductionWiringTests
[Fact]
public void LocalPlayerInboundSetState_RoutesThroughTheTypedOwnerEntry()
{
string source = ReadSource(
"Physics",
"LiveEntityNetworkUpdateController.cs");
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.ReadDeclared(
typeof(LiveEntityNetworkUpdateController));
Assert.Single(
Regex.Matches(source, @"ApplyServerPhysicsState\(")
.Cast<Match>());
calls,
call =>
call.Target.DeclaringType == typeof(PlayerMovementController)
&& call.Target.Name == "ApplyServerPhysicsState");
Assert.DoesNotContain(
".ApplyPhysicsState(",
source,
StringComparison.Ordinal);
}
private static string ReadSource(params string[] relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
{
return File.ReadAllText(Path.Combine(
directory.FullName,
"src",
"AcDream.App",
Path.Combine(relativePath)));
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
calls,
call => call.Target.DeclaringType == typeof(PlayerMovementController)
&& call.Target.Name == nameof(PlayerMovementController.ApplyPhysicsState));
}
}

View file

@ -1,4 +1,9 @@
using System.Text.RegularExpressions;
using System.Reflection;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Physics;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Physics;
@ -13,77 +18,98 @@ public sealed class Issue270ProductionWiringTests
// the factory's deleted ApplyMovementStats body into
// LiveMovementStatsApplier, which routes through the Runtime
// movement owner's typed seam instead of touching the controller.
string applier = ReadSource("Net", "LiveMovementStatsApplier.cs");
string factory = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
_ = Assert.Single(
typeof(LiveMovementStatsApplier).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(StaminaExhaustionEdgeTracker));
Assert.Contains(
"_staminaExhaustion.Observe(snapshot.CurrentStamina)",
applier,
StringComparison.Ordinal);
IReadOnlyList<CompiledCall> applierCalls =
CompiledCallGraph.ReadDeclared(typeof(LiveMovementStatsApplier));
Assert.Single(
Regex.Matches(
applier,
@"_movement\.ReportExhaustion\(\);")
.Cast<Match>());
Assert.Contains(
"_staminaExhaustion.Reset();",
applier,
StringComparison.Ordinal);
Assert.Contains(
"_movementStats.Reset();",
factory,
StringComparison.Ordinal);
applierCalls,
call =>
call.Target.DeclaringType == typeof(StaminaExhaustionEdgeTracker)
&& call.Target.Name == nameof(StaminaExhaustionEdgeTracker.Observe));
Assert.Single(
applierCalls,
call =>
call.Target.DeclaringType
== typeof(RuntimeLocalPlayerMovementState)
&& call.Target.Name == nameof(
RuntimeLocalPlayerMovementState.ReportExhaustion));
Assert.Single(
applierCalls,
call =>
call.Target.DeclaringType == typeof(StaminaExhaustionEdgeTracker)
&& call.Target.Name == nameof(StaminaExhaustionEdgeTracker.Reset));
IReadOnlyList<CompiledCall> factoryCalls =
CompiledCallGraph.ReadDeclared(typeof(LiveSessionRuntimeFactory));
Assert.Single(
factoryCalls,
call =>
call.Target.DeclaringType == typeof(LiveMovementStatsApplier)
&& call.Target.Name == nameof(LiveMovementStatsApplier.Reset));
// The factory keeps zero direct controller mutations: both stat
// callbacks route through the applier's seam.
Assert.Equal(
2,
Regex.Matches(factory, @"_movementStats\.Apply\(").Count);
factoryCalls.Count(call =>
call.Target.DeclaringType == typeof(LiveMovementStatsApplier)
&& call.Target.Name == nameof(LiveMovementStatsApplier.Apply)));
Assert.DoesNotContain(
"Motion.ReportExhaustion",
factory,
StringComparison.Ordinal);
factoryCalls,
call => call.Target.DeclaringType == typeof(MotionInterpreter)
&& call.Target.Name == nameof(MotionInterpreter.ReportExhaustion));
}
[Fact]
public void RemoteSpawnSettle_IsRetriedAndCoversBothCreationRoutes()
{
string source = ReadSource(
"Physics",
"LiveEntityNetworkUpdateController.cs");
const BindingFlags flags = BindingFlags.Instance
| BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
MethodInfo[] callers = typeof(LiveEntityNetworkUpdateController)
.GetMethods(flags)
.Where(method => method.GetMethodBody() is not null)
.Where(method => CompiledCallGraph.Read(method).Any(call =>
call.Target.DeclaringType
== typeof(LiveEntityNetworkUpdateController)
&& call.Target.Name == "SeedRemoteSpawnPlacement"))
.ToArray();
Assert.Contains(
"if (!remote.Body.InContact)",
source,
StringComparison.Ordinal);
Assert.Equal(2, callers.Length);
Assert.Equal(
3,
Regex.Matches(source, @"SeedRemoteSpawnPlacement\(").Count);
["DispatchRemoteInboundMotion", "OnPosition"],
callers.Select(method => method.Name).Order().ToArray());
MethodInfo retryingInboundRoute = Assert.Single(
callers,
method => method.Name == "DispatchRemoteInboundMotion");
IReadOnlyList<CompiledCall> retryCalls =
CompiledCallGraph.Read(retryingInboundRoute);
int contact = CompiledCallGraph.IndexOf(
retryCalls,
typeof(PhysicsBody),
"get_InContact");
int settle = CompiledCallGraph.IndexOf(
retryCalls,
typeof(LiveEntityNetworkUpdateController),
"SeedRemoteSpawnPlacement");
Assert.True(contact >= 0 && settle > contact);
// C3c-F5: the settle helper moved to Core (SpawnPlacementSettler) so
// the local player's Runtime first-entry activation shares the same
// tested compressed-first-gravity-frame sweep.
MethodInfo seed = typeof(LiveEntityNetworkUpdateController).GetMethod(
"SeedRemoteSpawnPlacement",
flags)
?? throw new MissingMethodException(
typeof(LiveEntityNetworkUpdateController).FullName,
"SeedRemoteSpawnPlacement");
Assert.Contains(
"SpawnPlacementSettler.TrySettle(",
source,
StringComparison.Ordinal);
}
private static string ReadSource(params string[] relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
{
return File.ReadAllText(Path.Combine(
directory.FullName,
"src",
"AcDream.App",
Path.Combine(relativePath)));
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
CompiledCallGraph.Read(seed),
call => call.Target.DeclaringType == typeof(SpawnPlacementSettler)
&& call.Target.Name == nameof(SpawnPlacementSettler.TrySettle));
}
}

View file

@ -1,5 +1,9 @@
using System.Text.RegularExpressions;
using System.Reflection;
using System.Reflection.Emit;
using AcDream.App.Tests.Architecture;
using AcDream.App.Physics;
using AcDream.Core.World;
using AcDream.Runtime.Session;
namespace AcDream.App.Tests.Physics;
@ -57,14 +61,14 @@ public sealed class LiveEntityNetworkBranchRoutingTests
// there: tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs.
/// <summary>
/// R8 review fix (2026-08-03): source pins for the generic-tail
/// R8 review fix (2026-08-03): assembly checks for the generic-tail
/// double-write guard in <c>LiveEntityNetworkUpdateController.OnPosition</c>.
/// A full behavioral fixture is impractical here for the SAME reason
/// <c>C3cF1ProductionWiringTests</c> gives — the controller's dependency
/// set is composition-only (67+ collaborators wired only by
/// <c>SessionPlayerComposition</c>) — so this follows that file's exact
/// established pattern: assert the STRUCTURE of the production source
/// rather than construct the class. The Runtime-level behavioral
/// established pattern: assert the compiled call/return structure rather
/// than construct the class. The Runtime-level behavioral
/// coverage for the seam itself lives in
/// RuntimeAcceptedPositionDriveControllerTests; these pins are what stop
/// this App-layer call site from silently reintroducing the retired
@ -74,122 +78,56 @@ public sealed class LiveEntityNetworkBranchRoutingTests
/// </summary>
public sealed class LiveEntityNetworkUpdateControllerForcePositionWiringTests
{
[Fact]
public void LocalForcePositionTransactionIsNeverCalledFromThisFile()
{
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
// The name may still appear in a comment explaining what
// replaced it (contract §"the deleted LocalForcePositionTransaction");
// what must be gone is any actual call into it.
Assert.DoesNotContain(
"LocalForcePositionTransaction.Apply(",
source,
StringComparison.Ordinal);
}
[Fact]
public void GenericTailWriteIsNeverDuplicatedForTheLocalForcePositionPath()
{
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
// The generic render-tail's WorldEntity write is the ONE
// remaining writer of an accepted Position — it must serve
// remotes only, never a second local-player write alongside the
// Runtime-committed one.
MethodInfo genericTail = typeof(LiveEntityNetworkUpdateController)
.GetMethod(
"TryApplyGenericRemoteRenderPose",
BindingFlags.Static | BindingFlags.NonPublic)
?? throw new MissingMethodException(
typeof(LiveEntityNetworkUpdateController).FullName,
"TryApplyGenericRemoteRenderPose");
Assert.Single(
Regex.Matches(source, @"entity\.SetPosition\(worldPos\);")
.Cast<Match>());
CompiledCallGraph.Read(genericTail),
call => call.Target.DeclaringType == typeof(WorldEntity)
&& call.Target.Name == nameof(WorldEntity.SetPosition));
}
[Fact]
public void CommittedOrDeferredCellReturnsBeforeReachingTheGenericTail()
{
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
// The Committed/DeferredCell branch must still return
// immediately after its two preserved side effects — a missing
// `return` here would fall through into the generic tail below
// and resurrect the double-write.
Assert.Matches(
new Regex(
@"ObserveAcceptedLocalPosition\(\s*"
+ @"update\.Position\.LandblockId\);\s*return;",
RegexOptions.Singleline),
source);
}
MethodInfo onPosition = typeof(LiveEntityNetworkUpdateController)
.GetMethod(
"OnPosition",
BindingFlags.Instance | BindingFlags.Public)
?? throw new MissingMethodException(
typeof(LiveEntityNetworkUpdateController).FullName,
"OnPosition");
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(onPosition);
CompiledCall drive = Assert.Single(calls, call =>
call.Target.DeclaringType
== typeof(RuntimeAcceptedPositionDriveController)
&& call.Target.Name == "TryExecuteAcceptedLocalPosition");
CompiledCall observe = calls.First(call =>
call.Offset > drive.Offset
&& call.Target.Name == "ObserveAcceptedLocalPosition");
IReadOnlyList<CompiledInstruction> instructions =
CompiledCallGraph.ReadInstructions(onPosition);
int observeInstruction = instructions
.Select((instruction, index) => (instruction, index))
.Single(pair => pair.instruction.Offset == observe.Offset)
.index;
/// <summary>
/// AP-140 (retired 2026-08-04): the accepted-Position routing gate
/// for the free-flight/landing decision must select the hard snap on
/// retail's CONTACT predicate
/// (<c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its
/// whole body on <c>transient_state &amp; 1</c> @0x00555D52, and bit 0
/// is <c>CONTACT_TS</c>), not on the client <c>Airborne</c> flag,
/// which is <c>!Body.OnWalkable</c> — WALKABILITY, a strictly wider
/// set that also captures a remote sliding on a steep face.
///
/// <para>
/// C4 route 4b-3's OnPosition collapse (2026-08-04) dissolved
/// <c>OnPosition</c>'s former standalone player-remote LANDING
/// TRANSITION block — which used to carry its own
/// <c>if (!rmState.Body.InContact)</c> copy of this gate — into
/// <c>ApplyRemoteContactRouting</c>'s free-flight carve-out, now the
/// ONE site (for every guid) that decides this. The gate itself did
/// not move in spirit, only in address: this pin follows it there.
/// </para>
///
/// <para>
/// A source pin rather than a behavioural fixture for the reason this
/// class already documents: the controller's dependency set is
/// composition-only. The gate inside <c>ApplyRemoteContactRouting</c>
/// — a static method, so reachable — IS covered behaviourally, in
/// <c>LiveEntityNetworkRemoteSteadyStateIntegrationTests</c>. Restore
/// <c>if (rmState.Airborne)</c> or <c>if (remote.Airborne)</c> here
/// and this test fails.
/// </para>
/// </summary>
[Fact]
public void PlayerRemoteLandingSnapSelectsOnContactNotOnWalkability()
{
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
Assert.Contains(
"if (!remote.Body.InContact)",
source,
StringComparison.Ordinal);
// `rmState.Airborne` survives as a WRITE target and in prose (the
// block's own comment explains why it is deliberately not cleared
// there); what must never come back is reading it as the gate.
Assert.DoesNotContain(
"if (rmState.Airborne)",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"if (remote.Airborne)",
source,
StringComparison.Ordinal);
}
private static string ReadSource(string fileName)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
{
return File.ReadAllText(Path.Combine(
directory.FullName,
"src",
"AcDream.App",
"Physics",
fileName));
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
Assert.Equal(OpCodes.Ret, instructions[observeInstruction + 1].OpCode);
}
}
}