using System.Net; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Headless.Configuration; using AcDream.Headless.Credentials; using AcDream.Headless.Diagnostics; using AcDream.Headless.Hosting; using AcDream.Headless.Platform; using AcDream.Runtime; using AcDream.Runtime.Session; namespace AcDream.Headless.Tests; public sealed class HeadlessSessionHostTests { [Fact] public void SingleSessionStartsReconnectsAndConvergesWithoutPresentation() { var operations = new FixtureSessionOperations(); using var diagnosticsOutput = new StringWriter(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(diagnosticsOutput), operations); RuntimeSessionStartResult first = host.Start(); ulong firstGeneration = host.Runtime.Generation.Value; host.Tick(0.015d); RuntimeSessionStartResult second = host.Reconnect(); Assert.Equal(RuntimeSessionStartStatus.Connected, first.Status); Assert.Equal(RuntimeSessionStartStatus.Connected, second.Status); Assert.Equal(0x50000002u, first.CharacterId); Assert.Equal("Headless", host.ActiveCharacterName); Assert.True(host.Runtime.Session.IsInWorld); Assert.True(host.Runtime.Generation.Value > firstGeneration); Assert.Equal(2, operations.CreatedSessionCount); Assert.Equal(1, operations.DisposedSessionCount); host.Dispose(); Assert.Equal(2, operations.DisposedSessionCount); Assert.True(host.Runtime.CaptureOwnership().IsConverged); Assert.True(credential.IsDisposed); string diagnostics = diagnosticsOutput.ToString(); Assert.Contains("\"state\":\"start-result\"", diagnostics); Assert.DoesNotContain("password", diagnostics); Assert.DoesNotContain("AcDream.App", diagnostics); } [Fact] public async Task ProcessHostRunsUntilCancellationAndReturnsStableExitCode() { var configuration = new HeadlessConfiguration { Version = 1, Sessions = [ Descriptor( HeadlessCredentialProviderKind.StandardInput, "stdin-bot"), ], }; HeadlessPathSet paths = HeadlessPathSet.Resolve( new HeadlessPathOverrides()); using var diagnostics = new StringWriter(); var operations = new FixtureSessionOperations(); using var host = new HeadlessProcessHost( configuration, paths, new StringReader("process-password" + Environment.NewLine), diagnostics, operations); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); HeadlessExitCode result = await host.RunAsync(cancellation.Token); Assert.Equal(HeadlessExitCode.Success, result); Assert.True(host.Session.Runtime.Session.IsInWorld); Assert.DoesNotContain( "process-password", diagnostics.ToString()); } [Fact] public void TeardownRetriesOnlyTheUnfinishedSuffix() { var operations = new FixtureSessionOperations(); var writer = new FailOnceTextWriter(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(writer), operations); Assert.Equal( RuntimeSessionStartStatus.Connected, host.Start().Status); writer.FailNextWrite = true; Assert.Throws(host.Dispose); Assert.Equal(1, operations.DisposedSessionCount); host.Dispose(); Assert.Equal(1, operations.DisposedSessionCount); Assert.True(host.Runtime.CaptureOwnership().IsConverged); } private static HeadlessSessionDescriptor Descriptor( HeadlessCredentialProviderKind provider = HeadlessCredentialProviderKind.Environment, string credentialReference = "BOT_PASSWORD") => new() { Id = "bot", Endpoint = new HeadlessEndpointDescriptor { Host = "127.0.0.1", Port = 9000, }, Account = "account", Character = new HeadlessCharacterSelector { Name = "headless", }, Policy = new HeadlessBotPolicyDescriptor { Id = "idle", }, Credential = new HeadlessCredentialReference { Provider = provider, Reference = credentialReference, }, }; private sealed class FixtureSessionOperations : ILiveSessionOperations { public int CreatedSessionCount { get; private set; } public int DisposedSessionCount { get; private set; } public IPEndPoint ResolveEndpoint(string host, int port) => new(IPAddress.Loopback, port); public WorldSession CreateSession(IPEndPoint endpoint) { CreatedSessionCount++; return new WorldSession(endpoint); } public void Connect( WorldSession session, string user, string password) { } public CharacterList.Parsed GetCharacters( WorldSession session) => new( 0u, [ new CharacterList.Character( 0x50000001u, "Other", 0u), new CharacterList.Character( 0x50000002u, "Headless", 0u), ], [], 11, "account", true, true); public void EnterWorld( WorldSession session, int activeCharacterIndex) { } public void Tick(WorldSession session) { } public void DisposeSession(WorldSession session) { DisposedSessionCount++; session.Dispose(); } } private sealed class FailOnceTextWriter : StringWriter { public bool FailNextWrite { get; set; } public override void WriteLine(string? value) { if (FailNextWrite) { FailNextWrite = false; throw new IOException("fixture write failure"); } base.WriteLine(value); } } }