test(app): add canonical connected soak snapshots

Make scripted lifecycle checkpoints acknowledged post-diagnostics render barriers, capture the exact frame outcome beside canonical resource ownership, and harden the nine-stop route with ordered same-location cache and lifetime gates without weakening process residency thresholds.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 20:01:06 +02:00
parent 2862622ba2
commit bca4148739
17 changed files with 995 additions and 52 deletions

View file

@ -55,7 +55,6 @@ internal sealed record FrameRootDependencies(
internal sealed record FrameRootResult(
UpdateFrameOrchestrator Update,
RenderFrameOrchestrator Render,
WorldLifecycleAutomationController? LifecycleAutomation,
FrameRootRuntimeBindings RuntimeBindings,
IDisposable FrameGraphPublication,
AcDream.App.Net.LiveSessionHost SessionHost);
@ -420,7 +419,10 @@ internal sealed class FrameRootCompositionPhase
artifactDirectory,
message => d.Log("[UI-PROBE] " + message));
bindings.Adopt(
"world lifecycle automation",
"world lifecycle automation owner",
lifecycleAutomation);
bindings.Adopt(
"world lifecycle automation binding",
interaction.LateBindings.Automation.Bind(
lifecycleAutomation));
}
@ -453,6 +455,8 @@ internal sealed class FrameRootCompositionPhase
worldSceneRenderer,
privatePresentation,
live.FrameDiagnostics,
(IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
?? NullRenderFramePostDiagnosticsPhase.Instance,
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
?? NullRenderFrameFailureRecovery.Instance);
Fault(FrameRootCompositionPoint.RenderRootCreated);
@ -509,7 +513,6 @@ internal sealed class FrameRootCompositionPhase
var result = new FrameRootResult(
updateFrame,
renderFrame,
lifecycleAutomation,
bindings,
frameGraphPublication,
session.SessionHost);

View file

