From 88b309aa76a68aeba1693c861a48310871652475 Mon Sep 17 00:00:00 2001 From: Jacob Dubin Date: Wed, 15 Apr 2026 18:24:18 -0500 Subject: [PATCH] fixes for test paths --- OpenJibo/docs/protocol-inventory.md | 1 + OpenJibo/src/Jibo.Cloud/dotnet/README.md | 1 + .../dotnet/src/Jibo.Cloud.Api/Program.cs | 5 ++++ .../ResponsePlanToSocketMessagesMapper.cs | 30 +++++++++++-------- .../WebSocketTurnFinalizationService.cs | 7 +++-- .../Models/WebSocketReply.cs | 1 + .../WebSockets/JiboWebSocketServiceTests.cs | 4 +++ 7 files changed, 33 insertions(+), 16 deletions(-) diff --git a/OpenJibo/docs/protocol-inventory.md b/OpenJibo/docs/protocol-inventory.md index 284b99f..8e5dcd5 100644 --- a/OpenJibo/docs/protocol-inventory.md +++ b/OpenJibo/docs/protocol-inventory.md @@ -72,6 +72,7 @@ The current .NET pass covers only a narrow, explicitly synthetic subset of obser - `CLIENT_NLU` turn completion using remembered listen/session metadata - `CLIENT_ASR` turn completion, including a synthetic STT seam for buffered-audio replay - `EOS` emission after completed turns +- delayed `SKILL_ACTION` emission after `EOS` on completed turn flows to better match the Node oracle timing - first richer vertical slice for joke/chat `SKILL_ACTION` playback This does not yet mean parity for: diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/README.md index 175e53e..5558ade 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/README.md @@ -76,6 +76,7 @@ Current websocket scope is still intentionally narrow: - structured websocket telemetry and live-run fixture export - `CONTEXT` capture and follow-up turn state - `EOS` completion +- delayed `SKILL_ACTION` emission after `EOS` to preserve the current Node-observed turn sequence - first skill vertical for joke/chat `SKILL_ACTION` playback - repo-root live-run capture support for both `captures/http/` and `captures/websocket/` diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Program.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Program.cs index 9b97b86..e2194ff 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Program.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Program.cs @@ -86,6 +86,11 @@ app.Use(async (context, next) => continue; } + if (reply.DelayMs > 0) + { + await Task.Delay(reply.DelayMs, context.RequestAborted); + } + var payload = Encoding.UTF8.GetBytes(reply.Text); await socket.SendAsync(payload, WebSocketMessageType.Text, true, context.RequestAborted); } diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs index a29dcfd..17efb7e 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/ResponsePlanToSocketMessagesMapper.cs @@ -6,7 +6,7 @@ namespace Jibo.Cloud.Application.Services; public sealed class ResponsePlanToSocketMessagesMapper { - public IReadOnlyList Map(ResponsePlan plan, TurnContext turn, CloudSession session, bool emitSkillActions) + public IReadOnlyList Map(ResponsePlan plan, TurnContext turn, CloudSession session, bool emitSkillActions) { var speak = plan.Actions.OfType().FirstOrDefault(); var skill = plan.Actions.OfType().FirstOrDefault(); @@ -15,9 +15,9 @@ public sealed class ResponsePlanToSocketMessagesMapper : session.LastTransId ?? string.Empty; var transcript = turn.NormalizedTranscript ?? turn.RawTranscript ?? string.Empty; var rules = ReadRules(turn); - var messages = new List(); + var messages = new List(); - messages.Add(JsonSerializer.Serialize(new + messages.Add(new SocketReplyPlan(JsonSerializer.Serialize(new { type = "LISTEN", transID = transId, @@ -43,9 +43,9 @@ public sealed class ResponsePlanToSocketMessagesMapper score = 0.95 } } - })); + }))); - messages.Add(JsonSerializer.Serialize(new + messages.Add(new SocketReplyPlan(JsonSerializer.Serialize(new { type = "EOS", data = new @@ -53,21 +53,23 @@ public sealed class ResponsePlanToSocketMessagesMapper sessionId = plan.SessionId, transID = transId } - })); + }))); if (emitSkillActions && speak is not null) { - messages.Add(JsonSerializer.Serialize(BuildSkillPayload(plan, turn, transId, speak, skill))); + messages.Add(new SocketReplyPlan( + JsonSerializer.Serialize(BuildSkillPayload(plan, turn, transId, speak, skill)), + DelayMs: 75)); } return messages; } - public IReadOnlyList MapFallback(CloudSession session, string transId, IReadOnlyList rules) + public IReadOnlyList MapFallback(CloudSession session, string transId, IReadOnlyList rules) { return [ - JsonSerializer.Serialize(new + new SocketReplyPlan(JsonSerializer.Serialize(new { type = "LISTEN", transID = transId, @@ -93,8 +95,8 @@ public sealed class ResponsePlanToSocketMessagesMapper score = 0.95 } } - }), - JsonSerializer.Serialize(new + })), + new SocketReplyPlan(JsonSerializer.Serialize(new { type = "EOS", data = new @@ -102,8 +104,8 @@ public sealed class ResponsePlanToSocketMessagesMapper sessionId = session.SessionId, transID = transId } - }), - JsonSerializer.Serialize(BuildGenericFallbackSkillPayload(transId)) + })), + new SocketReplyPlan(JsonSerializer.Serialize(BuildGenericFallbackSkillPayload(transId)), DelayMs: 75) ]; } @@ -231,4 +233,6 @@ public sealed class ResponsePlanToSocketMessagesMapper .Replace("\"", """, StringComparison.Ordinal) .Replace("'", "'", StringComparison.Ordinal); } + + public sealed record SocketReplyPlan(string Text, int DelayMs = 0); } diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs index 6e84a2e..7f0bc8d 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs @@ -269,7 +269,7 @@ public sealed class WebSocketTurnFinalizationService( session.LastIntent = "heyJibo"; session.LastListenType = "fallback"; var fallbackReplies = replyMapper.MapFallback(session, turnState.TransId ?? session.LastTransId ?? string.Empty, turnState.ListenRules) - .Select(text => new WebSocketReply { Text = text }) + .Select(map => new WebSocketReply { Text = map.Text, DelayMs = map.DelayMs }) .ToArray(); ResetBufferedAudio(session); return fallbackReplies; @@ -308,9 +308,10 @@ public sealed class WebSocketTurnFinalizationService( turnState.AwaitingTurnCompletion = false; var emitSkillActions = messageType != "CLIENT_NLU"; - var replies = replyMapper.Map(plan, finalizedTurn, session, emitSkillActions).Select(text => new WebSocketReply + var replies = replyMapper.Map(plan, finalizedTurn, session, emitSkillActions).Select(map => new WebSocketReply { - Text = text + Text = map.Text, + DelayMs = map.DelayMs }).ToArray(); ResetBufferedAudio(session); diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketReply.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketReply.cs index ec2cb17..ff34a46 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketReply.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketReply.cs @@ -3,5 +3,6 @@ namespace Jibo.Cloud.Domain.Models; public sealed class WebSocketReply { public string? Text { get; init; } + public int DelayMs { get; init; } public bool Close { get; init; } } diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs index 788ae55..bf285c9 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs @@ -49,6 +49,7 @@ public sealed class JiboWebSocketServiceTests Assert.Equal("LISTEN", ReadReplyType(replies[0])); Assert.Equal("EOS", ReadReplyType(replies[1])); Assert.Equal("SKILL_ACTION", ReadReplyType(replies[2])); + Assert.Equal(75, replies[2].DelayMs); using var listenPayload = JsonDocument.Parse(replies[0].Text!); Assert.Equal("hello jibo", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString()); @@ -124,6 +125,7 @@ public sealed class JiboWebSocketServiceTests Assert.Equal("LISTEN", ReadReplyType(replies[0])); Assert.Equal("EOS", ReadReplyType(replies[1])); Assert.Equal("SKILL_ACTION", ReadReplyType(replies[2])); + Assert.Equal(75, replies[2].DelayMs); using var listenPayload = JsonDocument.Parse(replies[0].Text!); Assert.Equal("tell me a joke", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString()); @@ -180,6 +182,7 @@ public sealed class JiboWebSocketServiceTests Assert.Equal("LISTEN", ReadReplyType(replies[0])); Assert.Equal("EOS", ReadReplyType(replies[1])); Assert.Equal("SKILL_ACTION", ReadReplyType(replies[2])); + Assert.Equal(75, replies[2].DelayMs); using var listenPayload = JsonDocument.Parse(replies[0].Text!); Assert.Equal("heyJibo", listenPayload.RootElement.GetProperty("data").GetProperty("nlu").GetProperty("intent").GetString()); @@ -324,6 +327,7 @@ public sealed class JiboWebSocketServiceTests Assert.Equal("LISTEN", ReadReplyType(finalizeReplies[0])); Assert.Equal("EOS", ReadReplyType(finalizeReplies[1])); Assert.Equal("SKILL_ACTION", ReadReplyType(finalizeReplies[2])); + Assert.Equal(75, finalizeReplies[2].DelayMs); using var listenPayload = JsonDocument.Parse(finalizeReplies[0].Text!); Assert.Equal("tell me a joke", listenPayload.RootElement.GetProperty("data").GetProperty("asr").GetProperty("text").GetString());