fix(headless): route wire-only chat commands

This commit is contained in:
Erik 2026-08-14 20:47:06 +02:00
parent 41b15efd4d
commit 259f0e5ac3
3 changed files with 219 additions and 0 deletions

View file

@ -71,6 +71,127 @@ public sealed class HeadlessSessionHostTests
BinaryPrimitives.ReadUInt32LittleEndian(captured[3].AsSpan(12)));
}
[Fact]
public void LoginCommandsRouteWireOnlyClientCommandsWithExactPolarityAndOrder()
{
var captured = new List<byte[]>();
var operations = new FixtureSessionOperations
{
GameActionCapture = body => captured.Add(body),
};
using var diagnosticsOutput = new StringWriter();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
using var host = new HeadlessSessionHost(
Descriptor(
loginCommands:
[
"/permit add Aunt Agatha",
"@permit remove Lord Gnarly Beard",
"/chat on",
"/chat off",
"/notell on",
"/notell off",
],
loginCommandDelayMs: 0),
credential,
new HeadlessDiagnosticWriter(diagnosticsOutput),
operations);
Assert.Equal(RuntimeSessionStartStatus.Connected, host.Start().Status);
Assert.Equal(
[
ClientCommandRequests.AddPlayerPermissionOpcode,
ClientCommandRequests.RemovePlayerPermissionOpcode,
ClientCommandRequests.ModifyGlobalSquelchOpcode,
ClientCommandRequests.ModifyGlobalSquelchOpcode,
ClientCommandRequests.ModifyGlobalSquelchOpcode,
ClientCommandRequests.ModifyGlobalSquelchOpcode,
],
captured.Select(ActionOpcode));
Assert.Equal("Aunt Agatha", StringActionArgument(captured[0]));
Assert.Equal("Lord Gnarly Beard", StringActionArgument(captured[1]));
Assert.Equal(
[
(Add: 0u, MessageType: 2u),
(Add: 1u, MessageType: 2u),
(Add: 1u, MessageType: 3u),
(Add: 0u, MessageType: 3u),
],
captured.Skip(2).Select(static body => (
Add: BinaryPrimitives.ReadUInt32LittleEndian(
body.AsSpan(12, sizeof(uint))),
MessageType: BinaryPrimitives.ReadUInt32LittleEndian(
body.AsSpan(16, sizeof(uint))))));
}
[Theory]
[InlineData("/permit add")]
[InlineData("/chat maybe")]
[InlineData("/notell maybe")]
public void InvalidWireOnlyArgumentKeepsTypedFeedbackWithoutStatusFailure(
string invalidCommand)
{
string statusPath = Path.Combine(
Path.GetTempPath(),
$"acdream-headless-wire-client-errors-{Guid.NewGuid():N}.jsonl");
try
{
var captured = new List<byte[]>();
var operations = new FixtureSessionOperations
{
GameActionCapture = body => captured.Add(body),
};
using var diagnosticsOutput = new StringWriter();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
using var host = new HeadlessSessionHost(
Descriptor(
statusFile: statusPath,
loginCommands:
[
invalidCommand,
"after",
],
loginCommandDelayMs: 0),
credential,
new HeadlessDiagnosticWriter(diagnosticsOutput),
operations);
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
Assert.Single(captured);
Assert.Equal("after", TalkText(captured[0]));
// Invalid registered-command arguments are handled exactly as
// typed: retail's ClientLocal refusal reaches canonical Runtime
// feedback and is not misclassified as a transport failure.
host.Runtime.CommunicationOwner.SpewBox.Tick(0d);
Assert.Equal(
"That is not a valid command.",
Assert.Single(
host.Runtime.CommunicationOwner.SpewBox.Snapshot()).Text);
JsonElement[] events = File.ReadAllLines(statusPath)
.Select(static line =>
JsonDocument.Parse(line).RootElement.Clone())
.ToArray();
Assert.DoesNotContain(
events,
static item => item.GetProperty("e").GetString()
== "loginCommandFailed");
Assert.True(host.Runtime.Session.IsInWorld);
}
finally
{
if (File.Exists(statusPath))
File.Delete(statusPath);
}
}
[Fact]
public void LoginCommandFailuresAreVersionedOrderedAndSessionIsolated()
{
@ -3286,6 +3407,13 @@ public sealed class HeadlessSessionHostTests
return System.Text.Encoding.ASCII.GetString(body, 14, length);
}
private static string StringActionArgument(byte[] body)
{
ushort length = BinaryPrimitives.ReadUInt16LittleEndian(
body.AsSpan(12, sizeof(ushort)));
return System.Text.Encoding.ASCII.GetString(body, 14, length);
}
private sealed class ManualTimeProvider : TimeProvider
{
private long _timestamp;