diff --git a/backend.tests/RealtimeSessionEndSessionTests.cs b/backend.tests/RealtimeSessionEndSessionTests.cs new file mode 100644 index 0000000..4f065f8 --- /dev/null +++ b/backend.tests/RealtimeSessionEndSessionTests.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Nodes; +using backend.Conversations; +using backend.Realtime; +using backend.Tools; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace backend.tests; + +public class RealtimeSessionEndSessionTests(ApiFactory factory) : IClassFixture +{ + private readonly ApiFactory _factory = factory; + + [Fact] + public async Task End_session_call_closes_session_with_reason_tool_and_skips_response_create() + { + using var scope = _factory.Services.CreateScope(); + var log = scope.ServiceProvider.GetRequiredService(); + var reg = new ToolRegistry(new ITool[] { new EndSessionTool() }); + var upstream = new ScriptedRealtimeUpstream(); + var sink = new RecordingSink(); + var cfg = new RealtimeSettings("m", "v", "p", 30, new HashSet { "end_session" }); + var session = new RealtimeSession(Guid.NewGuid(), upstream, sink, log, reg, cfg, + new FakeDeviceChannel(), NullLogger.Instance); + + upstream.Push(new JsonObject { ["type"] = "session.updated" }); + var run = session.RunAsync(default); + await sink.WaitForAsync("session_started", TimeSpan.FromSeconds(2)); + + upstream.Push(new JsonObject + { + ["type"] = "response.audio.delta", + ["delta"] = Convert.ToBase64String(new byte[3840]), + }); + upstream.Push(new JsonObject + { + ["type"] = "response.function_call_arguments.done", + ["call_id"] = "c1", + ["name"] = "end_session", + ["arguments"] = "{}", + }); + upstream.Push(new JsonObject { ["type"] = "response.done" }); + + await run; + + sink.Binary.Should().NotBeEmpty(); + + var ended = sink.Envelopes.Last(e => (string?)e["type"] == "session_ended"); + ((string?)ended["reason"]).Should().Be("tool"); + + upstream.Sent.Should().NotContain(e => (string?)e["type"] == "response.create"); + } +}