test: replace input and physics source freezes
This commit is contained in:
parent
0ad2ee1cdf
commit
caa5eb8b2b
6 changed files with 313 additions and 238 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 & 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue