fix(launcher): close Campaign LA LA1 review findings
This commit is contained in:
parent
4edc122085
commit
d511e4c348
12 changed files with 413 additions and 65 deletions
|
|
@ -19,12 +19,18 @@ namespace AcDream.App.Tests.Configuration;
|
|||
public sealed class SessionConfigurationSharedFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
public void AppReaderAcceptsTheSharedFixtureAndParsesTheFiveNewFields()
|
||||
public void AppReaderAcceptsTheProductionShapedSharedFixture()
|
||||
{
|
||||
(SessionConfiguration configuration, SessionDescriptor session) =
|
||||
SessionConfigurationLoader.Load(SharedFixturePath());
|
||||
|
||||
Assert.Equal(1, configuration.Version);
|
||||
Assert.Equal(
|
||||
"shared-fixture-dats",
|
||||
configuration.Process?.Content?.DatDirectory);
|
||||
Assert.Equal(
|
||||
"shared-fixture-dats/acdream.pak",
|
||||
configuration.Process?.Content?.PreparedAssetPath);
|
||||
Assert.Equal("shared-fixture", session.Id);
|
||||
Assert.Equal("127.0.0.1", session.Endpoint.Host);
|
||||
Assert.Equal(9000, session.Endpoint.Port);
|
||||
|
|
@ -34,9 +40,9 @@ public sealed class SessionConfigurationSharedFixtureTests
|
|||
// the pinned contract's "parsed-and-ignored" clause.
|
||||
Assert.Equal("idle", session.Policy?.Id);
|
||||
Assert.Equal(
|
||||
SessionCredentialProviderKind.Environment,
|
||||
SessionCredentialProviderKind.StandardInput,
|
||||
session.Credential.Provider);
|
||||
Assert.Equal("SHARED_FIXTURE_PASSWORD", session.Credential.Reference);
|
||||
Assert.Equal("session", session.Credential.Reference);
|
||||
|
||||
Assert.Equal(["ExamplePlugin", "AnotherPlugin"], session.Plugins);
|
||||
Assert.Equal(
|
||||
|
|
|
|||
|
|
@ -87,6 +87,27 @@ public sealed class LinuxPlatformBoundaryTests
|
|||
Assert.Empty(offenders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimePlatformGuardHasOneDefinitionAndOneApprovedConsumer()
|
||||
{
|
||||
string app = AppSourceRoot();
|
||||
string[] files = Directory
|
||||
.EnumerateFiles(app, "*.cs", SearchOption.AllDirectories)
|
||||
.Where(path => File.ReadAllText(path).Contains(
|
||||
"RuntimePlatformGuard",
|
||||
StringComparison.Ordinal))
|
||||
.Select(path => Path.GetRelativePath(app, path).Replace('\\', '/'))
|
||||
.OrderBy(static path => path, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"Credentials/AppCredentialResolver.cs",
|
||||
"Platform/GraphicalHostPlatformServices.cs",
|
||||
],
|
||||
files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmokePluginCopyUsesRidAwarePortableBuildAndPublishPaths()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -234,6 +234,26 @@ public sealed class RuntimeOptionsTests
|
|||
Assert.Equal("testpassword", realValues.LivePass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordPrintMembersRedactsTheLivePassword()
|
||||
{
|
||||
RuntimeOptions options = RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new()
|
||||
{
|
||||
["ACDREAM_LIVE"] = "1",
|
||||
["ACDREAM_TEST_USER"] = "testaccount",
|
||||
["ACDREAM_TEST_PASS"] = "top-secret-value",
|
||||
}));
|
||||
|
||||
string printed = options.ToString();
|
||||
|
||||
Assert.DoesNotContain("top-secret-value", printed, StringComparison.Ordinal);
|
||||
Assert.Contains("LivePass = <redacted>", printed, StringComparison.Ordinal);
|
||||
Assert.Contains("LiveHost = 127.0.0.1", printed, StringComparison.Ordinal);
|
||||
Assert.Contains("HasLiveCredentials = True", printed, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasLiveCredentials_RequiresLiveModeAndBothUserAndPass()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -62,6 +62,73 @@ public sealed class HeadlessSessionHostTests
|
|||
Assert.DoesNotContain("AcDream.App", diagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA LA1 review fixes F3/F6: reconnect is a visible lifecycle
|
||||
/// replacement, so the retiring connection must publish disconnected
|
||||
/// before the new connection publishes connected. Its reason is distinct
|
||||
/// from the final host stop and from the terminal process outcome.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ReconnectPublishesDisconnectedBeforeTheSecondConnectedEdge()
|
||||
{
|
||||
string statusPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"acdream-headless-reconnect-status-{Guid.NewGuid():N}.jsonl");
|
||||
try
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var diagnosticsOutput = new StringWriter();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(statusFile: statusPath),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(diagnosticsOutput),
|
||||
operations);
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Reconnect().Status);
|
||||
host.Dispose();
|
||||
|
||||
JsonElement[] events = File.ReadAllLines(statusPath)
|
||||
.Select(static line => JsonDocument.Parse(line).RootElement.Clone())
|
||||
.ToArray();
|
||||
Assert.Equal(
|
||||
[
|
||||
"started", "connected", "characterList", "enteredWorld",
|
||||
"disconnected", "connected", "characterList", "enteredWorld",
|
||||
"disconnected", "exited",
|
||||
],
|
||||
events.Select(static item => item.GetProperty("e").GetString()));
|
||||
|
||||
JsonElement[] disconnected = events
|
||||
.Where(static item =>
|
||||
item.GetProperty("e").GetString() == "disconnected")
|
||||
.ToArray();
|
||||
Assert.Equal(2, disconnected.Length);
|
||||
Assert.Equal(
|
||||
"reconnect",
|
||||
disconnected[0].GetProperty("reason").GetString());
|
||||
Assert.Equal(
|
||||
"stopped",
|
||||
disconnected[1].GetProperty("reason").GetString());
|
||||
|
||||
JsonElement exited = events[^1];
|
||||
Assert.Equal(0, exited.GetProperty("code").GetInt32());
|
||||
Assert.Equal("graceful", exited.GetProperty("reason").GetString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(statusPath))
|
||||
File.Delete(statusPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA1: proves the status-event writer fires the
|
||||
/// pinned lifecycle vocabulary — started/connected/characterList/
|
||||
|
|
|
|||
|
|
@ -18,11 +18,17 @@ namespace AcDream.Headless.Tests;
|
|||
public sealed class SessionConfigurationSharedFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
public void HeadlessReaderAcceptsTheSharedFixtureAndParsesTheFiveNewFields()
|
||||
public void HeadlessReaderAcceptsTheProductionShapedSharedFixture()
|
||||
{
|
||||
HeadlessConfiguration configuration =
|
||||
HeadlessConfigurationLoader.Load(SharedFixturePath());
|
||||
|
||||
Assert.Equal(
|
||||
"shared-fixture-dats",
|
||||
configuration.Process.Content?.DatDirectory);
|
||||
Assert.Equal(
|
||||
"shared-fixture-dats/acdream.pak",
|
||||
configuration.Process.Content?.PreparedAssetPath);
|
||||
HeadlessSessionDescriptor session = Assert.Single(configuration.Sessions)!;
|
||||
Assert.Equal("shared-fixture", session.Id);
|
||||
Assert.Equal("127.0.0.1", session.Endpoint.Host);
|
||||
|
|
@ -31,9 +37,9 @@ public sealed class SessionConfigurationSharedFixtureTests
|
|||
Assert.Equal("SharedToon", session.Character.Name);
|
||||
Assert.Equal("idle", session.Policy.Id);
|
||||
Assert.Equal(
|
||||
HeadlessCredentialProviderKind.Environment,
|
||||
HeadlessCredentialProviderKind.StandardInput,
|
||||
session.Credential.Provider);
|
||||
Assert.Equal("SHARED_FIXTURE_PASSWORD", session.Credential.Reference);
|
||||
Assert.Equal("session", session.Credential.Reference);
|
||||
|
||||
Assert.Equal(["ExamplePlugin", "AnotherPlugin"], session.Plugins);
|
||||
Assert.Equal(
|
||||
|
|
|
|||
|
|
@ -97,6 +97,63 @@ public sealed class SessionStatusWriterTests
|
|||
writer.Started("s1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F3: both hosts share this writer but have separate reconnect command
|
||||
/// adapters. The sink therefore closes an open connection before it
|
||||
/// accepts another connected edge, preserving a coherent external
|
||||
/// lifecycle even if a host has no reconnect-specific status hook.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SecondConnectedEdgeFirstClosesTheRetiringConnection()
|
||||
{
|
||||
using TemporaryFile file = TemporaryFile.Create();
|
||||
var writer = new SessionStatusWriter(file.Path);
|
||||
|
||||
writer.Connected("s1");
|
||||
writer.Connected("s1");
|
||||
|
||||
JsonElement[] events = File.ReadAllLines(file.Path)
|
||||
.Select(Parse)
|
||||
.ToArray();
|
||||
Assert.Equal(
|
||||
["connected", "disconnected", "connected"],
|
||||
events.Select(static item => item.GetProperty("e").GetString()));
|
||||
Assert.Equal(
|
||||
"reconnect",
|
||||
events[1].GetProperty("reason").GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F6: exited is a terminal fact, not an append request. Repeated host
|
||||
/// disposal and any late callback after disposal must not create a second
|
||||
/// terminal edge or resurrect the stream. If a host exits while still
|
||||
/// connected, the writer closes that connection first with a distinct,
|
||||
/// truthful reason.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExitedIsIdempotentTerminalAndClosesAnOpenConnection()
|
||||
{
|
||||
using TemporaryFile file = TemporaryFile.Create();
|
||||
var writer = new SessionStatusWriter(file.Path);
|
||||
|
||||
writer.Connected("s1");
|
||||
writer.Exited("s1", 0, "graceful");
|
||||
writer.Exited("s1", 1, "duplicate-must-not-win");
|
||||
writer.Connected("s1");
|
||||
|
||||
JsonElement[] events = File.ReadAllLines(file.Path)
|
||||
.Select(Parse)
|
||||
.ToArray();
|
||||
Assert.Equal(
|
||||
["connected", "disconnected", "exited"],
|
||||
events.Select(static item => item.GetProperty("e").GetString()));
|
||||
Assert.Equal(
|
||||
"process-exit",
|
||||
events[1].GetProperty("reason").GetString());
|
||||
Assert.Equal(0, events[2].GetProperty("code").GetInt32());
|
||||
Assert.Equal("graceful", events[2].GetProperty("reason").GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F7 (Campaign LA LA1 review fix round): replaces the earlier
|
||||
/// "DoesNotContain 'hunter2'/'password'" assertion, which could never
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue