using System.Text.Json.Nodes; using backend.Conversations; using backend.Data; using backend.Realtime; using backend.Tools; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace backend.tests; public class RealtimeSessionTranscriptTests(ApiFactory factory) : IClassFixture { private readonly ApiFactory _factory = factory; [Fact] public async Task User_and_assistant_transcripts_are_persisted_and_response_done_emits_assistant_done() { using var scope = _factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); 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", 30, 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 cts = new CancellationTokenSource(); var run = session.RunAsync(cts.Token); await sink.WaitForAsync("session_started", TimeSpan.FromSeconds(2)); upstream.Push(new JsonObject { ["type"] = "conversation.item.input_audio_transcription.completed", ["transcript"] = "hello", }); upstream.Push(new JsonObject { ["type"] = "response.audio_transcript.done", ["transcript"] = "hi there", }); upstream.Push(new JsonObject { ["type"] = "response.done" }); await sink.WaitForAsync("assistant_done", TimeSpan.FromSeconds(2)); var convoId = session.ConversationId!.Value; var turns = await db.Turns .Where(t => t.ConversationId == convoId) .OrderBy(t => t.Index).ToListAsync(); turns.Select(t => t.Role).Should().Equal(TurnRole.User, TurnRole.Assistant); turns[0].Text.Should().Be("hello"); turns[1].Text.Should().Be("hi there"); cts.Cancel(); await run; } }