test(runtime): add deterministic world gate artifacts

This commit is contained in:
Erik 2026-07-20 22:37:16 +02:00
parent db45b81f75
commit 354c2adc2e
11 changed files with 703 additions and 10 deletions

View file

@ -0,0 +1,155 @@
using System.Text.Json;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
namespace AcDream.App.Diagnostics;
internal static class AutomationArtifactName
{
public static bool TryValidate(string? name, out string error)
{
if (string.IsNullOrWhiteSpace(name) || name.Length > 80)
{
error = "artifact name must contain 1-80 characters";
return false;
}
foreach (char c in name)
{
if (!char.IsAsciiLetterOrDigit(c) && c is not '-' and not '_')
{
error = $"artifact name '{name}' may contain only letters, digits, '-' and '_'";
return false;
}
}
error = string.Empty;
return true;
}
}
internal sealed record WorldLifecycleResourceSnapshot(
int LoadedLandblocks,
int WorldEntities,
int AnimatedEntities,
int VisibleLandblocks,
int TotalLandblocks,
int LiveEntities,
int MaterializedLiveEntities,
int PendingLiveTeardowns,
int PendingLandblockRetirements,
int ParticleEmitters,
int Particles,
int ParticleBindings,
int ParticleOwners,
int EffectOwners,
int LightOwners,
int ScriptOwners,
int ActiveScripts,
int MeshRenderData,
int MeshAtlasArrays,
long MeshEstimatedBytes,
int StagedMeshUploads,
long StagedMeshBytes,
long TrackedGpuBytes,
int TrackedGpuBuffers,
int TrackedGpuTextures,
int OwnedCompositeTextures,
int CompositeTextureOwners,
int ActiveParticleTextures,
int ParticleTextureOwners,
int CompositeWarmupPending,
long ManagedBytes,
long ManagedCommittedBytes,
double Fps,
double FrameMilliseconds,
string? LastFrameProfile);
internal sealed record WorldLifecycleCheckpoint(
int Sequence,
string Name,
DateTime TimestampUtc,
int ProcessId,
WorldRevealLifecycleSnapshot Reveal,
WorldLifecycleResourceSnapshot Resources);
/// <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
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
private readonly Func<WorldRevealLifecycleSnapshot> _getReveal;
private readonly Func<int> _getPortalMaterializationCount;
private readonly Func<WorldLifecycleResourceSnapshot> _captureResources;
private readonly FrameScreenshotController _screenshots;
private readonly string _artifactDirectory;
private readonly Action<string> _log;
private int _sequence;
public WorldLifecycleAutomationController(
Func<WorldRevealLifecycleSnapshot> getReveal,
Func<int> getPortalMaterializationCount,
Func<WorldLifecycleResourceSnapshot> captureResources,
FrameScreenshotController screenshots,
string artifactDirectory,
Action<string>? log = null)
{
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
_getPortalMaterializationCount = getPortalMaterializationCount
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
_screenshots = screenshots ?? throw new ArgumentNullException(nameof(screenshots));
_artifactDirectory = string.IsNullOrWhiteSpace(artifactDirectory)
? throw new ArgumentException("An automation artifact directory is required.", nameof(artifactDirectory))
: Path.GetFullPath(artifactDirectory);
_log = log ?? (_ => { });
}
public bool IsWorldReady => _getReveal().IsReady;
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
public int PortalMaterializationCount => _getPortalMaterializationCount();
public bool TryWriteCheckpoint(string name, out string error)
{
if (!AutomationArtifactName.TryValidate(name, out error))
return false;
try
{
Directory.CreateDirectory(_artifactDirectory);
var checkpoint = new WorldLifecycleCheckpoint(
Sequence: checked(++_sequence),
Name: name,
TimestampUtc: DateTime.UtcNow,
ProcessId: Environment.ProcessId,
Reveal: _getReveal(),
Resources: _captureResources());
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);
string timelinePath = Path.Combine(_artifactDirectory, "world-lifecycle.checkpoints.jsonl");
File.AppendAllText(timelinePath, json + Environment.NewLine);
File.WriteAllText(
Path.Combine(_artifactDirectory, $"checkpoint-{name}.json"),
json + Environment.NewLine);
_log($"[world-gate] checkpoint name={name} sequence={checkpoint.Sequence} path={timelinePath}");
error = string.Empty;
return true;
}
catch (Exception exception)
{
error = $"checkpoint '{name}' failed: {exception.Message}";
return false;
}
}
public bool TryRequestScreenshot(string name, out string error) =>
_screenshots.TryRequest(name, out error);
public bool IsScreenshotComplete(string name) => _screenshots.IsComplete(name);
}