@ -446,14 +446,25 @@ internal sealed class DeferredWorldLifecycleAutomationRuntime
return new ExpectedOwnerBinding<IRetailUiAutomationRuntime>(target, Release);
}
public bool TryWriteCheckpoint(string name, out string error)
public bool TryRequestCheckpoint(
string name,
out IRetailUiAutomationCheckpoint? checkpoint,
out string error)
{
if (!_deactivated && _target is { } target)
return target.TryWriteCheckpoint(name, out error);
return target.TryRequestCheckpoint(name, out checkpoint, out error);
checkpoint = null;
error = "world lifecycle automation is not bound";
return false;
}
public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint)
{
ArgumentNullException.ThrowIfNull(checkpoint);
if (!_deactivated && _target is { } target)
target.CancelCheckpoint(checkpoint);
}
public bool TryRequestScreenshot(string name, out string error)
{
if (!_deactivated && _target is { } target)

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
@ -71,14 +72,73 @@ internal sealed record WorldLifecycleCheckpoint(
DateTime TimestampUtc,
int ProcessId,
WorldRevealLifecycleSnapshot Reveal,
RenderFrameOutcome Render,
WorldLifecycleResourceSnapshot Resources);
internal sealed class WorldLifecycleCheckpointRequest :
IRetailUiAutomationCheckpoint
{
private readonly object _sync = new();
private RetailUiAutomationCheckpointStatus _status =
RetailUiAutomationCheckpointStatus.Pending;
private string? _error;
public WorldLifecycleCheckpointRequest(object owner, int sequence, string name)
{
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
Sequence = sequence;
Name = name ?? throw new ArgumentNullException(nameof(name));
}
internal object Owner { get; }
public int Sequence { get; }
public string Name { get; }
public RetailUiAutomationCheckpointStatus Status
{
get
{
lock (_sync)
return _status;
}
}
public string? Error
{
get
{
lock (_sync)
return _error;
}
}
internal bool TrySetTerminal(
RetailUiAutomationCheckpointStatus status,
string? error)
{
if (status == RetailUiAutomationCheckpointStatus.Pending)
throw new ArgumentOutOfRangeException(nameof(status));
lock (_sync)
{
if (_status != RetailUiAutomationCheckpointStatus.Pending)
return false;
_status = status;
_error = error;
return true;
}
}
}
/// <summary>
/// Diagnostic-only runtime seam used by production retained-UI scripts. It
/// writes one structured checkpoint at explicit script edges and delegates GL
/// capture to the render-thread screenshot owner.
/// </summary>
internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRuntime
internal sealed class WorldLifecycleAutomationController :
IRetailUiAutomationRuntime,
IRenderFramePostDiagnosticsPhase,
IDisposable
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
@ -87,16 +147,21 @@ internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRu
private readonly Func<WorldRevealLifecycleSnapshot> _getReveal;
private readonly Func<int> _getPortalMaterializationCount;
private readonly Func<WorldLifecycleResourceSnapshot> _captureResources;
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
_captureResources;
private readonly FrameScreenshotController _screenshots;
private readonly string _artifactDirectory;
private readonly Action<string> _log;
private readonly object _requestOwner = new();
private readonly object _sync = new();
private readonly Queue<WorldLifecycleCheckpointRequest> _requests = [];
private int _sequence;
private bool _disposed;
public WorldLifecycleAutomationController(
Func<WorldRevealLifecycleSnapshot> getReveal,
Func<int> getPortalMaterializationCount,
Func<WorldLifecycleResourceSnapshot> captureResources,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
FrameScreenshotController screenshots,
string artifactDirectory,
Action<string>? log = null)
@ -116,35 +181,127 @@ internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRu
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
public int PortalMaterializationCount => _getPortalMaterializationCount();
public bool TryWriteCheckpoint(string name, out string error)
public bool TryRequestCheckpoint(
string name,
out IRetailUiAutomationCheckpoint? checkpoint,
out string error)
{
checkpoint = null;
if (!AutomationArtifactName.TryValidate(name, out error))
return false;
lock (_sync)
{
if (_disposed)
{
error = $"checkpoint '{name}' was rejected because world lifecycle automation is shutting down";
return false;
}
var request = new WorldLifecycleCheckpointRequest(
_requestOwner,
checked(++_sequence),
name);
_requests.Enqueue(request);
checkpoint = request;
error = string.Empty;
return true;
}
}
public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint)
{
ArgumentNullException.ThrowIfNull(checkpoint);
if (checkpoint is not WorldLifecycleCheckpointRequest request
|| !ReferenceEquals(request.Owner, _requestOwner))
{
throw new ArgumentException(
"The checkpoint acknowledgement is not owned by this automation runtime.",
nameof(checkpoint));
}
lock (_sync)
{
request.TrySetTerminal(
RetailUiAutomationCheckpointStatus.Cancelled,
$"checkpoint '{request.Name}' was cancelled before render-frame capture");
}
}
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
{
lock (_sync)
{
if (_disposed)
return;
while (_requests.Count != 0)
{
WorldLifecycleCheckpointRequest request = _requests.Dequeue();
if (request.Status != RetailUiAutomationCheckpointStatus.Pending)
continue;
WriteCheckpoint(request, outcome);
}
}
}
private void WriteCheckpoint(
WorldLifecycleCheckpointRequest request,
RenderFrameOutcome outcome)
{
try
{
Directory.CreateDirectory(_artifactDirectory);
var checkpoint = new WorldLifecycleCheckpoint(
Sequence: checked(++_sequence),
Name: name,
Sequence: request.Sequence,
Name: request.Name,
TimestampUtc: DateTime.UtcNow,
ProcessId: Environment.ProcessId,
Reveal: _getReveal(),
Resources: _captureResources());
Render: outcome,
Resources: _captureResources(outcome));
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);
string timelinePath = Path.Combine(_artifactDirectory, "world-lifecycle.checkpoints.jsonl");
File.AppendAllText(timelinePath, json + Environment.NewLine);
string timelinePath = Path.Combine(
_artifactDirectory,
"world-lifecycle.checkpoints.jsonl");
File.WriteAllText(
Path.Combine(_artifactDirectory, $"checkpoint-{name}.json"),
Path.Combine(
_artifactDirectory,
$"checkpoint-{request.Name}.json"),
json + Environment.NewLine);
_log($"[world-gate] checkpoint name={name} sequence={checkpoint.Sequence} path={timelinePath}");
error = string.Empty;
return true;
// Timeline publication is the commit edge: a visible row always
// has its complete named checkpoint artifact beside it.
File.AppendAllText(timelinePath, json + Environment.NewLine);
request.TrySetTerminal(
RetailUiAutomationCheckpointStatus.Succeeded,
error: null);
_log(
$"[world-gate] checkpoint name={request.Name} "
+ $"sequence={request.Sequence} path={timelinePath}");
}
catch (Exception exception)
{
error = $"checkpoint '{name}' failed: {exception.Message}";
return false;
request.TrySetTerminal(
RetailUiAutomationCheckpointStatus.Failed,
$"checkpoint '{request.Name}' sequence {request.Sequence} failed: "
+ exception.Message);
}
}
public void Dispose()
{
lock (_sync)
{
if (_disposed)
return;
_disposed = true;
while (_requests.Count != 0)
{
WorldLifecycleCheckpointRequest request = _requests.Dequeue();
request.TrySetTerminal(
RetailUiAutomationCheckpointStatus.Cancelled,
$"checkpoint '{request.Name}' was cancelled during world lifecycle automation shutdown");
}
}
}

