From fbc09186f367aad870b74774369ae2d9f9d4d92a Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Sun, 29 Mar 2026 12:43:57 +0800 Subject: [PATCH] process TODO.md step 10 --- Assets/Scripts/MovementComponent.cs | 30 +++ Assets/Scripts/NetworkManager.cs | 5 +- .../Network/ClientGameplayFlowTests.cs | 161 ++++++++++++ .../Network/ClientGameplayFlowTests.cs.meta | 11 + .../Network/GameplayFlowRoundTripTests.cs | 139 +++++++++++ .../GameplayFlowRoundTripTests.cs.meta | 11 + .../Network/GameplayFlowTestSupport.cs | 230 ++++++++++++++++++ .../Network/GameplayFlowTestSupport.cs.meta | 11 + TODO.md | 40 +-- .../.openspec.yaml | 2 + .../design.md | 53 ++++ .../proposal.md | 22 ++ .../gameplay-flow-regression-coverage/spec.md | 31 +++ .../tasks.md | 17 ++ .../.openspec.yaml | 2 + .../design.md | 40 +++ .../proposal.md | 21 ++ .../specs/build-test-verification/spec.md | 22 ++ .../tasks.md | 21 ++ .../specs/build-test-verification/spec.md | 28 +++ .../gameplay-flow-regression-coverage/spec.md | 35 +++ 21 files changed, 913 insertions(+), 19 deletions(-) create mode 100644 Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs create mode 100644 Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs.meta create mode 100644 Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs create mode 100644 Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs.meta create mode 100644 Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs create mode 100644 Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs.meta create mode 100644 openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/.openspec.yaml create mode 100644 openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/design.md create mode 100644 openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/proposal.md create mode 100644 openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/specs/gameplay-flow-regression-coverage/spec.md create mode 100644 openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/tasks.md create mode 100644 openspec/changes/archive/2026-03-29-re-verify-build-and-test/.openspec.yaml create mode 100644 openspec/changes/archive/2026-03-29-re-verify-build-and-test/design.md create mode 100644 openspec/changes/archive/2026-03-29-re-verify-build-and-test/proposal.md create mode 100644 openspec/changes/archive/2026-03-29-re-verify-build-and-test/specs/build-test-verification/spec.md create mode 100644 openspec/changes/archive/2026-03-29-re-verify-build-and-test/tasks.md create mode 100644 openspec/specs/build-test-verification/spec.md create mode 100644 openspec/specs/gameplay-flow-regression-coverage/spec.md diff --git a/Assets/Scripts/MovementComponent.cs b/Assets/Scripts/MovementComponent.cs index ffc3a3a..dc05f1a 100644 --- a/Assets/Scripts/MovementComponent.cs +++ b/Assets/Scripts/MovementComponent.cs @@ -50,6 +50,36 @@ public static class ClientGameplayInputFlow TargetId = targetId ?? string.Empty }; } + + public static void SendShootInput( + MessageManager messageManager, + string playerId, + long tick, + Vector3 aimDirection, + string targetId = "") + { + if (messageManager == null) + { + throw new System.ArgumentNullException(nameof(messageManager)); + } + + SendShootInput(messageManager, CreateShootInput(playerId, tick, aimDirection, targetId)); + } + + public static void SendShootInput(MessageManager messageManager, ShootInput message) + { + if (messageManager == null) + { + throw new System.ArgumentNullException(nameof(messageManager)); + } + + if (message == null) + { + throw new System.ArgumentNullException(nameof(message)); + } + + messageManager.SendMessage(message, MessageType.ShootInput); + } } public class MovementComponent : MonoBehaviour diff --git a/Assets/Scripts/NetworkManager.cs b/Assets/Scripts/NetworkManager.cs index 69ab8ce..dcc6b66 100644 --- a/Assets/Scripts/NetworkManager.cs +++ b/Assets/Scripts/NetworkManager.cs @@ -215,12 +215,13 @@ public class NetworkManager : MonoBehaviour public void SendShootInput(string playerId, Vector3 direction, long tick = 0, string targetId = "") { - SendShootInput(ClientGameplayInputFlow.CreateShootInput(playerId, tick, direction, targetId)); + ClientGameplayInputFlow.SendShootInput(_networkRuntime.MessageManager, playerId, tick, direction, targetId); + Debug.Log($"PlayerShootSeq: {_sequence++}"); } public void SendShootInput(ShootInput message) { - _networkRuntime.MessageManager.SendMessage(message, MessageType.ShootInput); + ClientGameplayInputFlow.SendShootInput(_networkRuntime.MessageManager, message); Debug.Log($"PlayerShootSeq: {_sequence++}"); } diff --git a/Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs b/Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs new file mode 100644 index 0000000..a9faab6 --- /dev/null +++ b/Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs @@ -0,0 +1,161 @@ +using System.Net; +using Network.Defines; +using Network.NetworkApplication; +using NUnit.Framework; +using UnityEngine; +using Vector3 = UnityEngine.Vector3; + +namespace Tests.EditMode.Network +{ + public class ClientGameplayFlowTests + { + private static readonly IPEndPoint Sender = new(IPAddress.Loopback, 9300); + + [Test] + public void ClientGameplayInputFlow_SendShootInput_UsesDedicatedGameplayPathAndReliableLane() + { + var reliableTransport = new GameplayFlowFakeTransport(); + var syncTransport = new GameplayFlowFakeTransport(); + var manager = new MessageManager( + reliableTransport, + new MainThreadNetworkDispatcher(), + new DefaultMessageDeliveryPolicyResolver(), + syncTransport); + + ClientGameplayInputFlow.SendShootInput( + manager, + "player-1", + 17, + new Vector3(3f, 0f, 4f), + "enemy-1"); + + Assert.That(reliableTransport.SentMessages.Count, Is.EqualTo(1)); + Assert.That(syncTransport.SentMessages.Count, Is.EqualTo(0)); + + var envelope = Envelope.Parser.ParseFrom(reliableTransport.SentMessages[0]); + var shootInput = ShootInput.Parser.ParseFrom(envelope.Payload); + Assert.That((MessageType)envelope.Type, Is.EqualTo(MessageType.ShootInput)); + Assert.That(shootInput.PlayerId, Is.EqualTo("player-1")); + Assert.That(shootInput.Tick, Is.EqualTo(17)); + Assert.That(shootInput.DirX, Is.EqualTo(0.6f).Within(0.0001f)); + Assert.That(shootInput.DirY, Is.EqualTo(0.8f).Within(0.0001f)); + Assert.That(shootInput.TargetId, Is.EqualTo("enemy-1")); + } + + [Test] + public void SharedNetworkRuntime_CombatEventReceivePath_AppliesAuthoritativeDamageAndDeath() + { + var transport = new GameplayFlowFakeTransport(); + var runtime = new SharedNetworkRuntime(transport, new MainThreadNetworkDispatcher()); + var harness = new ClientGameplayTestHarness("player-1"); + harness.Register(runtime.MessageManager); + + transport.EmitReceive( + GameplayFlowTestSupport.BuildEnvelope( + MessageType.PlayerState, + GameplayFlowTestSupport.CreatePlayerState("player-1", 10, Vector3.zero, hp: 100)), + Sender); + transport.EmitReceive( + GameplayFlowTestSupport.BuildEnvelope( + MessageType.CombatEvent, + new CombatEvent + { + Tick = 11, + EventType = CombatEventType.DamageApplied, + AttackerId = "enemy-1", + TargetId = "player-1", + Damage = 35, + HitPosition = new global::Network.Defines.Vector3 { X = 2f, Y = 0f, Z = 1f } + }), + Sender); + transport.EmitReceive( + GameplayFlowTestSupport.BuildEnvelope( + MessageType.CombatEvent, + new CombatEvent + { + Tick = 12, + EventType = CombatEventType.Death, + AttackerId = "enemy-1", + TargetId = "player-1" + }), + Sender); + + runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); + + Assert.That(harness.TryGetState("player-1", out var snapshot), Is.True); + Assert.That(snapshot.Hp, Is.EqualTo(0)); + Assert.That(harness.TryGetCombatPresentation("player-1", out var combatPresentation), Is.True); + Assert.That(combatPresentation.HasLastEvent, Is.True); + Assert.That(combatPresentation.LastEventType, Is.EqualTo(CombatEventType.Death)); + Assert.That(combatPresentation.IsDead, Is.True); + } + + [Test] + public void SharedNetworkRuntime_CombatEventReceivePath_RoutesShootRejectedToAttackerDiagnostics() + { + var transport = new GameplayFlowFakeTransport(); + var runtime = new SharedNetworkRuntime(transport, new MainThreadNetworkDispatcher()); + var harness = new ClientGameplayTestHarness("player-1"); + harness.Register(runtime.MessageManager); + + transport.EmitReceive( + GameplayFlowTestSupport.BuildEnvelope( + MessageType.PlayerState, + GameplayFlowTestSupport.CreatePlayerState("player-1", 20, Vector3.zero, hp: 90)), + Sender); + transport.EmitReceive( + GameplayFlowTestSupport.BuildEnvelope( + MessageType.CombatEvent, + new CombatEvent + { + Tick = 21, + EventType = CombatEventType.ShootRejected, + AttackerId = "player-1", + TargetId = "enemy-2" + }), + Sender); + + runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); + + Assert.That(harness.TryGetState("player-1", out var snapshot), Is.True); + Assert.That(snapshot.Hp, Is.EqualTo(90)); + Assert.That(harness.TryGetCombatPresentation("player-1", out var combatPresentation), Is.True); + Assert.That(combatPresentation.LastEventType, Is.EqualTo(CombatEventType.ShootRejected)); + Assert.That(combatPresentation.LastDamage, Is.EqualTo(0)); + Assert.That(combatPresentation.IsDead, Is.False); + } + + [Test] + public void ClientGameplayHarness_RemotePlayerStateFlow_RejectsStaleSnapshots_AndUsesInterpolationOrLatestClamp() + { + var harness = new ClientGameplayTestHarness("local-player"); + + var firstAccepted = harness.HandlePlayerState( + GameplayFlowTestSupport.CreatePlayerState("remote-player", 10, new Vector3(0f, 0f, 0f), rotation: 0f), + receivedAtSeconds: 0f); + var secondAccepted = harness.HandlePlayerState( + GameplayFlowTestSupport.CreatePlayerState("remote-player", 11, new Vector3(10f, 0f, 0f), rotation: 90f), + receivedAtSeconds: 0.05f); + var staleAccepted = harness.HandlePlayerState( + GameplayFlowTestSupport.CreatePlayerState("remote-player", 9, new Vector3(99f, 0f, 0f), rotation: 180f), + receivedAtSeconds: 0.06f); + + var interpolated = harness.SampleRemote("remote-player", 0.125f); + var clamped = harness.SampleRemote("remote-player", 0.35f); + + Assert.That(firstAccepted, Is.True); + Assert.That(secondAccepted, Is.True); + Assert.That(staleAccepted, Is.False); + Assert.That(harness.GetBufferedSnapshotCount("remote-player"), Is.EqualTo(1)); + Assert.That(harness.GetLatestBufferedTick("remote-player"), Is.EqualTo(11)); + Assert.That(interpolated.HasValue, Is.True); + Assert.That(interpolated.UsedInterpolation, Is.True); + Assert.That(interpolated.Position.x, Is.EqualTo(5f).Within(0.001f)); + Assert.That(interpolated.Rotation.eulerAngles.y, Is.EqualTo(45f).Within(0.01f)); + Assert.That(clamped.HasValue, Is.True); + Assert.That(clamped.UsedInterpolation, Is.False); + Assert.That(clamped.LatestSnapshot.Tick, Is.EqualTo(11)); + Assert.That(clamped.Position, Is.EqualTo(new Vector3(10f, 0f, 0f))); + } + } +} diff --git a/Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs.meta b/Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs.meta new file mode 100644 index 0000000..184926b --- /dev/null +++ b/Assets/Tests/EditMode/Network/ClientGameplayFlowTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a4fcc49ae3441f6bc9f251af0f1f067 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs new file mode 100644 index 0000000..a296c47 --- /dev/null +++ b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Network.Defines; +using Network.NetworkApplication; +using Network.NetworkHost; +using NUnit.Framework; +using UnityEngine; +using Vector3 = UnityEngine.Vector3; + +namespace Tests.EditMode.Network +{ + public class GameplayFlowRoundTripTests + { + private static readonly IPEndPoint ClientPeer = new(IPAddress.Loopback, 9401); + private static readonly IPEndPoint RemotePeer = new(IPAddress.Loopback, 9402); + private static readonly IPEndPoint ServerSender = new(IPAddress.Loopback, 9000); + + [Test] + public void FakeTransportRoundTrip_MoveInputAndShootInput_ProduceAuthoritativePlayerStateAndCombatEvent() + { + var clientReliableTransport = new GameplayFlowFakeTransport(); + var clientSyncTransport = new GameplayFlowFakeTransport(); + var clientRuntime = new SharedNetworkRuntime( + clientReliableTransport, + new MainThreadNetworkDispatcher(), + syncTransport: clientSyncTransport); + var clientHarness = new ClientGameplayTestHarness("player-a"); + clientHarness.Register(clientRuntime.MessageManager); + + var serverTransports = new Dictionary(); + var configuration = new ServerRuntimeConfiguration(9000) + { + SyncPort = 9001, + Dispatcher = new MainThreadNetworkDispatcher(), + TransportFactory = port => CreateTransport(serverTransports, port), + AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration + { + MoveSpeed = 10f, + BroadcastInterval = TimeSpan.FromMilliseconds(50), + DefaultHp = 100 + }, + AuthoritativeCombat = new ServerAuthoritativeCombatConfiguration + { + DamagePerShot = 30 + } + }; + + clientRuntime.StartAsync().GetAwaiter().GetResult(); + using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); + + serverTransports[9001].EmitReceive( + GameplayFlowTestSupport.BuildEnvelope( + MessageType.MoveInput, + new MoveInput + { + PlayerId = "player-b", + Tick = 1, + MoveX = 0f, + MoveY = 0f + }), + RemotePeer); + serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); + serverTransports[9001].ClearOutgoing(); + serverTransports[9000].ClearOutgoing(); + + clientRuntime.MessageManager.SendMessage( + new MoveInput + { + PlayerId = "player-a", + Tick = 1, + MoveX = 1f, + MoveY = 0f + }, + MessageType.MoveInput); + ClientGameplayInputFlow.SendShootInput( + clientRuntime.MessageManager, + "player-a", + 2, + Vector3.right, + "player-b"); + + TransferSentMessages(clientSyncTransport, serverTransports[9001], ClientPeer); + TransferSentMessages(clientReliableTransport, serverTransports[9000], ClientPeer); + + serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); + serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50)); + + TransferBroadcastMessages(serverTransports[9000], clientReliableTransport, ServerSender); + clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); + TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender); + clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); + + Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localServerState), Is.True); + Assert.That(localServerState.PlayerId, Is.EqualTo("player-a")); + Assert.That(localServerState.PositionX, Is.EqualTo(0.5f).Within(0.0001f)); + Assert.That(serverRuntime.TryGetAuthoritativeCombatState(RemotePeer, out var remoteCombatState), Is.True); + Assert.That(remoteCombatState.PlayerId, Is.EqualTo("player-b")); + Assert.That(remoteCombatState.Hp, Is.EqualTo(70)); + + Assert.That(clientHarness.TryGetState("player-a", out var localClientState), Is.True); + Assert.That(localClientState.Tick, Is.EqualTo(1)); + Assert.That(localClientState.Position.x, Is.EqualTo(0.5f).Within(0.0001f)); + Assert.That(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True); + Assert.That(remoteClientState.Hp, Is.EqualTo(70)); + Assert.That(clientHarness.TryGetCombatPresentation("player-b", out var remoteCombatPresentation), Is.True); + Assert.That(remoteCombatPresentation.LastEventType, Is.EqualTo(CombatEventType.DamageApplied)); + Assert.That(remoteCombatPresentation.LastDamage, Is.EqualTo(30)); + Assert.That(remoteCombatPresentation.IsDead, Is.False); + } + + private static GameplayFlowFakeTransport CreateTransport(IDictionary serverTransports, int port) + { + var transport = new GameplayFlowFakeTransport(); + serverTransports.Add(port, transport); + return transport; + } + + private static void TransferSentMessages(GameplayFlowFakeTransport source, GameplayFlowFakeTransport destination, IPEndPoint sender) + { + foreach (var payload in source.SentMessages) + { + destination.EmitReceive(payload, sender); + } + + source.ClearOutgoing(); + } + + private static void TransferBroadcastMessages(GameplayFlowFakeTransport source, GameplayFlowFakeTransport destination, IPEndPoint sender) + { + foreach (var payload in source.BroadcastMessages) + { + destination.EmitReceive(payload, sender); + } + + source.ClearOutgoing(); + } + } +} diff --git a/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs.meta b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs.meta new file mode 100644 index 0000000..a1bbb4e --- /dev/null +++ b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bbe6dfdb34f64d4e895f57f7191b9407 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs b/Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs new file mode 100644 index 0000000..aa1f8ef --- /dev/null +++ b/Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Google.Protobuf; +using Network.Defines; +using Network.NetworkApplication; +using Network.NetworkTransport; +using UnityEngine; +using Vector3 = UnityEngine.Vector3; + +namespace Tests.EditMode.Network +{ + internal sealed class ClientGameplayTestHarness + { + private readonly string localPlayerId; + private readonly Dictionary states = new(); + private readonly Dictionary remoteInterpolators = new(); + + public ClientGameplayTestHarness(string localPlayerId) + { + this.localPlayerId = localPlayerId ?? string.Empty; + } + + public void Register(MessageManager messageManager) + { + messageManager.RegisterHandler(MessageType.PlayerState, (payload, sender) => + { + HandlePlayerState(PlayerState.Parser.ParseFrom(payload), receivedAtSeconds: 0f); + }); + messageManager.RegisterHandler(MessageType.CombatEvent, (payload, sender) => + { + HandleCombatEvent(CombatEvent.Parser.ParseFrom(payload)); + }); + } + + public bool HandlePlayerState(PlayerState state, float receivedAtSeconds) + { + var owner = GetOrCreateOwner(state.PlayerId); + var accepted = owner.TryAccept(state, out var snapshot); + if (!accepted) + { + return false; + } + + if (!IsLocalPlayer(state.PlayerId)) + { + GetOrCreateInterpolator(state.PlayerId).TryAddSnapshot(snapshot, receivedAtSeconds); + } + + return true; + } + + public bool HandleCombatEvent(CombatEvent combatEvent) + { + if (!ClientCombatEventRouting.TryGetAffectedPlayerId(combatEvent, out var playerId)) + { + return false; + } + + var owner = GetOrCreateOwner(playerId); + return owner.TryApplyCombatEvent(combatEvent, playerId, out _, out _); + } + + public bool TryGetState(string playerId, out ClientAuthoritativePlayerStateSnapshot snapshot) + { + if (states.TryGetValue(playerId, out var owner) && owner.Current != null) + { + snapshot = owner.Current; + return true; + } + + snapshot = null; + return false; + } + + public bool TryGetCombatPresentation(string playerId, out ClientCombatPresentationSnapshot snapshot) + { + if (states.TryGetValue(playerId, out var owner)) + { + snapshot = owner.CombatPresentation; + return true; + } + + snapshot = ClientCombatPresentationSnapshot.Empty; + return false; + } + + public int GetBufferedSnapshotCount(string playerId) + { + return remoteInterpolators.TryGetValue(playerId, out var interpolator) + ? interpolator.BufferedSnapshotCount + : 0; + } + + public long GetLatestBufferedTick(string playerId) + { + return remoteInterpolators.TryGetValue(playerId, out var interpolator) + ? interpolator.LatestBufferedTick + : -1; + } + + public RemotePlayerInterpolationSample SampleRemote(string playerId, float nowSeconds) + { + return GetOrCreateInterpolator(playerId).Sample(nowSeconds); + } + + private bool IsLocalPlayer(string playerId) + { + return string.Equals(playerId, localPlayerId, StringComparison.Ordinal); + } + + private ClientAuthoritativePlayerState GetOrCreateOwner(string playerId) + { + if (!states.TryGetValue(playerId, out var owner)) + { + owner = new ClientAuthoritativePlayerState(); + states.Add(playerId, owner); + } + + return owner; + } + + private RemotePlayerSnapshotInterpolator GetOrCreateInterpolator(string playerId) + { + if (!remoteInterpolators.TryGetValue(playerId, out var interpolator)) + { + interpolator = new RemotePlayerSnapshotInterpolator(); + remoteInterpolators.Add(playerId, interpolator); + } + + return interpolator; + } + } + + internal sealed class GameplayFlowFakeTransport : ITransport + { + private readonly List sentMessages = new(); + private readonly List targetMessages = new(); + private readonly List<(byte[] Data, IPEndPoint Target)> targetedSends = new(); + private readonly List broadcastMessages = new(); + + public event Action OnReceive; + + public IReadOnlyList SentMessages => sentMessages; + + public IReadOnlyList TargetMessages => targetMessages; + + public IReadOnlyList<(byte[] Data, IPEndPoint Target)> TargetedSends => targetedSends; + + public IReadOnlyList BroadcastMessages => broadcastMessages; + + public Task StartAsync() + { + return Task.CompletedTask; + } + + public void Stop() + { + } + + public void Send(byte[] data) + { + sentMessages.Add(Copy(data)); + } + + public void SendTo(byte[] data, IPEndPoint target) + { + var copy = Copy(data); + targetMessages.Add(copy); + targetedSends.Add((copy, target)); + } + + public void SendToAll(byte[] data) + { + broadcastMessages.Add(Copy(data)); + } + + public void EmitReceive(byte[] data, IPEndPoint sender) + { + OnReceive?.Invoke(Copy(data), sender); + } + + public void ClearOutgoing() + { + sentMessages.Clear(); + targetMessages.Clear(); + targetedSends.Clear(); + broadcastMessages.Clear(); + } + + private static byte[] Copy(byte[] data) + { + if (data == null) + { + return null; + } + + var copy = new byte[data.Length]; + Array.Copy(data, copy, data.Length); + return copy; + } + } + + internal static class GameplayFlowTestSupport + { + public static byte[] BuildEnvelope(MessageType type, IMessage payload) + { + return new Envelope + { + Type = (int)type, + Payload = payload.ToByteString() + }.ToByteArray(); + } + + public static PlayerState CreatePlayerState(string playerId, long tick, Vector3 position, int hp = 100, float rotation = 0f, Vector3? velocity = null) + { + var resolvedVelocity = velocity ?? Vector3.zero; + return new PlayerState + { + PlayerId = playerId, + Tick = tick, + Position = new global::Network.Defines.Vector3 { X = position.x, Y = position.y, Z = position.z }, + Velocity = new global::Network.Defines.Vector3 { X = resolvedVelocity.x, Y = resolvedVelocity.y, Z = resolvedVelocity.z }, + Rotation = rotation, + Hp = hp + }; + } + } +} diff --git a/Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs.meta b/Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs.meta new file mode 100644 index 0000000..58fd656 --- /dev/null +++ b/Assets/Tests/EditMode/Network/GameplayFlowTestSupport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3db9ca1d45945ed9f5fd1b14922ca21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TODO.md b/TODO.md index cb9bb26..575ff26 100644 --- a/TODO.md +++ b/TODO.md @@ -31,8 +31,8 @@ Still missing for MVP: - [ ] Full `PlayerState` field application for rotation / HP / velocity - [ ] Remote-player snapshot buffering and interpolation strategy - [x] Explicit movement-stop handling via zero-input `MoveInput` -- [ ] End-to-end gameplay regression coverage -- [ ] Re-run build/test in an environment with the required .NET runtime installed +- [x] End-to-end gameplay regression coverage +- [x] Re-run build/test in an environment with the required .NET runtime installed ## Checklist @@ -154,33 +154,39 @@ Acceptance: ### 9. Expand Regression Coverage From Network Layer To Gameplay Flow -- [ ] Extend [`Assets/Tests/EditMode/Network/MessageManagerTests.cs`](./Assets/Tests/EditMode/Network/MessageManagerTests.cs) only as needed for lane policy regressions +- [x] Extend [`Assets/Tests/EditMode/Network/MessageManagerTests.cs`](./Assets/Tests/EditMode/Network/MessageManagerTests.cs) only as needed for lane policy regressions - [x] Add tests that cover explicit zero-input movement stop behavior -- [ ] Add tests for client `ShootInput` send routing -- [ ] Add tests for `CombatEvent` receive/apply behavior -- [ ] Add tests for remote `PlayerState` buffering / interpolation decisions where practical +- [x] Add tests for client `ShootInput` send routing +- [x] Add tests for `CombatEvent` receive/apply behavior +- [x] Add tests for remote `PlayerState` buffering / interpolation decisions where practical - [x] Add tests for server-authoritative movement processing - [x] Add tests for server-authoritative shooting/combat result generation -- [ ] Add at least one end-to-end fake-transport test that covers `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` +- [x] Add at least one end-to-end fake-transport test that covers `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` Acceptance: -- [ ] MVP gameplay flow is covered beyond transport-only assertions -- [ ] Both client single-session and server multi-session behaviors remain protected -- [ ] Regression tests fail if movement/combat authority accidentally drifts back to the client +- [x] MVP gameplay flow is covered beyond transport-only assertions +- [x] Both client single-session and server multi-session behaviors remain protected +- [x] Regression tests fail if movement/combat authority accidentally drifts back to the client ### 10. Re-Verify Build And Test -- [ ] Install or use an environment that contains the required .NET runtime for this repository -- [ ] Run `dotnet build Network.EditMode.Tests.csproj -v minimal` -- [ ] Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` -- [ ] Record the actual result after the environment issue is resolved +- [x] Install or use an environment that contains the required .NET runtime for this repository +- [x] Run `dotnet build Network.EditMode.Tests.csproj -v minimal` +- [x] Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` +- [x] Record the actual result after the environment issue is resolved Acceptance: -- [ ] Build succeeds in a runnable local environment -- [ ] Edit-mode network tests succeed -- [ ] New MVP gameplay regression tests succeed +- [x] Build succeeds in a runnable local environment +- [x] Edit-mode network tests succeed +- [x] New MVP gameplay regression tests succeed + +Recorded result: + +- [x] Verified on 2026-03-29 in a local environment with .NET SDK 10.0.201 installed +- [x] `dotnet build Network.EditMode.Tests.csproj -v minimal` succeeded with 4 non-fatal MSB3277 warning groups about `System.Net.Http` and `System.Security.Cryptography.Algorithms` assembly-version conflicts in Unity dependencies +- [x] `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` succeeded, covering the edit-mode network and MVP gameplay regression suite ## Recommended Order diff --git a/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/.openspec.yaml b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/.openspec.yaml new file mode 100644 index 0000000..5e98b74 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-29 diff --git a/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/design.md b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/design.md new file mode 100644 index 0000000..670a5d1 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/design.md @@ -0,0 +1,53 @@ +## Context + +The repository already has solid shared-network regression coverage for message typing, lane policy, stale filtering, and recent server-authoritative movement/combat behavior. What remains thin is the gameplay-flow layer that joins client input send paths, client receive/apply paths, remote snapshot decisions, and at least one fake-transport round trip across the server authority loop. The main constraint is to improve confidence without turning edit-mode tests into Unity-scene integration tests or changing shared networking contracts just to satisfy test code. + +## Goals / Non-Goals + +**Goals:** +- Add gameplay-flow regression tests that protect the MVP contract from client input through authoritative server outputs and client-side application. +- Keep low-level routing assertions in focused tests while moving broader gameplay expectations into runtime-level regression fixtures. +- Reuse existing fake transports, host/runtime entry points, and explicit runtime handles where possible. +- Introduce only minimal test seams when existing runtime surfaces cannot expose the state needed for stable assertions. + +**Non-Goals:** +- Rework production gameplay architecture beyond what is minimally required for observability in tests. +- Add Unity play-mode or scene-driven end-to-end automation. +- Change message definitions, delivery policy rules, or authority ownership as part of this testing change. + +## Decisions + +### Decision: Keep regression coverage layered instead of expanding `MessageManagerTests` +`MessageManagerTests` should remain responsible for lane-policy and message-routing invariants only. Gameplay send/receive/application assertions belong in higher-level edit-mode tests that exercise `NetworkManager`, authoritative client state application, interpolation buffers, and server runtime handles together. + +Alternative considered: continue adding gameplay assertions into `MessageManagerTests`. +- Rejected because it would blur transport-policy failures with gameplay-flow failures and make the tests harder to maintain. + +### Decision: Prefer existing fake transports and runtime surfaces, add only narrow observability seams +The test suite should keep using fake transports and explicit runtime handles to drive inputs and inspect outputs. If a client-side flow cannot be asserted without reaching into private state, add a narrow seam such as an inspectable snapshot buffer view or combat-application callback surface rather than introducing Unity-only dependencies into shared code. + +Alternative considered: add broad test-only hooks or mock-heavy abstractions around the whole networking layer. +- Rejected because it would distort the production architecture and create maintenance burden unrelated to MVP behavior. + +### Decision: Add one explicit fake-transport gameplay round-trip that spans client and server responsibilities +Beyond isolated unit-style tests, the suite should include at least one end-to-end fake-transport regression that proves `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` still flow through the MVP authority model. This test should stay deterministic, edit-mode friendly, and limited to the shared/client runtime surfaces already used in the repository. + +Alternative considered: rely only on separate client tests and separate server tests. +- Rejected because it would leave the handoff between client send paths and authoritative server outputs unprotected. + +## Risks / Trade-offs + +- [Risk] Gameplay-flow tests may become brittle if they assert too many incidental details. → Mitigation: assert stable observable outcomes such as lane choice, accepted state updates, interpolation decisions, and authoritative event types rather than internal call order unless the order is part of the contract. +- [Risk] End-to-end fake-transport tests may need extra setup and slow the suite. → Mitigation: keep the number of full-flow tests small and reuse focused fixtures/helpers. +- [Risk] Client observability needs may tempt test-only architecture changes. → Mitigation: require every new seam to be narrow, production-safe, and useful for diagnostics as well as tests. + +## Migration Plan + +1. Add or extend edit-mode fixtures for client send/receive/application flow and remote snapshot buffering. +2. Add one deterministic fake-transport end-to-end gameplay test that drives both movement and combat authority. +3. Leave existing lane-policy tests in place, trimming or extending `MessageManagerTests` only where delivery-policy regressions specifically need coverage. +4. Update TODO/OpenSpec task tracking after the regression suite protects the MVP gameplay loop. + +## Open Questions + +- None. The remaining work is implementation detail inside the agreed MVP gameplay surfaces. diff --git a/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/proposal.md b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/proposal.md new file mode 100644 index 0000000..8fbaceb --- /dev/null +++ b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/proposal.md @@ -0,0 +1,22 @@ +## Why + +The MVP networking flow now has concrete client input, client authoritative-state application, remote interpolation, and server-authoritative movement/combat behavior, but regression coverage still skews toward transport and isolated network-layer rules. Step 9 is needed now so future changes cannot silently push gameplay authority, routing, or presentation flow back toward client-side guesswork. + +## What Changes + +- Add a dedicated gameplay-flow regression coverage capability for edit-mode network tests. +- Define regression expectations for client `ShootInput` send routing, client `CombatEvent` receive/apply flow, remote `PlayerState` buffering/interpolation decisions, and fake-transport end-to-end gameplay message flow. +- Keep `MessageManagerTests` focused on lane-policy regressions only, with broader gameplay assertions moving into higher-level flow tests. +- Require coverage for both client single-session behavior and server multi-session authority paths where the MVP gameplay loop crosses that boundary. + +## Capabilities + +### New Capabilities +- `gameplay-flow-regression-coverage`: Define the required regression coverage that protects the MVP gameplay loop from client input through authoritative server state/combat results and client-side application. + +### Modified Capabilities +- None. + +## Impact + +Affected areas include `Assets/Tests/EditMode/Network/`, lightweight fake-transport/runtime test fixtures, and any minimal runtime/test seams needed to observe client gameplay flow without changing shared networking contracts. This change should not alter production transport policy or MVP message definitions. diff --git a/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/specs/gameplay-flow-regression-coverage/spec.md b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/specs/gameplay-flow-regression-coverage/spec.md new file mode 100644 index 0000000..aaae2d8 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/specs/gameplay-flow-regression-coverage/spec.md @@ -0,0 +1,31 @@ +## ADDED Requirements + +### Requirement: Gameplay-flow regressions cover client gameplay send and receive paths +The edit-mode regression suite SHALL cover the MVP client gameplay flow above the raw transport router, including `ShootInput` send routing and authoritative `CombatEvent` receive/apply behavior. Lane-policy assertions that belong to `MessageManager` MAY remain in `MessageManagerTests`, but gameplay-flow assertions MUST live in tests that exercise the client runtime or player-facing application path. + +#### Scenario: Client fire intent regression proves dedicated `ShootInput` routing +- **WHEN** the controlled client gameplay path is exercised in an edit-mode regression test for a fire action +- **THEN** the test observes a `ShootInput` payload sent through the dedicated client shooting path +- **THEN** any lane-policy assertion in that flow remains limited to confirming the MVP reliable-lane contract rather than replacing broader gameplay-flow coverage + +#### Scenario: Authoritative combat event regression proves client-side application +- **WHEN** an edit-mode regression test delivers an authoritative `CombatEvent` into the client gameplay receive path +- **THEN** the relevant player-owned authoritative state, presentation model, or diagnostics surface reflects the authoritative hit, damage, death, or rejection result +- **THEN** the test proves the outcome is applied from server truth rather than speculative local combat logic + +### Requirement: Gameplay-flow regressions cover remote authoritative snapshot decisions +The edit-mode regression suite SHALL cover the client path that buffers and consumes remote authoritative `PlayerState` snapshots, including stale rejection and interpolation/clamp behavior where practical. + +#### Scenario: Remote interpolation regression proves buffering and stale rejection +- **WHEN** an edit-mode regression test feeds ordered and stale remote `PlayerState` snapshots into the client remote-player path +- **THEN** the test observes that newer authoritative snapshots enter the remote buffer while stale snapshots do not replace newer accepted state +- **THEN** the test verifies the resulting interpolation or latest-snapshot clamp decision matches the MVP remote-presentation rules + +### Requirement: Gameplay-flow regressions include a fake-transport authoritative round trip +The edit-mode regression suite SHALL include at least one deterministic fake-transport test that spans client send behavior, server-authoritative processing, and outgoing authoritative results. That round-trip regression MUST cover `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` within the same MVP gameplay-flow suite. + +#### Scenario: Fake-transport round trip preserves server authority across movement and combat +- **WHEN** an edit-mode regression test drives gameplay input through fake client/server transports and advances the server authority loop +- **THEN** the authoritative server path emits `PlayerState` snapshots in response to movement input +- **THEN** the authoritative server path emits `CombatEvent` results in response to shooting input +- **THEN** the combined test protects both client single-session input flow and server multi-session authoritative behavior from regression diff --git a/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/tasks.md b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/tasks.md new file mode 100644 index 0000000..7091e59 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-expand-regression-coverage-from-network-layer-to-gameplay-flow/tasks.md @@ -0,0 +1,17 @@ +## 1. Client Gameplay Flow Coverage + +- [x] 1.1 Add or extend edit-mode client tests that prove fire intent sends `ShootInput` through the dedicated gameplay path and only retain `MessageManagerTests` assertions needed for reliable-lane policy regressions. +- [x] 1.2 Add edit-mode regression tests that deliver authoritative `CombatEvent` messages through the client receive path and verify player-owned authoritative state, presentation, or diagnostics apply hit/damage/death/rejection results from server truth. +- [x] 1.3 Add any minimal production-safe observability seams needed for the client gameplay tests without introducing Unity dependencies into shared networking code. + +## 2. Snapshot And End-To-End Coverage + +- [x] 2.1 Add edit-mode regression tests that cover remote `PlayerState` buffering, stale snapshot rejection, and interpolation-versus-clamp decisions where practical. +- [x] 2.2 Add at least one deterministic fake-transport gameplay-flow test that drives `MoveInput -> PlayerState` through the authoritative server path. +- [x] 2.3 Extend that fake-transport gameplay-flow coverage to also drive `ShootInput -> CombatEvent` and verify the combined flow protects client single-session input behavior plus server multi-session authority. + +## 3. Tracking And Verification + +- [x] 3.1 Update `TODO.md` to mark the gameplay-flow regression coverage items completed or narrowed to any remaining follow-up work. +- [x] 3.2 Keep the new regression suite organized under `Assets/Tests/EditMode/Network/` with names and fixtures that separate lane-policy tests from higher-level gameplay-flow tests. +- [x] 3.3 Run `dotnet build Network.EditMode.Tests.csproj -v minimal` and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`, then record the actual result in the implementation summary. diff --git a/openspec/changes/archive/2026-03-29-re-verify-build-and-test/.openspec.yaml b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/.openspec.yaml new file mode 100644 index 0000000..5e98b74 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-29 diff --git a/openspec/changes/archive/2026-03-29-re-verify-build-and-test/design.md b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/design.md new file mode 100644 index 0000000..5d789a8 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/design.md @@ -0,0 +1,40 @@ +## Context + +The repository's MVP networking and gameplay-flow work is already implemented and covered by edit-mode tests, but TODO step 10 remains open until the build and test commands are re-run in an environment that actually contains the required .NET runtime. The change is intentionally narrow: it does not introduce new gameplay behavior, only a final verification pass and a recorded outcome in project tracking. + +## Goals / Non-Goals + +**Goals:** +- Define a repeatable verification path for the repository's edit-mode build and test commands. +- Re-run the existing CLI commands in a runnable local environment instead of leaving the result inferred from partial or blocked attempts. +- Record the actual outcome, including any remaining warnings, in the TODO and change tracking. + +**Non-Goals:** +- Introducing new runtime, gameplay, or transport behavior. +- Expanding the regression suite beyond what step 9 already added. +- Solving unrelated SDK or editor installation issues outside what is minimally needed to run the verification commands. + +## Decisions + +### Decision: Treat this as a verification-only change +This change stays focused on environment readiness, command execution, and result recording. That keeps the scope aligned with TODO step 10 and avoids reopening already-implemented gameplay work. + +Alternative considered: Roll environment fixes and additional code cleanup into the same change. Rejected because it would blur whether a failure came from verification setup or from new functional modifications. + +### Decision: Verify the exact documented commands +The source of truth remains the repository commands already documented in `AGENTS.md` and `TODO.md`: +- `dotnet build Network.EditMode.Tests.csproj -v minimal` +- `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` + +Alternative considered: Use ad hoc command variants or Unity editor-driven test execution. Rejected because the TODO explicitly calls for these CLI verification steps. + +### Decision: Record warnings separately from pass/fail status +If build and test succeed but still emit known Unity dependency warnings, the recorded result should preserve that nuance instead of flattening everything into a generic success line. + +Alternative considered: Ignore warnings once commands pass. Rejected because the TODO asks for the actual result, not a simplified interpretation. + +## Risks / Trade-offs + +- [Environment drift] -> The runtime available on the current machine may differ from prior attempts. Mitigation: record the actual command outcome from the environment used for this change. +- [Over-scoping] -> Verification-only work can accidentally turn into general cleanup. Mitigation: limit edits to tracking/docs unless command failures expose a clear regression that must be fixed to complete step 10. +- [False confidence] -> A successful CLI run does not prove every Unity editor path. Mitigation: keep the scope explicit: this change verifies the documented build/test path, not all editor execution modes. diff --git a/openspec/changes/archive/2026-03-29-re-verify-build-and-test/proposal.md b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/proposal.md new file mode 100644 index 0000000..6091ef9 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/proposal.md @@ -0,0 +1,21 @@ +## Why + +The MVP networking work is already implemented, but the TODO still requires a final build-and-test verification in a runnable local environment. This change closes that gap by making the verification step explicit, repeatable, and recorded against the current gameplay regression suite. + +## What Changes + +- Define a small verification capability for running the repository's edit-mode build and test commands in an environment with the required .NET runtime. +- Record the actual build and test result for the current MVP networking and gameplay-flow regression suite. +- Update project tracking so TODO step 10 reflects the completed verification state and any remaining environment caveats. + +## Capabilities + +### New Capabilities +- `build-test-verification`: Defines the required local environment assumptions, commands, and recorded result for final MVP build/test verification. + +### Modified Capabilities +- None. + +## Impact + +Affected areas include OpenSpec tracking under `openspec/`, the root `TODO.md`, and the CLI verification path driven by `dotnet build Network.EditMode.Tests.csproj -v minimal` and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`. diff --git a/openspec/changes/archive/2026-03-29-re-verify-build-and-test/specs/build-test-verification/spec.md b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/specs/build-test-verification/spec.md new file mode 100644 index 0000000..376103c --- /dev/null +++ b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/specs/build-test-verification/spec.md @@ -0,0 +1,22 @@ +## ADDED Requirements + +### Requirement: Runnable CLI verification environment +The repository SHALL define step 10 completion in terms of a local environment that can execute the documented `dotnet build` and `dotnet test` commands for `Network.EditMode.Tests.csproj` without failing due to a missing required .NET runtime. + +#### Scenario: Environment is suitable for verification +- **WHEN** a maintainer performs the final MVP verification pass +- **THEN** the environment used for that pass MUST contain the runtime needed to execute the documented CLI build and test commands +- **AND** the verification record MUST distinguish environment readiness issues from actual build or test failures + +### Requirement: Build and test commands are re-run and recorded +The repository SHALL re-run the documented edit-mode CLI verification commands and record the actual outcome for the current MVP networking codebase. + +#### Scenario: Build and test both succeed +- **WHEN** `dotnet build Network.EditMode.Tests.csproj -v minimal` succeeds and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` succeeds +- **THEN** project tracking MUST mark the build/test verification step complete +- **AND** the recorded result MUST state that the edit-mode network test suite passed in the runnable environment + +#### Scenario: Verification succeeds with warnings +- **WHEN** the documented build and test commands succeed but emit non-fatal warnings +- **THEN** the recorded result MUST preserve the warnings as part of the verification summary +- **AND** the step MUST still be considered complete because the commands passed diff --git a/openspec/changes/archive/2026-03-29-re-verify-build-and-test/tasks.md b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/tasks.md new file mode 100644 index 0000000..a64ec75 --- /dev/null +++ b/openspec/changes/archive/2026-03-29-re-verify-build-and-test/tasks.md @@ -0,0 +1,21 @@ +## 1. Verification Environment + +- [x] 1.1 Confirm or switch to a local environment that contains the required .NET runtime for `Network.EditMode.Tests.csproj`. +- [x] 1.2 Re-check the documented verification commands and any required environment variables before execution. + +## 2. CLI Verification + +- [x] 2.1 Run `dotnet build Network.EditMode.Tests.csproj -v minimal` in the runnable environment. +- [x] 2.2 Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` in the same runnable environment. +- [x] 2.3 Capture the actual pass/fail outcome and any remaining non-fatal warnings from both commands. + +## 3. Tracking Update + +- [x] 3.1 Update `TODO.md` step 10 and acceptance items to reflect the real verification result. +- [x] 3.2 Update this change's implementation tracking with the recorded verification summary so archive-ready state is explicit. + +## Verification Summary + +- Environment used: local machine with .NET SDK 10.0.201 and the repository's Unity project files available. +- `dotnet build Network.EditMode.Tests.csproj -v minimal`: succeeded with 4 non-fatal MSB3277 warning groups related to `System.Net.Http` and `System.Security.Cryptography.Algorithms` Unity dependency conflicts. +- `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`: succeeded for the edit-mode network and MVP gameplay regression suite. diff --git a/openspec/specs/build-test-verification/spec.md b/openspec/specs/build-test-verification/spec.md new file mode 100644 index 0000000..637ca6c --- /dev/null +++ b/openspec/specs/build-test-verification/spec.md @@ -0,0 +1,28 @@ +# build-test-verification Specification + +## Purpose + +Define the runnable local environment and recorded CLI verification result required to close the final MVP build/test verification step. + +## Requirements + +### Requirement: Runnable CLI verification environment +The repository SHALL define step 10 completion in terms of a local environment that can execute the documented `dotnet build` and `dotnet test` commands for `Network.EditMode.Tests.csproj` without failing due to a missing required .NET runtime. + +#### Scenario: Environment is suitable for verification +- **WHEN** a maintainer performs the final MVP verification pass +- **THEN** the environment used for that pass MUST contain the runtime needed to execute the documented CLI build and test commands +- **AND** the verification record MUST distinguish environment readiness issues from actual build or test failures + +### Requirement: Build and test commands are re-run and recorded +The repository SHALL re-run the documented edit-mode CLI verification commands and record the actual outcome for the current MVP networking codebase. + +#### Scenario: Build and test both succeed +- **WHEN** `dotnet build Network.EditMode.Tests.csproj -v minimal` succeeds and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` succeeds +- **THEN** project tracking MUST mark the build/test verification step complete +- **AND** the recorded result MUST state that the edit-mode network test suite passed in the runnable environment + +#### Scenario: Verification succeeds with warnings +- **WHEN** the documented build and test commands succeed but emit non-fatal warnings +- **THEN** the recorded result MUST preserve the warnings as part of the verification summary +- **AND** the step MUST still be considered complete because the commands passed diff --git a/openspec/specs/gameplay-flow-regression-coverage/spec.md b/openspec/specs/gameplay-flow-regression-coverage/spec.md new file mode 100644 index 0000000..fb8ca3e --- /dev/null +++ b/openspec/specs/gameplay-flow-regression-coverage/spec.md @@ -0,0 +1,35 @@ +# gameplay-flow-regression-coverage Specification + +## Purpose +Define the required edit-mode regression coverage that protects the MVP gameplay flow from client gameplay input through authoritative server outputs and client-side application. + +## Requirements +### Requirement: Gameplay-flow regressions cover client gameplay send and receive paths +The edit-mode regression suite SHALL cover the MVP client gameplay flow above the raw transport router, including `ShootInput` send routing and authoritative `CombatEvent` receive/apply behavior. Lane-policy assertions that belong to `MessageManager` MAY remain in `MessageManagerTests`, but gameplay-flow assertions MUST live in tests that exercise the client runtime or player-facing application path. + +#### Scenario: Client fire intent regression proves dedicated `ShootInput` routing +- **WHEN** the controlled client gameplay path is exercised in an edit-mode regression test for a fire action +- **THEN** the test observes a `ShootInput` payload sent through the dedicated client shooting path +- **THEN** any lane-policy assertion in that flow remains limited to confirming the MVP reliable-lane contract rather than replacing broader gameplay-flow coverage + +#### Scenario: Authoritative combat event regression proves client-side application +- **WHEN** an edit-mode regression test delivers an authoritative `CombatEvent` into the client gameplay receive path +- **THEN** the relevant player-owned authoritative state, presentation model, or diagnostics surface reflects the authoritative hit, damage, death, or rejection result +- **THEN** the test proves the outcome is applied from server truth rather than speculative local combat logic + +### Requirement: Gameplay-flow regressions cover remote authoritative snapshot decisions +The edit-mode regression suite SHALL cover the client path that buffers and consumes remote authoritative `PlayerState` snapshots, including stale rejection and interpolation/clamp behavior where practical. + +#### Scenario: Remote interpolation regression proves buffering and stale rejection +- **WHEN** an edit-mode regression test feeds ordered and stale remote `PlayerState` snapshots into the client remote-player path +- **THEN** the test observes that newer authoritative snapshots enter the remote buffer while stale snapshots do not replace newer accepted state +- **THEN** the test verifies the resulting interpolation or latest-snapshot clamp decision matches the MVP remote-presentation rules + +### Requirement: Gameplay-flow regressions include a fake-transport authoritative round trip +The edit-mode regression suite SHALL include at least one deterministic fake-transport test that spans client send behavior, server-authoritative processing, and outgoing authoritative results. That round-trip regression MUST cover `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` within the same MVP gameplay-flow suite. + +#### Scenario: Fake-transport round trip preserves server authority across movement and combat +- **WHEN** an edit-mode regression test drives gameplay input through fake client/server transports and advances the server authority loop +- **THEN** the authoritative server path emits `PlayerState` snapshots in response to movement input +- **THEN** the authoritative server path emits `CombatEvent` results in response to shooting input +- **THEN** the combined test protects both client single-session input flow and server multi-session authoritative behavior from regression