The Launcher's XP11 port (8730b9b) now extends to everything: one net40
flavor across Console, vPOD, Contract, and SecureConfig (Newtonsoft.Json
everywhere; the net48/System.Text.Json legs and their #if splits are gone
since nothing consumed them).
Console (net40, single TFM like the Launcher):
- The ~31 BinaryFormatter bitmap blobs in the .resx files became raw
embedded files under assets/icons/ (extracted byte-faithfully via a
serialization surrogate — the animated square_throbber.gif survives),
loaded by Properties.Resources.EmbeddedBitmap/EmbeddedIcon. Reason:
System.Resources.Extensions' DeserializingResourceReader is net461+
and cannot load on net40. Strings stay in the .resx.
- IReadOnlyList -> IList in AppRegistry (net45+ interface).
vPOD (net40, single TFM):
- Zip extraction now shares the Launcher's MiniZip.cs (linked source), so
the diff-test install round-trip exercises it against ZipArchive zips.
- RPC args as JTokens; LaunchApps.json persistence via Newtonsoft;
Thread.VolatileRead instead of Volatile.Read.
Contract/SecureConfig: net40-only; Client/** (PodManagerConnection) now
ships in the one build. The Launcher package gains
TeslaSecureConfiguration.dll as a dependency of the client half.
Tests: the net48 xunit host loads the net40 assemblies (both CLR4), so
the suite exercises exactly what ships — 106/106 green. Also verified
live: net40 console provisioned, managed, and ran a full RP mission
against net40 vPOD (beacon/passphrase/RSA, 53290 RPC, egg load,
Run/Stop Mission).
Version: 4.11.4.3 across Launcher, Console, and vPOD (vPOD joins the
suite version line; was 1.0.0). Ship the dotNetFx40 redistributable in
Launcher/assets for XP-era pods.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
156 lines
5.8 KiB
C#
156 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using Tesla.Net;
|
|
using Xunit;
|
|
|
|
namespace TeslaConsole.DiffTests
|
|
{
|
|
/// <summary>
|
|
/// Exercises the framed JSON RPC protocol (PodRpc) that replaced the OFB +
|
|
/// BinaryFormatter wire. Both the Console client (PodManagerConnection) and the
|
|
/// Launcher Service share this exact code, so an in-process round-trip
|
|
/// (encode request → decode → encode response → decode) validates the wire
|
|
/// contract both ends depend on. The live console↔pod path still needs a pod;
|
|
/// this covers the serialization, which is the part that can silently break.
|
|
/// </summary>
|
|
public class PodRpcProtocolTests
|
|
{
|
|
private static readonly Guid Key = new Guid("7D241B1F-AB6D-4e08-9C20-12294E743D94");
|
|
|
|
[Fact]
|
|
public void Request_RoundTrips_Method_And_Args()
|
|
{
|
|
var ms = new MemoryStream();
|
|
PodRpc.WriteRequest(ms, "KillApp", new object[] { Key, 4242 });
|
|
ms.Position = 0;
|
|
|
|
var req = PodRpc.ReadRequest(ms);
|
|
Assert.Equal("KillApp", req.Method);
|
|
Assert.Equal(2, req.Args.Count);
|
|
Assert.Equal(Key, req.Args[0].ToObject<Guid>(PodRpc.JsonOptions));
|
|
Assert.Equal(4242, req.Args[1].ToObject<int>(PodRpc.JsonOptions));
|
|
}
|
|
|
|
[Fact]
|
|
public void Request_With_No_Args_RoundTrips()
|
|
{
|
|
var ms = new MemoryStream();
|
|
PodRpc.WriteRequest(ms, "FullUpdate", null);
|
|
ms.Position = 0;
|
|
|
|
var req = PodRpc.ReadRequest(ms);
|
|
Assert.Equal("FullUpdate", req.Method);
|
|
Assert.Empty(req.Args);
|
|
}
|
|
|
|
[Fact]
|
|
public void Two_Frames_Read_Back_To_Back()
|
|
{
|
|
var ms = new MemoryStream();
|
|
PodRpc.WriteRequest(ms, "Ping", new object[] { new DateTime(2026, 6, 30, 12, 0, 0, DateTimeKind.Utc) });
|
|
PodRpc.WriteRequest(ms, "KillAllApps", null);
|
|
ms.Position = 0;
|
|
|
|
Assert.Equal("Ping", PodRpc.ReadRequest(ms).Method);
|
|
Assert.Equal("KillAllApps", PodRpc.ReadRequest(ms).Method);
|
|
}
|
|
|
|
[Fact]
|
|
public void Response_Error_RoundTrips()
|
|
{
|
|
var ms = new MemoryStream();
|
|
PodRpc.WriteResponse(ms, null, "boom on the pod");
|
|
ms.Position = 0;
|
|
|
|
var resp = PodRpc.ReadResponse(ms);
|
|
Assert.Equal("boom on the pod", resp.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Response_Primitive_Results_RoundTrip()
|
|
{
|
|
Assert.Equal(99, RoundTripResult<int>(99));
|
|
Assert.Equal(Key, RoundTripResult<Guid>(Key));
|
|
Assert.Equal(0.5f, RoundTripResult<float>(0.5f));
|
|
Assert.Equal(new DateTime(2026, 6, 30), RoundTripResult<DateTime>(new DateTime(2026, 6, 30)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Response_LaunchData_Array_RoundTrips_With_Fields()
|
|
{
|
|
var apps = new[]
|
|
{
|
|
new LaunchData
|
|
{
|
|
LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "Red Planet" },
|
|
WorkingDirectory = @"C:\Games\RedPlanet",
|
|
ExeFile = "RedPlanet.exe",
|
|
Arguments = "-res 1024 768",
|
|
AutoRestart = true,
|
|
},
|
|
};
|
|
|
|
var got = RoundTripResult<LaunchData[]>(apps);
|
|
Assert.Single(got);
|
|
Assert.Equal(Key, got[0].LaunchPair.LaunchKey);
|
|
Assert.Equal("Red Planet", got[0].LaunchPair.DisplayName);
|
|
Assert.Equal(@"C:\Games\RedPlanet", got[0].WorkingDirectory);
|
|
Assert.Equal("RedPlanet.exe", got[0].ExeFile);
|
|
Assert.Equal("-res 1024 768", got[0].Arguments);
|
|
Assert.True(got[0].AutoRestart);
|
|
}
|
|
|
|
[Fact]
|
|
public void Response_FullUpdateData_RoundTrips()
|
|
{
|
|
var full = new FullUpdateData
|
|
{
|
|
InstalledApps = new[]
|
|
{
|
|
new LaunchData { LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "RP" } },
|
|
},
|
|
LaunchedApps = new[] { new LaunchedAppData { LaunchKey = Key, ProcessId = 1234 } },
|
|
VolumeLevel = 0.75f,
|
|
};
|
|
|
|
var got = RoundTripResult<FullUpdateData>(full);
|
|
Assert.Equal(0.75f, got.VolumeLevel);
|
|
Assert.Equal("RP", got.InstalledApps[0].LaunchPair.DisplayName);
|
|
Assert.Equal(1234, got.LaunchedApps[0].ProcessId);
|
|
Assert.Equal(Key, got.LaunchedApps[0].LaunchKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void Response_OutOfBandProgress_RoundTrips()
|
|
{
|
|
var p = new OutOfBandProgress { PercentComplete = 73, Status = "Extracting...", IsCompleted = false };
|
|
var got = RoundTripResult<OutOfBandProgress>(p);
|
|
Assert.Equal(73, got.PercentComplete);
|
|
Assert.Equal("Extracting...", got.Status);
|
|
Assert.False(got.IsCompleted);
|
|
}
|
|
|
|
[Fact]
|
|
public void Oversized_Frame_Length_Is_Rejected()
|
|
{
|
|
var ms = new MemoryStream();
|
|
// Craft a frame header claiming more than MaxFrameBytes.
|
|
ms.Write(BitConverter.GetBytes(PodRpc.MaxFrameBytes + 1), 0, 4);
|
|
ms.Position = 0;
|
|
Assert.Throws<IOException>(() => PodRpc.ReadFrame(ms));
|
|
}
|
|
|
|
// Serialize a result the way the Launcher does, read it the way the Console does.
|
|
private static T RoundTripResult<T>(object result)
|
|
{
|
|
var ms = new MemoryStream();
|
|
PodRpc.WriteResponse(ms, result, null);
|
|
ms.Position = 0;
|
|
var resp = PodRpc.ReadResponse(ms);
|
|
Assert.Null(resp.Error);
|
|
return resp.Result.ToObject<T>(PodRpc.JsonOptions);
|
|
}
|
|
}
|
|
}
|