View file

@ -70,7 +70,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
?? throw new ArgumentNullException(nameof(frameProfiler));
}
public WorldLifecycleResourceSnapshot Capture()
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
{
ObjectMeshManager meshManager = _meshes.MeshManager
?? throw new InvalidOperationException(
@ -82,8 +82,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
LoadedLandblocks: _world.LoadedLandblockIds.Count,
WorldEntities: _world.Entities.Count,
AnimatedEntities: _animations.Count,
VisibleLandblocks: render.VisibleLandblocks,
TotalLandblocks: render.TotalLandblocks,
VisibleLandblocks: outcome.World.VisibleLandblocks,
TotalLandblocks: outcome.World.TotalLandblocks,
LiveEntities: _liveEntities.Count,
MaterializedLiveEntities: _liveEntities.MaterializedCount,
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,

View file

@ -103,7 +103,6 @@ public sealed class GameWindow :
private LivePresentationRuntimeBindings? _livePresentationBindings;
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
private FrameRootRuntimeBindings? _frameRootBindings;
private IDisposable? _frameGraphPublication;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
@ -1048,15 +1047,13 @@ public sealed class GameWindow :
FrameRootResult result)
{
ArgumentNullException.ThrowIfNull(result);
if (_worldLifecycleAutomation is not null
|| _frameRootBindings is not null
if (_frameRootBindings is not null
|| _frameGraphPublication is not null)
{
throw new InvalidOperationException(
"The GameWindow composition shell already owns frame roots.");
}
_worldLifecycleAutomation = result.LifecycleAutomation;
_frameRootBindings = result.RuntimeBindings;
_frameGraphPublication = result.FrameGraphPublication;
}

View file

@ -60,6 +60,25 @@ internal interface IRenderFrameDiagnosticsPhase
void Publish(RenderFrameInput input, RenderFrameOutcome outcome);
}
internal interface IRenderFramePostDiagnosticsPhase
{
void Process(RenderFrameInput input, RenderFrameOutcome outcome);
}
internal sealed class NullRenderFramePostDiagnosticsPhase :
IRenderFramePostDiagnosticsPhase
{
public static NullRenderFramePostDiagnosticsPhase Instance { get; } = new();
private NullRenderFramePostDiagnosticsPhase()
{
}
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
{
}
}
/// <summary>
/// Closes presentation transactions which may have opened before a later
/// render phase failed. Recovery is idempotent so failures before the
@ -102,6 +121,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
private readonly IWorldSceneFramePhase _world;
private readonly IPrivatePresentationFramePhase _presentation;
private readonly IRenderFrameDiagnosticsPhase _diagnostics;
private readonly IRenderFramePostDiagnosticsPhase _postDiagnostics;
private readonly IRenderFrameFailureRecovery _recovery;
public RenderFrameOrchestrator(
@ -110,6 +130,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
IWorldSceneFramePhase world,
IPrivatePresentationFramePhase presentation,
IRenderFrameDiagnosticsPhase diagnostics,
IRenderFramePostDiagnosticsPhase postDiagnostics,
IRenderFrameFailureRecovery recovery)
{
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
@ -117,6 +138,8 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
_world = world ?? throw new ArgumentNullException(nameof(world));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_postDiagnostics = postDiagnostics
?? throw new ArgumentNullException(nameof(postDiagnostics));
_recovery = recovery ?? throw new ArgumentNullException(nameof(recovery));
}
@ -132,6 +155,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
_presentation.Render(input, world);
var outcome = new RenderFrameOutcome(world, presentation);
_diagnostics.Publish(input, outcome);
_postDiagnostics.Process(input, outcome);
return outcome;
}
catch (Exception error)

