acdream/tests/AcDream.App.Tests/Composition/InteractionUiRuntimeSourcesTests.cs
Erik 826f9ea9b5 refactor(app): complete session startup composition
Move the live-session reset and routing graph, combat and diagnostic command targets, and the sole gameplay input subscriber into Phase 7 before frame publication. Add exact retryable ownership for late bindings so partial startup cannot strand session or component teardown edges.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 18:49:31 +02:00

240 lines
7.8 KiB
C#

using System.Numerics;
using System.Runtime.CompilerServices;
using AcDream.App.Composition;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Net;
using AcDream.Core.World;
using AcDream.UI.Abstractions;
namespace AcDream.App.Tests.Composition;
public sealed class InteractionUiRuntimeSourcesTests
{
[Fact]
public void DesiredComponentSnapshotNormalizesNullAndClearsExactly()
{
var state = new DesiredComponentSnapshotState();
IReadOnlyList<(uint Id, uint Amount)> items =
[(0x01020304u, 7u)];
Assert.Empty(state.Items);
state.Items = items;
Assert.Same(items, state.Items);
state.Clear();
Assert.Empty(state.Items);
state.Items = null!;
Assert.Empty(state.Items);
}
[Fact]
public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly()
{
var source = new DeferredLiveSessionUiAuthority();
var first = new SessionTarget(true);
var second = new SessionTarget(false);
Assert.False(source.IsInWorld);
Assert.Same(NullCommandBus.Instance, source.Commands);
IDisposable stale = source.Bind(first);
Assert.True(source.IsInWorld);
Assert.Same(first.Commands, source.Commands);
Assert.Throws<InvalidOperationException>(() => source.Bind(second));
stale.Dispose();
IDisposable current = source.Bind(second);
stale.Dispose();
Assert.False(source.IsInWorld);
Assert.Same(second.Commands, source.Commands);
source.Deactivate();
current.Dispose();
Assert.Same(NullCommandBus.Instance, source.Commands);
Assert.Throws<ObjectDisposedException>(() => source.Bind(first));
}
[Fact]
public void SelectionAuthorityUsesRetailSafeDefaultsAndExactBindingLifetime()
{
var source = new DeferredSelectionUiAuthority();
var query = new SelectionQuery();
SelectionInteractionController interactions =
Stub<SelectionInteractionController>();
Assert.True(source.IsWithinExternalContainerUseRange(1u));
Assert.False(source.ShouldShowHealth(1u));
Assert.Null(source.ResolveVividTargetInfo(1u));
IDisposable binding = source.Bind(query, interactions);
Assert.False(source.IsWithinExternalContainerUseRange(1u));
Assert.True(source.ShouldShowHealth(1u));
Assert.Equal(3f, source.ResolveVividTargetInfo(1u)?.SelectionSphereRadius);
Assert.Throws<InvalidOperationException>(() => source.Bind(query, interactions));
binding.Dispose();
Assert.True(source.IsWithinExternalContainerUseRange(1u));
source.Deactivate();
Assert.Throws<ObjectDisposedException>(() => source.Bind(query, interactions));
}
[Fact]
public void RadarNeverCachesAnUnboundBootstrapSnapshot()
{
var source = new DeferredRadarSnapshotSource();
Assert.Same(UiRadarSnapshot.Empty, source.Snapshot());
var provider = new RadarSnapshotProvider(
new AcDream.Core.Items.ClientObjectTable(),
static () => new Dictionary<uint, WorldEntity>(),
static () => new Dictionary<uint, WorldSession.EntitySpawn>(),
static () => 0u,
static () => 0f,
static () => 0u,
static () => null,
static () => false,
static () => true);
IDisposable binding = source.Bind(provider);
Assert.True(source.Snapshot().UiLocked);
binding.Dispose();
Assert.False(source.Snapshot().UiLocked);
}
[Fact]
public void AutomationProxyIsInertUntilExactRuntimeBinds()
{
var source = new DeferredWorldLifecycleAutomationRuntime();
Assert.False(source.TryWriteCheckpoint("early", out string earlyError));
Assert.Contains("not bound", earlyError);
var target = new AutomationRuntime();
IDisposable binding = source.Bind(target);
Assert.True(source.IsWorldReady);
Assert.True(source.TryWriteCheckpoint("ready", out _));
Assert.Equal("ready", target.LastCheckpoint);
binding.Dispose();
Assert.False(source.IsWorldReady);
source.Deactivate();
Assert.Throws<ObjectDisposedException>(() => source.Bind(target));
}
[Fact]
public void ExpectedOwnerBindingReleasesOnce()
{
object target = new();
int releases = 0;
var binding = new ExpectedOwnerBinding<object>(target, value =>
{
Assert.Same(target, value);
releases++;
});
binding.Dispose();
binding.Dispose();
Assert.Equal(1, releases);
}
[Fact]
public void RetainedInputCaptureBindingCannotClearAReplacement()
{
var slot = new RetainedUiInputCaptureSlot();
var first = new UiRoot();
var second = new UiRoot();
IDisposable stale = slot.Bind(first);
Assert.Same(first, slot.Root);
Assert.Throws<InvalidOperationException>(() => slot.Bind(second));
stale.Dispose();
IDisposable current = slot.Bind(second);
stale.Dispose();
Assert.Same(second, slot.Root);
current.Dispose();
Assert.Null(slot.Root);
}
[Fact]
public void LateBindingCleanupRetriesOnlyTheFailedSuffix()
{
var bindings = new InteractionUiLateBindings();
var first = new RetryBinding(failures: 0);
var second = new RetryBinding(failures: 1);
bindings.AdoptLateOwnerBinding("first", first);
bindings.AdoptLateOwnerBinding("second", second);
Assert.Throws<AggregateException>(bindings.Dispose);
Assert.Equal(1, first.Calls);
Assert.Equal(1, second.Calls);
bindings.Dispose();
Assert.Equal(1, first.Calls);
Assert.Equal(2, second.Calls);
Assert.Throws<ObjectDisposedException>(() =>
bindings.AdoptLateOwnerBinding("late", new RetryBinding(0)));
}
private sealed class SessionTarget(bool inWorld) : ILiveUiSessionTarget
{
public bool IsInWorld { get; } = inWorld;
public WorldSession? CurrentSession => null;
public ICommandBus Commands { get; } = new LiveCommandBus();
}
private sealed class SelectionQuery : IRetainedUiSelectionQuery
{
public bool ShouldShowHealth(uint serverGuid) => true;
public VividTargetInfo? ResolveVividTargetInfo(uint serverGuid) =>
new(Vector3.Zero, 3f, 0u, 0u);
public bool IsWithinExternalContainerUseRange(uint serverGuid) => false;
}
private sealed class AutomationRuntime
: AcDream.App.UI.Testing.IRetailUiAutomationRuntime
{
public bool IsWorldReady => true;
public bool IsWorldViewportVisible => true;
public int PortalMaterializationCount => 2;
public string? LastCheckpoint { get; private set; }
public bool TryWriteCheckpoint(string name, out string error)
{
LastCheckpoint = name;
error = string.Empty;
return true;
}
public bool TryRequestScreenshot(string name, out string error)
{
error = string.Empty;
return true;
}
public bool IsScreenshotComplete(string name) => true;
}
private sealed class RetryBinding(int failures) : IDisposable
{
private int _failures = failures;
public int Calls { get; private set; }
public void Dispose()
{
Calls++;
if (_failures > 0)
{
_failures--;
throw new InvalidOperationException("retry");
}
}
}
private static T Stub<T>() where T : class =>
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
}