diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bff6a29..bdc223e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -50,7 +50,8 @@ "Read(//h/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/**)", "Bash(2)", "Bash(dotnet --version)", - "WebSearch" + "WebSearch", + "Bash(dotnet script:*)" ] } } diff --git a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs index 8affdbd..229578b 100644 --- a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs +++ b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs @@ -71,7 +71,12 @@ public struct SequenceBinaryInput : IBinaryInputBase buffer = _savedBuffer!; position = _savedPosition; bufferLength = _savedBufferLength; - return position < bufferLength; + + // Restored segment has enough bytes for the requested read — done + if (bufferLength - position >= needed) + return true; + + // Not enough bytes in restored segment — fall through to cross-boundary or next segment } var remaining = bufferLength - position; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs index 41581f2..b97f595 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs @@ -1,4 +1,5 @@ -using AyCode.Core.Extensions; +using System.Buffers; +using AyCode.Core.Extensions; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; @@ -7,6 +8,7 @@ using AyCode.Services.SignalRs; using MessagePack.Resolvers; using AyCode.Core.Tests.Serialization; using AyCode.Core.Serializers; +using Microsoft.AspNetCore.SignalR.Protocol; namespace AyCode.Services.Server.Tests.SignalRs; @@ -1049,6 +1051,35 @@ public abstract class SignalRClientToHubTestBase /// /// Runs all SignalR tests with JSON serialization. /// +/// +/// Diagnostic test: isolates protocol round-trip from the full client-hub flow. +/// +[TestClass] +public class ProtocolRoundTripDiagnosticTest +{ + [TestMethod] + public void Protocol_RoundTrip_BasicInvocation() + { + var protocol = new AyCodeBinaryHubProtocol(); + var binder = new TestInvocationBinder(); + + var signalParams = new SignalParams { Status = SignalResponseStatus.Success }; + var invocation = new InvocationMessage( + nameof(IAcSignalRHubClient.OnReceiveMessage), + new object?[] { 42, (int?)1, signalParams, Array.Empty() }); + + var bytes = protocol.GetMessageBytes(invocation); + + // Diagnostic: dump first bytes + var arr = bytes.ToArray(); + var lengthPrefix = BitConverter.ToInt32(arr, 0); + var firstByte = arr.Length > 4 ? arr[4] : (byte)0; + + Assert.Fail($"Bytes={arr.Length}, LengthPrefix={lengthPrefix}, Remaining={arr.Length - 4}, " + + $"FirstPayloadByte=0x{firstByte:X2}, First16={BitConverter.ToString(arr, 0, Math.Min(16, arr.Length))}"); + } +} + [TestClass] public class SignalRClientToHubTest_Json : SignalRClientToHubTestBase { diff --git a/AyCode.Services.Server.Tests/SignalRs/TestInvocationBinder.cs b/AyCode.Services.Server.Tests/SignalRs/TestInvocationBinder.cs new file mode 100644 index 0000000..d4e37da --- /dev/null +++ b/AyCode.Services.Server.Tests/SignalRs/TestInvocationBinder.cs @@ -0,0 +1,21 @@ +using AyCode.Services.SignalRs; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace AyCode.Services.Server.Tests.SignalRs; + +/// +/// Test IInvocationBinder that returns OnReceiveMessage parameter types +/// for protocol deserialization: (int messageTag, int? requestId, SignalParams signalParams, object data). +/// +internal class TestInvocationBinder : IInvocationBinder +{ + private static readonly Type[] OnReceiveMessageTypes = + [typeof(int), typeof(int?), typeof(SignalParams), typeof(object)]; + + public IReadOnlyList GetParameterTypes(string methodName) => OnReceiveMessageTypes; + + public Type GetReturnType(string invocationId) => typeof(object); + + public Type GetStreamItemType(string streamId) => typeof(object); +} diff --git a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs new file mode 100644 index 0000000..4840708 --- /dev/null +++ b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs @@ -0,0 +1,52 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using AyCode.Services.SignalRs; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace AyCode.Services.Server.Tests.SignalRs; + +/// +/// Test protocol that forces multi-segment ReadOnlySequence parsing. +/// Splits serialized bytes into chunks before calling base.TryParseMessage, +/// exercising SequenceBinaryInput cross-boundary reads and SequenceToByteArray multi-segment paths. +/// +internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol +{ + private const int SegmentSize = 4096; + + public override bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, + [NotNullWhen(true)] out HubMessage? message) + { + // Temporarily bypass multi-segment to isolate the issue + return base.TryParseMessage(ref input, binder, out message); + } + + private static ReadOnlySequence CreateMultiSegmentSequence(ReadOnlySequence source, int chunkSize) + { + var bytes = source.ToArray(); + + var first = new MemorySegment(bytes.AsMemory(0, Math.Min(chunkSize, bytes.Length))); + var current = first; + + for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize) + { + var length = Math.Min(chunkSize, bytes.Length - offset); + current = current.Append(bytes.AsMemory(offset, length)); + } + + return new ReadOnlySequence(first, 0, current, current.Memory.Length); + } + + private sealed class MemorySegment : ReadOnlySequenceSegment + { + public MemorySegment(ReadOnlyMemory memory) => Memory = memory; + + public MemorySegment Append(ReadOnlyMemory memory) + { + var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length }; + Next = next; + return next; + } + } +} diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs index 2fdcbf1..c6fb215 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs @@ -1,3 +1,4 @@ +using System.Buffers; using AyCode.Core; using AyCode.Core.Extensions; using AyCode.Core.Tests.TestModels; @@ -5,16 +6,21 @@ using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using AyCode.Services.Tests.SignalRs; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.SignalR.Protocol; namespace AyCode.Services.Server.Tests.SignalRs; /// /// Testable SignalR client that allows testing without real HubConnection. +/// Routes all messages through AyCodeBinaryHubProtocol with multi-segment splitting +/// to exercise the full serialization/deserialization pipeline. /// public class TestableSignalRClient2 : AcSignalRClientBase, IAcSignalRHubItemServer { private HubConnectionState _connectionState = HubConnectionState.Connected; private readonly TestableSignalRHub2 _signalRHub; + private readonly TestMultiSegmentProtocol _protocol = new(); + private readonly TestInvocationBinder _binder = new(); /// /// Testable SignalR client that allows testing without real HubConnection. @@ -35,7 +41,7 @@ public class TestableSignalRClient2 : AcSignalRClientBase, IAcSignalRHubItemServ } protected override HubConnectionState GetConnectionState() => _connectionState; - + protected override bool IsConnected() => _connectionState == HubConnectionState.Connected; protected override Task StartConnectionInternal() @@ -54,7 +60,21 @@ public class TestableSignalRClient2 : AcSignalRClientBase, IAcSignalRHubItemServ protected override Task SendToHubAsync(int messageTag, int? requestId, SignalParams signalParams, object? data) { - return _signalRHub.OnReceiveMessage(messageTag, requestId, signalParams, data ?? Array.Empty()); + // Protocol round-trip: serialize → multi-segment split → deserialize + var invocation = new InvocationMessage( + nameof(IAcSignalRHubClient.OnReceiveMessage), + [messageTag, requestId, signalParams, data ?? Array.Empty()]); + + var bytes = _protocol.GetMessageBytes(invocation); + var sequence = new ReadOnlySequence(bytes); + if (!_protocol.TryParseMessage(ref sequence, _binder, out var parsed) || parsed is not InvocationMessage invMsg) + throw new InvalidOperationException( + $"Protocol round-trip failed. ByteCount={bytes.Length}, Parsed={parsed?.GetType().Name ?? "null"}, " + + $"Tag={messageTag}, RequestId={requestId}, DataType={data?.GetType().Name ?? "null"}"); + + var args = invMsg.Arguments; + return _signalRHub.OnReceiveMessage( + (int)args[0]!, (int?)args[1], (SignalParams)args[2]!, args[3]!); } #endregion diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs index 9310ad3..db0809e 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs @@ -1,9 +1,14 @@ +using System.Buffers; using System.Security.Claims; +using AyCode.Core; +using AyCode.Core.Extensions; using AyCode.Core.Serializers; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using AyCode.Models.Server.DynamicMethods; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; +using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.Configuration; namespace AyCode.Services.Server.Tests.SignalRs; @@ -11,12 +16,14 @@ namespace AyCode.Services.Server.Tests.SignalRs; /// /// Testable SignalR hub that overrides infrastructure dependencies. /// Enables unit testing without SignalR server or mocks. -/// Uses base SendMessageToClient which sends raw objects directly. -/// GetResponseData<T>() handles deserialization with 3-tier fallback. +/// Routes responses through AyCodeBinaryHubProtocol with multi-segment splitting +/// to exercise the full serialization/deserialization pipeline. /// public class TestableSignalRHub2 : AcWebSignalRHubBase { private IAcSignalRHubItemServer _callerClient; + private readonly TestMultiSegmentProtocol _protocol = new(); + private readonly TestInvocationBinder _binder = new(); #region Test Configuration @@ -90,5 +97,39 @@ public class TestableSignalRHub2 : AcWebSignalRHubBase SendMessageToClient(_callerClient, messageTag, status, responseData, requestId, clientSignalParams); + protected override Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, + SignalResponseStatus status, object? responseData, int? requestId = null, SignalParams? clientSignalParams = null) + { + var isRawBytes = clientSignalParams?.IsRawBytesData == true; + + if (isRawBytes && responseData != null && responseData is not byte[]) + { + responseData = SerializerOptions.SerializerType == AcSerializerType.Binary + ? AcBinarySerializer.Serialize(responseData) + : AyCode.Core.Compression.GzipHelper.Compress(responseData.ToJson()); + } + + var signalParams = new SignalParams + { + Status = status, + DataSerializerType = SerializerOptions.SerializerType, + SignalDataType = isRawBytes ? null : responseData?.GetType().AssemblyQualifiedName, + IsRawBytesData = isRawBytes + }; + + // Protocol round-trip: serialize → multi-segment split → deserialize + var invocation = new InvocationMessage( + nameof(IAcSignalRHubClient.OnReceiveMessage), + [messageTag, requestId, signalParams, responseData ?? Array.Empty()]); + + var bytes = _protocol.GetMessageBytes(invocation); + var sequence = new ReadOnlySequence(bytes); + _protocol.TryParseMessage(ref sequence, _binder, out var parsed); + + var args = ((InvocationMessage)parsed!).Arguments; + return sendTo.OnReceiveMessage( + (int)args[0]!, (int?)args[1], (SignalParams)args[2]!, args[3]!); + } + #endregion } diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index c03b955..2626655 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -209,7 +209,7 @@ public class AcBinaryHubProtocol : IHubProtocol #region TryParseMessage - public bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, [NotNullWhen(true)] out HubMessage? message) + public virtual bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, [NotNullWhen(true)] out HubMessage? message) { message = null;