View file

@ -7,6 +7,22 @@ using AcDream.UI.Abstractions.Input;
namespace AcDream.App.UI.Testing;
public enum RetailUiAutomationCheckpointStatus
{
Pending,
Succeeded,
Failed,
Cancelled,
}
public interface IRetailUiAutomationCheckpoint
{
int Sequence { get; }
string Name { get; }
RetailUiAutomationCheckpointStatus Status { get; }
string? Error { get; }
}
/// <summary>
/// Narrow bridge from retained-UI scripts to render/world lifecycle
/// diagnostics. Implementations run on the same update/render thread as the
@ -17,7 +33,11 @@ public interface IRetailUiAutomationRuntime
bool IsWorldReady { get; }
bool IsWorldViewportVisible { get; }
int PortalMaterializationCount { get; }
bool TryWriteCheckpoint(string name, out string error);
bool TryRequestCheckpoint(
string name,
out IRetailUiAutomationCheckpoint? checkpoint,
out string error);
void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint);
bool TryRequestScreenshot(string name, out string error);
bool IsScreenshotComplete(string name);
}
@ -40,6 +60,8 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
private readonly HashSet<InputAction> _heldInputs = new();
private readonly bool _dumpOnStart;
private readonly string? _loadError;
private IRetailUiAutomationCheckpoint? _checkpoint;
private int _checkpointCommandIndex = -1;
private int _index;
private int _activeIndex = -1;
private double _commandElapsedMs;
@ -405,8 +427,47 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
return Stop(command, "usage: checkpoint <name>");
if (_runtime is null)
return Stop(command, "world lifecycle automation is unavailable");
return _runtime.TryWriteCheckpoint(command.Parts[1], out string error)
|| Stop(command, error);
if (_checkpoint is null)
{
if (!_runtime.TryRequestCheckpoint(
command.Parts[1],
out IRetailUiAutomationCheckpoint? checkpoint,
out string error))
{
return Stop(command, error);
}
if (checkpoint is null)
{
return Stop(
command,
"world lifecycle automation accepted a checkpoint without returning its acknowledgement");
}
_checkpoint = checkpoint;
_checkpointCommandIndex = _index;
}
if (_checkpointCommandIndex != _index)
{
return Stop(
command,
"checkpoint acknowledgement belongs to a different script command");
}
RetailUiAutomationCheckpointStatus status = _checkpoint.Status;
if (status == RetailUiAutomationCheckpointStatus.Pending)
return false;
string? terminalError = _checkpoint.Error;
_checkpoint = null;
_checkpointCommandIndex = -1;
return status == RetailUiAutomationCheckpointStatus.Succeeded
|| Stop(
command,
terminalError
?? $"checkpoint '{command.Parts[1]}' ended with {status}");
}
private bool DoScreenshot(ScriptCommand command)
@ -476,6 +537,12 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
public void Dispose()
{
if (_disposed) return;
if (_checkpoint is not null)
{
_runtime?.CancelCheckpoint(_checkpoint);
_checkpoint = null;
_checkpointCommandIndex = -1;
}
ReleaseHeldInputs();
_disposed = true;
}