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 RealtimeSessionIdleTimeoutTests(ApiFactory factory) : IClassFixture { private readonly ApiFactory _factory = factory; [Fact] public async Task Session_ends_with_reason_idle_when_no_speech_for_timeout() { using var scope = _factory.Services.CreateScope(); var log = scope.ServiceProvider.GetRequiredService(); var reg = new ToolRegistry(Array.Empty()); var upstream = new ScriptedRealtimeUpstream(); var sink = new RecordingSink(); var cfg = new RealtimeSettings("m", "v", "p", IdleTimeoutSeconds: 1, new HashSet()); 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)); await run.WaitAsync(TimeSpan.FromSeconds(4)); var ended = sink.Envelopes.Last(e => (string?)e["type"] == "session_ended"); ((string?)ended["reason"]).Should().Be("idle"); } [Fact] public async Task Speech_started_resets_the_idle_timer() { using var scope = _factory.Services.CreateScope(); var log = scope.ServiceProvider.GetRequiredService(); var reg = new ToolRegistry(Array.Empty()); var upstream = new ScriptedRealtimeUpstream(); var sink = new RecordingSink(); var cfg = new RealtimeSettings("m", "v", "p", IdleTimeoutSeconds: 2, new HashSet()); 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)); await Task.Delay(1000); upstream.Push(new JsonObject { ["type"] = "input_audio_buffer.speech_started" }); await Task.Delay(1500); sink.Envelopes.Any(e => (string?)e["type"] == "session_ended").Should().BeFalse(); await run.WaitAsync(TimeSpan.FromSeconds(4)); var ended = sink.Envelopes.Last(e => (string?)e["type"] == "session_ended"); ((string?)ended["reason"]).Should().Be("idle"); } }