fix(headless): route wire-only chat commands
This commit is contained in:
parent
41b15efd4d
commit
259f0e5ac3
3 changed files with 219 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue