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"); } }