Merge nucleic/warm-north-vole-pf37 into dev

This commit is contained in:
2026-08-04 02:46:53 -07:00
parent c34eb1ff61
commit f5ee895d3f
18 changed files with 1515 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
using System.Runtime.InteropServices;
using NucleicProtocol.Interop;
namespace NucleicApp.Tests;
internal sealed class FakeNativeProtocol : INativeProtocol
{
private NativeProtocolCallback? eventCallback;
private NativeProtocolCallback? stateCallback;
private nint context;
public string? ConfigurationJson { get; private set; }
public string? RendezvousJson { get; private set; }
public List<string> Intents { get; } = [];
public bool Closed { get; private set; }
public string ProjectionResult { get; set; } = "{\"splices\":[],\"version\":1}";
public nint ClientCreate(string configurationJson)
{
ConfigurationJson = configurationJson;
return 11;
}
public void ClientSetCallbacks(
nint handle, NativeProtocolCallback? eventCallback,
NativeProtocolCallback? stateCallback, nint context)
{
this.eventCallback = eventCallback;
this.stateCallback = stateCallback;
this.context = context;
}
public int ClientConnectLocal(nint handle, string rendezvousJson)
{
RendezvousJson = rendezvousJson;
return 0;
}
public int ClientConnectRemote(nint handle, string endpointJson) => 0;
public int ClientPair(nint handle, string pairingCode) => 0;
public int ClientSendIntent(nint handle, string clientMessageJson)
{
Intents.Add(clientMessageJson);
return 0;
}
public void ClientClose(nint handle) => Closed = true;
public nint ProjectionCreate() => 22;
public nint ProjectionApply(nint projection, string hostMessageJson) =>
Marshal.StringToCoTaskMemUTF8(ProjectionResult);
public void ProjectionFree(nint projection) { }
public string CopyAndFreeString(nint value)
{
try { return Marshal.PtrToStringUTF8(value)!; }
finally { Marshal.FreeCoTaskMem(value); }
}
public void EmitEvent(string json) => Emit(eventCallback, json);
public void EmitState(string json) => Emit(stateCallback, json);
private void Emit(NativeProtocolCallback? callback, string json)
{
var pointer = Marshal.StringToCoTaskMemUTF8(json);
try { callback?.Invoke(context, pointer); }
finally { Marshal.FreeCoTaskMem(pointer); }
}
}
internal sealed class FakeProtocolClient : IProtocolClient
{
public event Action<string>? MessageJsonReceived;
public event Action<string>? StateJsonReceived;
public HostdRendezvous? Rendezvous { get; private set; }
public List<string> Intents { get; } = [];
public Queue<ProtocolResult> ConnectLocalResults { get; } = [];
public int ConnectLocalCount { get; private set; }
public bool Disposed { get; private set; }
public ProtocolResult ConnectLocal(HostdRendezvous rendezvous)
{
ConnectLocalCount++;
Rendezvous = rendezvous;
return ConnectLocalResults.TryDequeue(out var result) ? result : ProtocolResult.Ok;
}
public ProtocolResult ConnectRemote(string endpointJson) => ProtocolResult.Ok;
public ProtocolResult Pair(string pairingCode) => ProtocolResult.Ok;
public ProtocolResult SendIntent(string clientMessageJson)
{
Intents.Add(clientMessageJson);
return ProtocolResult.Ok;
}
public void Dispose() => Disposed = true;
public void EmitMessage(string json) => MessageJsonReceived?.Invoke(json);
public void EmitState(string json) => StateJsonReceived?.Invoke(json);
}
internal sealed class FakeProtocolClientFactory(FakeProtocolClient client) : IProtocolClientFactory
{
public ProtocolClientConfiguration? Configuration { get; private set; }
public IProtocolClient Create(ProtocolClientConfiguration configuration)
{
Configuration = configuration;
return client;
}
}
internal static class TestRepository
{
public static string Root
{
get
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (Directory.Exists(Path.Combine(current.FullName, "fixtures", "protocol-abi")))
return current.FullName;
current = current.Parent;
}
throw new DirectoryNotFoundException("could not locate repository fixtures");
}
}
public static string Fixture(string side, string name) => File.ReadAllText(
Path.Combine(Root, "fixtures", "protocol-abi", "v1", side, name + ".json"));
}
+22
View File
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../NucleicApp/NucleicApp.csproj" />
<ProjectReference Include="../NucleicProtocol.Interop/NucleicProtocol.Interop.csproj" />
</ItemGroup>
</Project>
+84
View File
@@ -0,0 +1,84 @@
using System.Text.Json;
using NucleicProtocol.Interop;
namespace NucleicApp.Tests;
public sealed class ProtocolInteropTests
{
[Fact]
public void EveryGoldenJsonFixtureHasTheFilenameDiscriminator()
{
var root = Path.Combine(TestRepository.Root, "fixtures", "protocol-abi", "v1");
var clients = Directory.GetFiles(Path.Combine(root, "client"), "*.json");
var hosts = Directory.GetFiles(Path.Combine(root, "host"), "*.json");
Assert.Equal(65, clients.Length);
Assert.Equal(52, hosts.Length);
foreach (var path in clients.Concat(hosts))
{
var expected = Path.GetFileNameWithoutExtension(path);
var message = ProtocolMessage.Parse(File.ReadAllText(path));
Assert.Equal(expected, message.Tag);
}
}
[Fact]
public void ManagedClientPinsCallbackContextUntilNativeCloseReturns()
{
var native = new FakeNativeProtocol();
var messages = new List<string>();
using (var client = new ProtocolClient(new ProtocolClientConfiguration
{
IdentityDirectory = @"C:\Users\me\AppData\Roaming\Nucleic-local\renderer",
Channel = "local",
}, native))
{
client.MessageJsonReceived += messages.Add;
native.EmitEvent("{\"t\":\"pong\"}");
var result = client.ConnectLocal(ValidRendezvous());
Assert.Equal(ProtocolResult.Ok, result);
Assert.Contains("\"hostID\":\"host-1\"", native.RendezvousJson);
}
Assert.True(native.Closed);
Assert.Single(messages, "{\"t\":\"pong\"}");
}
[Fact]
public void ProjectionCopiesThenFreesNativeString()
{
var native = new FakeNativeProtocol
{
ProjectionResult = "{\"sessionID\":\"s1\",\"splices\":[],\"version\":1}",
};
using var projection = new ProtocolProjection(native);
var diff = projection.Apply("{\"t\":\"pong\"}");
Assert.Equal("s1", diff.Root.GetProperty("sessionID").GetString());
}
[Fact]
public void IntentHelpersMatchFrozenWireShapes()
{
AssertJsonEqual(TestRepository.Fixture("client", "listSessions"), ClientIntents.ListSessions());
AssertJsonEqual(TestRepository.Fixture("client", "unsubscribe"), ClientIntents.Unsubscribe("session-1"));
var expectedSend = "{\"input\":{\"parts\":[{\"text\":\"continue\",\"type\":\"text\"}]},\"sessionID\":\"session-1\",\"t\":\"sendInput\"}";
AssertJsonEqual(expectedSend, ClientIntents.SendInput("session-1", "continue"));
}
internal static HostdRendezvous ValidRendezvous() => new()
{
ProcessId = Environment.ProcessId,
Host = "127.0.0.1",
Port = 51900,
LocalPsk = Convert.ToBase64String(new byte[32]),
HostId = "host-1",
};
private static void AssertJsonEqual(string expected, string actual)
{
using var expectedDocument = JsonDocument.Parse(expected);
using var actualDocument = JsonDocument.Parse(actual);
Assert.True(JsonElement.DeepEquals(expectedDocument.RootElement, actualDocument.RootElement),
$"expected {expected}\nactual {actual}");
}
}
+120
View File
@@ -0,0 +1,120 @@
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using NucleicApp.Services;
using NucleicProtocol.Interop;
namespace NucleicApp.Tests;
public sealed class RendererCoreTests
{
[Fact]
public void StoreProjectsSessionsApprovalsAndSettingsFromHostMessagesOnly()
{
using var store = new RendererStore();
store.Apply(TestRepository.Fixture("host", "sessionList"));
store.Apply(TestRepository.Fixture("host", "approvalRequested"));
store.Apply(TestRepository.Fixture("host", "dashboard"));
store.Apply(TestRepository.Fixture("host", "settings"));
var session = Assert.Single(store.Sessions);
Assert.Equal("session-1", session.Id);
Assert.Equal("awaitingApproval", session.Status);
Assert.Equal(1, session.PendingApprovalCount);
Assert.Equal("approval-1", Assert.Single(store.PendingApprovals).Id);
Assert.NotNull(store.Dashboard);
Assert.True(store.Settings!.Value.GetProperty("resolveApprovalsViaCloud").GetBoolean());
store.Apply(TestRepository.Fixture("host", "approvalResolved"));
Assert.Empty(store.PendingApprovals);
}
[Fact]
public async Task HostConnectionReadsRendezvousAndRequestsInitialStateOnReady()
{
var directory = Directory.CreateTempSubdirectory("nucleic-renderer-");
try
{
var rendezvousPath = Path.Combine(directory.FullName, "hostd.json");
await File.WriteAllTextAsync(rendezvousPath, JsonSerializer.Serialize(
ProtocolInteropTests.ValidRendezvous()));
using var store = new RendererStore();
var client = new FakeProtocolClient();
var factory = new FakeProtocolClientFactory(client);
await using var connection = new HostConnection(
store, Path.Combine(directory.FullName, "identity"), factory,
InlineRendererDispatcher.Instance);
await connection.StartLocalAsync(rendezvousPath);
Assert.Equal("host-1", client.Rendezvous?.HostId);
client.EmitState("{\"state\":\"ready\",\"hostID\":\"host-1\",\"welcome\":{}}");
Assert.Equal(HostConnectionState.Connected, connection.State);
var tags = client.Intents.Select(json => ProtocolMessage.Parse(json).Tag).ToArray();
Assert.Equal(["listSessions", "listDashboard", "listPeers"], tags);
client.EmitMessage(TestRepository.Fixture("host", "sessionUpdated"));
Assert.Equal("session-1", Assert.Single(store.Sessions).Id);
}
finally { directory.Delete(recursive: true); }
}
[Fact]
public async Task HostConnectionKeepsRetryingAfterSynchronousAdmissionFailures()
{
var directory = Directory.CreateTempSubdirectory("nucleic-reconnect-");
try
{
var rendezvousPath = Path.Combine(directory.FullName, "hostd.json");
await File.WriteAllTextAsync(rendezvousPath, JsonSerializer.Serialize(
ProtocolInteropTests.ValidRendezvous()));
using var store = new RendererStore();
var client = new FakeProtocolClient();
client.ConnectLocalResults.Enqueue(ProtocolResult.InvalidState);
client.ConnectLocalResults.Enqueue(ProtocolResult.InvalidState);
client.ConnectLocalResults.Enqueue(ProtocolResult.Ok);
await using var connection = new HostConnection(
store, Path.Combine(directory.FullName, "identity"),
new FakeProtocolClientFactory(client), InlineRendererDispatcher.Instance);
await connection.StartLocalAsync(rendezvousPath);
await WaitUntilAsync(() => client.ConnectLocalCount == 3, TimeSpan.FromSeconds(3));
Assert.Equal(3, client.ConnectLocalCount);
Assert.Equal(HostConnectionState.Reconnecting, connection.State);
}
finally { directory.Delete(recursive: true); }
}
[Fact]
public async Task LauncherAcceptsOnlyALiveLoopbackRendezvous()
{
var directory = Directory.CreateTempSubdirectory("nucleic-launcher-");
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
try
{
var record = ProtocolInteropTests.ValidRendezvous() with
{
Port = ((IPEndPoint)listener.LocalEndpoint).Port,
};
var path = Path.Combine(directory.FullName, "hostd.json");
await File.WriteAllTextAsync(path, JsonSerializer.Serialize(record));
var launcher = new HostdLauncher(new HostdLaunchOptions(
Path.Combine(directory.FullName, "must-not-launch.exe"), path));
var result = await launcher.EnsureRunningAsync();
Assert.Equal(record.Port, result.Port);
}
finally
{
listener.Stop();
directory.Delete(recursive: true);
}
}
private static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
{
using var deadline = new CancellationTokenSource(timeout);
while (!condition()) await Task.Delay(10, deadline.Token);
}
}