118 lines
5.0 KiB
C#
118 lines
5.0 KiB
C#
using System.Net.Http.Json;
|
|
using System.Net.WebSockets;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using FluentAssertions;
|
|
using Xunit;
|
|
|
|
namespace backend.tests;
|
|
|
|
[Collection("WakeFlow")] // tests touch FakeRealtimeSessionFactory.Upstream static — serialize
|
|
public class DeviceHubWakeFlowTests(ApiFactory factory) : IClassFixture<ApiFactory>
|
|
{
|
|
private readonly ApiFactory _factory = factory;
|
|
|
|
private async Task<string> PairAndGetTokenAsync(string email)
|
|
{
|
|
var http = _factory.CreateClient();
|
|
await http.PostAsJsonAsync("/api/auth/register",
|
|
new { email, password = "Passw0rd!" });
|
|
await http.PostAsJsonAsync("/api/auth/login",
|
|
new { email, password = "Passw0rd!" });
|
|
var codeRes = await http.PostAsJsonAsync("/api/pair-code", new { name = "wake-test" });
|
|
var code = (await codeRes.Content.ReadFromJsonAsync<Dictionary<string, object>>())!["code"].ToString()!;
|
|
var pairRes = await _factory.CreateClient().PostAsJsonAsync("/api/pair",
|
|
new { code, hostname = "smoke", client_version = "0.1" });
|
|
var pairBody = await pairRes.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
return pairBody!["device_token"].ToString()!;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Wake_opens_session_started_then_assistant_done_then_session_ended_on_cancel()
|
|
{
|
|
FakeRealtimeSessionFactory.Upstream = new ScriptedRealtimeUpstream();
|
|
var token = await PairAndGetTokenAsync("wake1@example.com");
|
|
var client = _factory.Server.CreateWebSocketClient();
|
|
client.ConfigureRequest = req => req.Headers["Authorization"] = $"Bearer {token}";
|
|
var uri = new Uri(_factory.Server.BaseAddress, "device");
|
|
var ws = await client.ConnectAsync(uri, CancellationToken.None);
|
|
|
|
await SendJson(ws, new { type = "hello", device_id = Guid.NewGuid(), client_version = "0.1" });
|
|
await ReceiveJson(ws); // ack
|
|
|
|
await SendJson(ws, new { type = "wake", at = 0L });
|
|
var started = await ReceiveJson(ws);
|
|
started.GetProperty("type").GetString().Should().Be("session_started");
|
|
|
|
FakeRealtimeSessionFactory.Upstream.Push(new JsonObject
|
|
{
|
|
["type"] = "response.audio_transcript.done",
|
|
["transcript"] = "hello!",
|
|
});
|
|
FakeRealtimeSessionFactory.Upstream.Push(new JsonObject { ["type"] = "response.done" });
|
|
|
|
var done = await ReceiveJson(ws);
|
|
done.GetProperty("type").GetString().Should().Be("assistant_done");
|
|
|
|
await SendJson(ws, new { type = "cancel" });
|
|
var ended = await ReceiveJson(ws);
|
|
ended.GetProperty("type").GetString().Should().Be("session_ended");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Binary_uplink_after_wake_reaches_upstream_as_input_audio_buffer_append()
|
|
{
|
|
FakeRealtimeSessionFactory.Upstream = new ScriptedRealtimeUpstream();
|
|
var token = await PairAndGetTokenAsync("wake2@example.com");
|
|
var client = _factory.Server.CreateWebSocketClient();
|
|
client.ConfigureRequest = req => req.Headers["Authorization"] = $"Bearer {token}";
|
|
var uri = new Uri(_factory.Server.BaseAddress, "device");
|
|
var ws = await client.ConnectAsync(uri, CancellationToken.None);
|
|
|
|
await SendJson(ws, new { type = "hello", device_id = Guid.NewGuid(), client_version = "0.1" });
|
|
await ReceiveJson(ws);
|
|
await SendJson(ws, new { type = "wake", at = 0L });
|
|
await ReceiveJson(ws); // session_started
|
|
|
|
var frame = new byte[3840];
|
|
for (int i = 0; i < frame.Length; i++) frame[i] = 0x55;
|
|
await ws.SendAsync(frame, WebSocketMessageType.Binary, true, CancellationToken.None);
|
|
|
|
// Allow time for the pump's 250 ms poll + send.
|
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
if (FakeRealtimeSessionFactory.Upstream.Sent.Any(e =>
|
|
(string?)e["type"] == "input_audio_buffer.append")) break;
|
|
await Task.Delay(50);
|
|
}
|
|
FakeRealtimeSessionFactory.Upstream.Sent
|
|
.Any(e => (string?)e["type"] == "input_audio_buffer.append")
|
|
.Should().BeTrue();
|
|
|
|
await SendJson(ws, new { type = "cancel" });
|
|
await ReceiveJson(ws);
|
|
}
|
|
|
|
private static async Task SendJson(WebSocket ws, object payload)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload));
|
|
await ws.SendAsync(bytes, WebSocketMessageType.Text, true, CancellationToken.None);
|
|
}
|
|
|
|
private static async Task<JsonElement> ReceiveJson(WebSocket ws)
|
|
{
|
|
var buf = new byte[16384];
|
|
using var stream = new MemoryStream();
|
|
while (true)
|
|
{
|
|
var r = await ws.ReceiveAsync(buf, CancellationToken.None);
|
|
if (r.MessageType == WebSocketMessageType.Binary) { stream.Write(buf, 0, r.Count); continue; }
|
|
stream.Write(buf, 0, r.Count);
|
|
if (r.EndOfMessage) break;
|
|
}
|
|
return JsonDocument.Parse(stream.ToArray()).RootElement.Clone();
|
|
}
|
|
}
|