using System.Text.Json; using AcDream.App.Diagnostics; using AcDream.App.Rendering; using AcDream.App.Streaming; using AcDream.App.UI.Testing; using SixLabors.ImageSharp; namespace AcDream.App.Tests.Diagnostics; public sealed class WorldLifecycleAutomationControllerTests { private static readonly RenderFrameInput FrameInput = new(1d / 60d, 1280, 720); private static readonly RenderFrameOutcome FrameOutcome = new( new WorldRenderFrameOutcome(4, 17, NormalWorldDrawn: true), new PrivatePresentationFrameOutcome( PortalViewportDrawn: false, ScreenshotCaptured: true)); [Fact] public void ScreenshotCapture_FlipsGlRowsAndWritesACompletePng() { string directory = NewDirectory(); // GL order: bottom row red/green, top row blue/white. byte[] rgba = [ 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, ]; var logs = new List(); var controller = new FrameScreenshotController( (_, _) => rgba, directory, logs.Add); try { Assert.True(controller.TryRequest("login_stable", out string error), error); Assert.False(controller.IsComplete("login_stable")); Assert.True(controller.CapturePending(2, 2)); Assert.True(controller.IsComplete("login_stable")); string path = Path.Combine(directory, "login_stable.png"); using Image image = Image.Load(path); Assert.Equal(2, image.Width); Assert.Equal(2, image.Height); Assert.Contains(logs, line => line.Contains("screenshot-complete", StringComparison.Ordinal)); byte[] flipped = FrameScreenshotController.FlipRows(rgba, 2, 2); Assert.Equal(rgba.AsSpan(8, 8).ToArray(), flipped.AsSpan(0, 8).ToArray()); Assert.Equal(rgba.AsSpan(0, 8).ToArray(), flipped.AsSpan(8, 8).ToArray()); } finally { Directory.Delete(directory, recursive: true); } } [Fact] public void ScreenshotCapture_ReportsNoWorkAndFailedCapture() { string directory = NewDirectory(); var controller = new FrameScreenshotController( (_, _) => [], directory); try { Assert.False(controller.CapturePending(0, 0)); Assert.True(controller.TryRequest("bad_frame", out string error), error); Assert.False(controller.CapturePending(0, 0)); Assert.False(controller.IsComplete("bad_frame")); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); } } [Theory] [InlineData("")] [InlineData("../escape")] [InlineData("has spaces")] [InlineData("name.png")] public void ArtifactNames_RejectPathsAndAmbiguousCharacters(string name) { Assert.False(AutomationArtifactName.TryValidate(name, out string error)); Assert.NotEmpty(error); } [Fact] public void Checkpoint_WritesCanonicalRevealAndResourceSnapshot() { string directory = NewDirectory(); var reveal = new WorldRevealLifecycleSnapshot( Generation: 7, Kind: WorldRevealKind.Portal, Readiness: new WorldRevealReadinessSnapshot( 0x8A020164u, IsIndoor: true, IsUnhydratable: false, RequiredRenderRadius: 0, IsRenderNeighborhoodReady: true, AreCompositeTexturesReady: true, IsCollisionReady: true), Materialized: true, Completed: true, Cancelled: false, WorldViewportObserved: true, InvariantFailureCount: 0); var resources = EmptyResources() with { LoadedLandblocks = 1, WorldEntities = 42, TrackedGpuBytes = 1234, }; var screenshots = new FrameScreenshotController( (_, _) => [0, 0, 0, 255], Path.Combine(directory, "screenshots")); var controller = new WorldLifecycleAutomationController( () => reveal, () => 3, _ => resources, screenshots, directory); try { Assert.True(controller.IsWorldReady); Assert.True(controller.IsWorldViewportVisible); Assert.Equal(3, controller.PortalMaterializationCount); Assert.True(controller.TryRequestCheckpoint( "dungeon", out IRetailUiAutomationCheckpoint? request, out string error), error); Assert.NotNull(request); Assert.Equal(RetailUiAutomationCheckpointStatus.Pending, request.Status); controller.Process(FrameInput, FrameOutcome); Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, request.Status); string line = Assert.Single(File.ReadAllLines( Path.Combine(directory, "world-lifecycle.checkpoints.jsonl"))); using JsonDocument json = JsonDocument.Parse(line); Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString()); Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64()); Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world") .GetProperty("visibleLandblocks").GetInt32()); Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation") .GetProperty("screenshotCaptured").GetBoolean()); Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32()); Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json"))); } finally { Directory.Delete(directory, recursive: true); } } [Fact] public void CheckpointRequests_DrainInFifoOrderOnce() { string directory = NewDirectory(); int captures = 0; var controller = CreateController( directory, _ => { captures++; return EmptyResources(); }); try { Assert.True(controller.TryRequestCheckpoint( "first", out IRetailUiAutomationCheckpoint? first, out _)); Assert.True(controller.TryRequestCheckpoint( "second", out IRetailUiAutomationCheckpoint? second, out _)); controller.Process(FrameInput, FrameOutcome); controller.Process(FrameInput, FrameOutcome); Assert.NotNull(first); Assert.NotNull(second); Assert.Equal(1, first.Sequence); Assert.Equal(2, second.Sequence); Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, first.Status); Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, second.Status); Assert.Equal(2, captures); string[] lines = File.ReadAllLines( Path.Combine(directory, "world-lifecycle.checkpoints.jsonl")); Assert.Equal(2, lines.Length); Assert.Contains("\"name\":\"first\"", lines[0], StringComparison.Ordinal); Assert.Contains("\"name\":\"second\"", lines[1], StringComparison.Ordinal); } finally { controller.Dispose(); Directory.Delete(directory, recursive: true); } } [Fact] public void CheckpointWriteFailure_IsReportedOnTokenWithoutThrowingFromFrame() { string directory = NewDirectory(); string blockedPath = Path.Combine(directory, "not-a-directory"); File.WriteAllText(blockedPath, "occupied"); var controller = CreateController(blockedPath, _ => EmptyResources()); try { Assert.True(controller.TryRequestCheckpoint( "failed", out IRetailUiAutomationCheckpoint? request, out _)); controller.Process(FrameInput, FrameOutcome); Assert.NotNull(request); Assert.Equal(RetailUiAutomationCheckpointStatus.Failed, request.Status); Assert.Contains("checkpoint 'failed' sequence 1 failed", request.Error); } finally { controller.Dispose(); Directory.Delete(directory, recursive: true); } } [Fact] public void CheckpointCancellationAndShutdown_AreExplicitAndNeverWritten() { string directory = NewDirectory(); var controller = CreateController(directory, _ => EmptyResources()); try { Assert.True(controller.TryRequestCheckpoint( "caller", out IRetailUiAutomationCheckpoint? caller, out _)); Assert.True(controller.TryRequestCheckpoint( "shutdown", out IRetailUiAutomationCheckpoint? shutdown, out _)); Assert.NotNull(caller); Assert.NotNull(shutdown); controller.CancelCheckpoint(caller); controller.Dispose(); controller.Process(FrameInput, FrameOutcome); Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, caller.Status); Assert.Contains("cancelled before render-frame capture", caller.Error); Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, shutdown.Status); Assert.Contains("cancelled during world lifecycle automation shutdown", shutdown.Error); Assert.False(controller.TryRequestCheckpoint( "late", out IRetailUiAutomationCheckpoint? late, out string error)); Assert.Null(late); Assert.Contains("shutting down", error); Assert.False(File.Exists(Path.Combine( directory, "world-lifecycle.checkpoints.jsonl"))); } finally { controller.Dispose(); Directory.Delete(directory, recursive: true); } } private static WorldLifecycleAutomationController CreateController( string directory, Func capture) => new( () => default, () => 0, capture, new FrameScreenshotController((_, _) => [], directory), directory); private static WorldLifecycleResourceSnapshot EmptyResources() => new( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0L, 0, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L, 0d, 0d, null); private static string NewDirectory() { string path = Path.Combine(Path.GetTempPath(), "acdream-world-gate-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } }