diff --git a/.gitignore b/.gitignore
index 45a5b1870..485bcdfa3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ assets/cache
config.json
assets/cacheMisses
http-client.private.env.json
+.nixos-test-history
.vscode/settings.json
@@ -34,4 +35,4 @@ schemas_final/
temp/
!/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/
-*.qcow2
+*.qcow2
\ No newline at end of file
diff --git a/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj
index 2788c5801..47c704006 100644
--- a/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj
+++ b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj
@@ -45,7 +45,7 @@
- Never
+ Always
diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/GatewayTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/GatewayTests.cs
new file mode 100644
index 000000000..511b41391
--- /dev/null
+++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/GatewayTests.cs
@@ -0,0 +1,107 @@
+using System.Diagnostics;
+using System.Net.Http.Json;
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
+using Spacebar.Models.Gateway;
+using Spacebar.Models.Generic;
+using Spacebar.Sdk.Core;
+using Spacebar.Tests.Abstractions;
+using Spacebar.Tests.Extensions;
+using Spacebar.Tests.Fixtures;
+using Xunit.Internal;
+using Xunit.Microsoft.DependencyInjection.Abstracts;
+
+namespace Spacebar.Tests.Tests;
+
+public class GatewayTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed(testOutputHelper, fixture), IAsyncLifetime {
+ private readonly Config _config = fixture.GetRequiredService(testOutputHelper);
+ private readonly UserAbstraction _userAbstraction = fixture.GetRequiredService(testOutputHelper);
+
+ private static AuthenticatedSpacebarClient Client { get; set; } = null!;
+
+ public async ValueTask InitializeAsync() {
+ testOutputHelper.WriteLine("Running InitializeAsync");
+ // All these tests can share a single client
+ Client = await _userAbstraction.GetSharedUser();
+ }
+
+ [Fact]
+ public async Task CanConnect() {
+ await Client.Gateway.Connect();
+ await Client.Gateway.Disconnect();
+ }
+
+ [Fact]
+ public async Task CanReceiveReady() {
+ Client.Gateway.OnceGatewayMessage.Add(async payload => {
+ if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) {
+ _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count);
+ await Client.Gateway.Disconnect();
+ return true;
+ }
+
+ _testOutputHelper.WriteLine("Received message: {0}", payload.ToJson(indent: false));
+ return false;
+ });
+ // Client.Gateway.TraceGatewayMessages = true;
+ await Client.Gateway.Connect();
+ await Client.Gateway.Start();
+ }
+
+ [Fact]
+ public async Task CanReceiveReadySupplemental() {
+ Client.Gateway.OnceGatewayMessage.Add(async payload => {
+ if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY_SUPPLEMENTAL" }) {
+ _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count);
+ await Client.Gateway.Disconnect();
+ return true;
+ }
+
+ _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count);
+ return false;
+ });
+ // Client.Gateway.TraceGatewayMessages = true;
+ await Client.Gateway.Connect();
+ await Client.Gateway.Start();
+ }
+
+
+ [Fact]
+ public async Task CanReceiveHeartbeatAck() {
+ Client.Gateway.OnceGatewayMessage.Add(async payload => {
+ if (payload is { Opcode: GatewayOpcode.S2CHeartbeatAck }) {
+ _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count);
+ await Client.Gateway.Disconnect();
+ return true;
+ }
+
+ _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count);
+ return false;
+ });
+ // Client.Gateway.TraceGatewayMessages = true;
+ await Client.Gateway.Connect();
+ await Client.Gateway.Start();
+ }
+
+ [Fact]
+ public async Task SensibleHello() {
+ Client.Gateway.OnceGatewayMessage.Add(async payload => {
+ if (payload is { Opcode: GatewayOpcode.S2CHello }) {
+ _testOutputHelper.WriteLine("Success: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count);
+ var data = payload.GetData();
+ await Client.Gateway.Disconnect();
+
+ Assert.NotEqual(0, data.HeartbeatInterval);
+ Assert.True(data.HeartbeatInterval > 1000, "data.HeartbeatInterval > 1000");
+ return true;
+ }
+
+ _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count);
+ return false;
+ });
+ // Client.Gateway.TraceGatewayMessages = true;
+ await Client.Gateway.Connect();
+ await Client.Gateway.Start();
+ }
+
+}
\ No newline at end of file
diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs
index e87d7f8fc..282f5824b 100644
--- a/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs
+++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/MessageTests.cs
@@ -5,6 +5,7 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using ArcaneLibs.Extensions;
using Spacebar.Models.Api;
+using Spacebar.Models.Gateway;
using Spacebar.Models.Generic;
using Spacebar.Sdk.Core;
using Spacebar.Tests.Abstractions;
@@ -222,4 +223,29 @@ public class MessageTests(ITestOutputHelper testOutputHelper, TestFixture fixtur
var msg = json.Deserialize();
Assert.Equal(content, msg.Content);
}
+
+ [Fact]
+ public async Task ShouldNotMessageUpdateForLinklessMessage() {
+ Client.Gateway.OnceGatewayMessage.Add(async payload => {
+ if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) {
+ _testOutputHelper.WriteLine("Got ready: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count);
+ await Channel.SendMessageAsync(new MessageSchema() {
+
+ });
+ return false;
+ }
+
+ if (payload is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "MESSAGE_UPDATE" }) {
+ _testOutputHelper.WriteLine("Got MESSAGE_UPDATE: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData!.Count);
+ Assert.False(true);
+ return true;
+ }
+
+ _testOutputHelper.WriteLine("Received message: {0} {1} ({2} data keys)", payload.Opcode, payload.DispatchEventType, payload.EventData?.Count);
+ return false;
+ });
+ // Client.Gateway.TraceGatewayMessages = true;
+ await Client.Gateway.Connect();
+ await Client.Gateway.Start();
+ }
}
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs b/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs
index 2058e21f3..25dabdca1 100644
--- a/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs
+++ b/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Net.Http.Json;
using System.Net.WebSockets;
+using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -128,6 +129,10 @@ public class SpacebarClientChannel(AuthenticatedSpacebarClient client, long chan
if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync())!);
return (await resp.Content.ReadFromJsonAsync())!;
}
+
+ public async Task SendMessageAsync(MessageSchema messageSchema) {
+ throw new NotImplementedException();
+ }
}
public class SpacebarClientGuild(AuthenticatedSpacebarClient client, long guildId) {
@@ -171,6 +176,7 @@ public class AuthenticatedSpacebarGatewayClient(ILogger()!.HeartbeatInterval).ContinueWith(ct => {
+ _ = _runHeartbeatLoop(msg.GetData()!.HeartbeatInterval, _cts.Token).ContinueWith(ct => {
logger.LogWarning("Heartbeat loop exited!");
if (ct.IsFaulted) throw ct.Exception;
});
@@ -226,7 +239,7 @@ public class AuthenticatedSpacebarGatewayClient(ILogger _runReceiveLoop() {
+ private async IAsyncEnumerable _runReceiveLoop([EnumeratorCancellation] CancellationToken ct) {
List messageParts = [];
List<(string Name, TimeSpan Elapsed)> trace = [];
var buffer = new byte[ReceiveBufferSize];
int idx = 0;
- while (RawClientWebSocket.State < WebSocketState.Closed) {
+ while (RawClientWebSocket.State < WebSocketState.CloseSent) {
var sw = Stopwatch.StartNew();
- var msg = await RawClientWebSocket.ReceiveAsync(buffer, CancellationToken.None);
+ WebSocketReceiveResult msg;
+ try {
+ msg = await RawClientWebSocket.ReceiveAsync(buffer, ct);
+ }
+ catch (TaskCanceledException) {
+ yield break;
+ }
+ catch (InvalidOperationException) {
+ yield break;
+ }
+
trace.Add(($"RCV.{idx}", sw.GetElapsedAndRestart()));
Console.WriteLine($"Websocket message chunk read: {msg.MessageType} {msg.Count} {msg.EndOfMessage}");