feat(runtime): public initial-Create completion surface for hosts
Cutover slice C3-1 (the C3 flip's Runtime prerequisite, landed separately after the flip itself was halted with structural findings — see the plan's C3a/b/c decomposition). Hosts can now read the executor-completion facts they must bind at cutover through one public, generation-gated channel accessor: RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion returns RuntimeInitialCreatePlacementCompletion — the teleport-hook phase, resident cell, replay outcomes, and per-Position route facts (disposition, constrain phase, hook phase, stop-interpolation/zero-velocity/preserve- heading/send-position flags) via public 1:1 mirror enums of the internal classifier vocabulary. The projection is built once at completion, cached in the same reaped entry as the internal receipt (identical acknowledge/ discard/clear lifecycle, ledger-covered), and read allocation-free. Mirror maps enumerate every value explicitly with throwing catch-alls, guarded by a sabotage-verified arity/round-trip reflection test. Doc comments pin the two consumption rules: unparent/placement-frame are already applied to the canonical snapshot (hosts must not re-apply), and array order — not Sequence — is the authoritative Position-fact ordering. Reviewed: architecture PASS + retail-conformance PASS (mirrors verified member-for-member against the retail phase semantics; the route-fact selection confirmed to cover exactly the host-bindable deferrals). Runtime 932/932; complete Release solution 10,727 passed / 4 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a32aba35d1
commit
fe02c4f56d
4 changed files with 588 additions and 9 deletions
|
|
@ -174,7 +174,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
||||||
Placements = new RuntimePlacementProjectionChannel(
|
Placements = new RuntimePlacementProjectionChannel(
|
||||||
Events,
|
Events,
|
||||||
Physics.SetPosition);
|
Physics.SetPosition,
|
||||||
|
InitialCreateExecution);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal RuntimeEntityObjectLifetime(
|
internal RuntimeEntityObjectLifetime(
|
||||||
|
|
@ -226,7 +227,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
||||||
Placements = new RuntimePlacementProjectionChannel(
|
Placements = new RuntimePlacementProjectionChannel(
|
||||||
Events,
|
Events,
|
||||||
Physics.SetPosition);
|
Physics.SetPosition,
|
||||||
|
InitialCreateExecution);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal RuntimeEntityObjectLifetime(
|
internal RuntimeEntityObjectLifetime(
|
||||||
|
|
@ -278,7 +280,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
||||||
Placements = new RuntimePlacementProjectionChannel(
|
Placements = new RuntimePlacementProjectionChannel(
|
||||||
Events,
|
Events,
|
||||||
Physics.SetPosition);
|
Physics.SetPosition,
|
||||||
|
InitialCreateExecution);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeEntityDirectory Entities { get; }
|
public RuntimeEntityDirectory Entities { get; }
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,99 @@ internal readonly record struct RuntimeInitialCreateExecutionReceipt(
|
||||||
ImmutableArray<RuntimeInitialCreateExecutedAction> Trace,
|
ImmutableArray<RuntimeInitialCreateExecutedAction> Trace,
|
||||||
int ReplayedDeferredChildCount);
|
int ReplayedDeferredChildCount);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C3-1: public projection of <see cref="RuntimeTeleportHookPhase"/>. A
|
||||||
|
/// separate public enum (rather than widening the internal one's
|
||||||
|
/// accessibility) keeps the classifier/executor's internal vocabulary free to
|
||||||
|
/// evolve without becoming a host-facing contract; values map 1:1 today.
|
||||||
|
/// </summary>
|
||||||
|
public enum RuntimeInitialCreateTeleportHookPhase : byte
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
BeforePositionOperation,
|
||||||
|
AfterPositionOperation,
|
||||||
|
AfterEnterWorld,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>C3-1: public projection of <see cref="RuntimeAuthoritativePositionDisposition"/>.</summary>
|
||||||
|
public enum RuntimeInitialCreatePositionDisposition : byte
|
||||||
|
{
|
||||||
|
RejectedAuthority,
|
||||||
|
RejectedData,
|
||||||
|
AwaitFreshPosition,
|
||||||
|
NoPositionOperation,
|
||||||
|
Interpolate,
|
||||||
|
SetPosition,
|
||||||
|
SetPositionSimple,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>C3-1: public projection of <see cref="RuntimePositionConstrainPhase"/>.</summary>
|
||||||
|
public enum RuntimeInitialCreatePositionConstrainPhase : byte
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
BeforePositionOperation,
|
||||||
|
AfterPositionOperation,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C3-1: one Position continuation's route facts from the executor's trace,
|
||||||
|
/// projected to a public shape so a host can bind constrain/interpolation
|
||||||
|
/// presentation (retail's <c>ConstrainTo</c> placement, stop-interpolate,
|
||||||
|
/// zero-velocity, preserve-heading, send-position-immediately) without
|
||||||
|
/// reaching into internal Runtime route-classifier types.
|
||||||
|
/// Review addendum (2026-08-02): the route's <c>UnparentBeforeRouting</c>
|
||||||
|
/// ("unset_parent") and <c>ApplyPlacementFrameBeforeRouting</c>
|
||||||
|
/// ("SetPlacementFrame") facts are deliberately NOT projected here - the
|
||||||
|
/// executor's own merge (<c>ApplyAcceptedPositionSnapshot</c>'s
|
||||||
|
/// <c>clearParent</c>/<c>installPlacementFrame</c> parameters) already
|
||||||
|
/// applies both directly to the canonical snapshot, synchronously, before
|
||||||
|
/// the trace entry carrying this fact is even built. A host reading this
|
||||||
|
/// record must NOT re-apply either one - the facts this type DOES carry
|
||||||
|
/// (<see cref="ConstrainPhase"/>, <see cref="StopInterpolating"/>,
|
||||||
|
/// <see cref="ZeroVelocity"/>, <see cref="PreserveHeading"/>,
|
||||||
|
/// <see cref="SendPositionImmediately"/>, <see cref="HookPhase"/>) are
|
||||||
|
/// exactly the bindings still DEFERRED to the host at presentation time;
|
||||||
|
/// everything already-applied is intentionally excluded.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct RuntimeInitialCreatePositionRouteFact(
|
||||||
|
ulong Sequence,
|
||||||
|
RuntimeInitialCreatePositionDisposition Disposition,
|
||||||
|
RuntimeInitialCreateTeleportHookPhase HookPhase,
|
||||||
|
RuntimeInitialCreatePositionConstrainPhase ConstrainPhase,
|
||||||
|
bool StopInterpolating,
|
||||||
|
bool ZeroVelocity,
|
||||||
|
bool PreserveHeading,
|
||||||
|
bool SendPositionImmediately);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C3-1: the public host consumption shape for one executor drain's
|
||||||
|
/// completion, reached via
|
||||||
|
/// <see cref="RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion"/>
|
||||||
|
/// using the correlated <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/>
|
||||||
|
/// receipt's own Entity/Sequence identity. Built exactly once, at completion
|
||||||
|
/// time, and cached alongside the internal receipt it projects (see
|
||||||
|
/// <see cref="RuntimeInitialCreateContinuationExecutor.ProjectCompletion"/>) -
|
||||||
|
/// a host retrying <c>TryGetInitialCreateCompletion</c> across multiple polls
|
||||||
|
/// never triggers a second allocation.
|
||||||
|
/// Review addendum (2026-08-02): <see cref="PositionRouteFacts"/>'s ARRAY
|
||||||
|
/// ORDER is the authoritative ordering, not <see cref="RuntimeInitialCreatePositionRouteFact.Sequence"/>
|
||||||
|
/// alone - Position facts drained from the SAME same-incarnation envelope
|
||||||
|
/// (<see cref="RuntimeInitialCreateExecutedAction.Stage"/> distinguishes
|
||||||
|
/// them internally, a field this projection does not carry) share one
|
||||||
|
/// continuation <c>Sequence</c>, so two entries can legitimately have equal
|
||||||
|
/// <c>Sequence</c> values. <see cref="RuntimeInitialCreateContinuationExecutor.ProjectCompletion"/>
|
||||||
|
/// walks the executor's trace strictly in construction order (itself the
|
||||||
|
/// exact FIFO drain order) and appends without reordering or deduplicating,
|
||||||
|
/// so array index - never a sort or group-by on <c>Sequence</c> - is the
|
||||||
|
/// only reliable way to recover drain order from this array.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct RuntimeInitialCreatePlacementCompletion(
|
||||||
|
RuntimeEntityKey Entity,
|
||||||
|
uint FullCellId,
|
||||||
|
RuntimeInitialCreateTeleportHookPhase TeleportHookPhase,
|
||||||
|
ImmutableArray<RuntimeInitialCreatePositionRouteFact> PositionRouteFacts,
|
||||||
|
int ReplayedDeferredChildCount);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies one entity's completed initial-Create residence: adopts the
|
/// Applies one entity's completed initial-Create residence: adopts the
|
||||||
/// initial placement exactly once, emits the AfterEnterWorld teleport-hook
|
/// initial placement exactly once, emits the AfterEnterWorld teleport-hook
|
||||||
|
|
@ -285,10 +378,16 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
||||||
/// reentrancy guard), so only the most recent completion for a key is
|
/// reentrancy guard), so only the most recent completion for a key is
|
||||||
/// ever meaningful; the exact-sequence check in
|
/// ever meaningful; the exact-sequence check in
|
||||||
/// <see cref="TryGetCompletionReceipt"/> rejects a stale lookup against a
|
/// <see cref="TryGetCompletionReceipt"/> rejects a stale lookup against a
|
||||||
/// superseded completion under a reused key.
|
/// superseded completion under a reused key. C3-1: the tuple's third
|
||||||
|
/// slot is the SAME receipt already projected once to the public
|
||||||
|
/// <see cref="RuntimeInitialCreatePlacementCompletion"/> shape (see
|
||||||
|
/// <see cref="ProjectCompletion"/>) - stored here, not recomputed per
|
||||||
|
/// read, so a host polling <c>TryGetInitialCreateCompletion</c> across
|
||||||
|
/// retries never allocates a second time for the same completion.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Dictionary<RuntimeEntityKey,
|
private readonly Dictionary<RuntimeEntityKey,
|
||||||
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt)>
|
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
|
||||||
|
RuntimeInitialCreatePlacementCompletion Public)>
|
||||||
_completionReceipts = [];
|
_completionReceipts = [];
|
||||||
private Func<RuntimeGenerationToken>? _generation;
|
private Func<RuntimeGenerationToken>? _generation;
|
||||||
private Func<bool>? _usePositionFromServer;
|
private Func<bool>? _usePositionFromServer;
|
||||||
|
|
@ -438,7 +537,8 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
||||||
{
|
{
|
||||||
if (_completionReceipts.TryGetValue(
|
if (_completionReceipts.TryGetValue(
|
||||||
token.Entity,
|
token.Entity,
|
||||||
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
|
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
|
||||||
|
RuntimeInitialCreatePlacementCompletion Public) entry)
|
||||||
&& entry.Sequence == token.Sequence)
|
&& entry.Sequence == token.Sequence)
|
||||||
{
|
{
|
||||||
receipt = entry.Receipt;
|
receipt = entry.Receipt;
|
||||||
|
|
@ -448,6 +548,185 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C3-1: the public host consumption surface for
|
||||||
|
/// <see cref="TryGetCompletionReceipt"/> - same exact-sequence
|
||||||
|
/// correlation rule, but returns the cached public projection instead of
|
||||||
|
/// the internal receipt/trace. Reached via
|
||||||
|
/// <see cref="RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal bool TryGetCompletion(
|
||||||
|
in RuntimePlacementProjectionToken token,
|
||||||
|
out RuntimeInitialCreatePlacementCompletion completion)
|
||||||
|
{
|
||||||
|
if (_completionReceipts.TryGetValue(
|
||||||
|
token.Entity,
|
||||||
|
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
|
||||||
|
RuntimeInitialCreatePlacementCompletion Public) entry)
|
||||||
|
&& entry.Sequence == token.Sequence)
|
||||||
|
{
|
||||||
|
completion = entry.Public;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
completion = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Review fix (2026-08-02, architecture pass): every arm is now listed
|
||||||
|
/// explicitly and the catch-all throws instead of silently folding an
|
||||||
|
/// unmapped future internal value into <c>None</c>. A value this method
|
||||||
|
/// cannot map must never reach a host disguised as "nothing to bind" -
|
||||||
|
/// that would silently drop presentation behavior (e.g. a real
|
||||||
|
/// teleport-hook phase host code never runs). See
|
||||||
|
/// <see cref="RuntimeInitialCreateContinuationExecutorTests"/>'s
|
||||||
|
/// reflection-based completeness test, which walks every declared
|
||||||
|
/// <see cref="RuntimeTeleportHookPhase"/> value through this exact
|
||||||
|
/// method and fails if a new internal value is ever added without a
|
||||||
|
/// matching arm here.
|
||||||
|
/// </summary>
|
||||||
|
private static RuntimeInitialCreateTeleportHookPhase MapHookPhase(
|
||||||
|
RuntimeTeleportHookPhase phase) => phase switch
|
||||||
|
{
|
||||||
|
RuntimeTeleportHookPhase.None =>
|
||||||
|
RuntimeInitialCreateTeleportHookPhase.None,
|
||||||
|
RuntimeTeleportHookPhase.BeforePositionOperation =>
|
||||||
|
RuntimeInitialCreateTeleportHookPhase.BeforePositionOperation,
|
||||||
|
RuntimeTeleportHookPhase.AfterPositionOperation =>
|
||||||
|
RuntimeInitialCreateTeleportHookPhase.AfterPositionOperation,
|
||||||
|
RuntimeTeleportHookPhase.AfterEnterWorld =>
|
||||||
|
RuntimeInitialCreateTeleportHookPhase.AfterEnterWorld,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(phase),
|
||||||
|
phase,
|
||||||
|
$"Unmapped {nameof(RuntimeTeleportHookPhase)} value - add an explicit arm to {nameof(MapHookPhase)} and to the public {nameof(RuntimeInitialCreateTeleportHookPhase)} projection."),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Review fix (2026-08-02): see <see cref="MapHookPhase"/>'s remarks -
|
||||||
|
/// same explicit-arms-plus-throwing-catch-all discipline.
|
||||||
|
/// </summary>
|
||||||
|
private static RuntimeInitialCreatePositionDisposition MapDisposition(
|
||||||
|
RuntimeAuthoritativePositionDisposition disposition) => disposition switch
|
||||||
|
{
|
||||||
|
RuntimeAuthoritativePositionDisposition.RejectedAuthority =>
|
||||||
|
RuntimeInitialCreatePositionDisposition.RejectedAuthority,
|
||||||
|
RuntimeAuthoritativePositionDisposition.RejectedData =>
|
||||||
|
RuntimeInitialCreatePositionDisposition.RejectedData,
|
||||||
|
RuntimeAuthoritativePositionDisposition.AwaitFreshPosition =>
|
||||||
|
RuntimeInitialCreatePositionDisposition.AwaitFreshPosition,
|
||||||
|
RuntimeAuthoritativePositionDisposition.NoPositionOperation =>
|
||||||
|
RuntimeInitialCreatePositionDisposition.NoPositionOperation,
|
||||||
|
RuntimeAuthoritativePositionDisposition.Interpolate =>
|
||||||
|
RuntimeInitialCreatePositionDisposition.Interpolate,
|
||||||
|
RuntimeAuthoritativePositionDisposition.SetPosition =>
|
||||||
|
RuntimeInitialCreatePositionDisposition.SetPosition,
|
||||||
|
RuntimeAuthoritativePositionDisposition.SetPositionSimple =>
|
||||||
|
RuntimeInitialCreatePositionDisposition.SetPositionSimple,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(disposition),
|
||||||
|
disposition,
|
||||||
|
$"Unmapped {nameof(RuntimeAuthoritativePositionDisposition)} value - add an explicit arm to {nameof(MapDisposition)} and to the public {nameof(RuntimeInitialCreatePositionDisposition)} projection."),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Review fix (2026-08-02): see <see cref="MapHookPhase"/>'s remarks -
|
||||||
|
/// same explicit-arms-plus-throwing-catch-all discipline.
|
||||||
|
/// </summary>
|
||||||
|
private static RuntimeInitialCreatePositionConstrainPhase MapConstrainPhase(
|
||||||
|
RuntimePositionConstrainPhase phase) => phase switch
|
||||||
|
{
|
||||||
|
RuntimePositionConstrainPhase.None =>
|
||||||
|
RuntimeInitialCreatePositionConstrainPhase.None,
|
||||||
|
RuntimePositionConstrainPhase.BeforePositionOperation =>
|
||||||
|
RuntimeInitialCreatePositionConstrainPhase.BeforePositionOperation,
|
||||||
|
RuntimePositionConstrainPhase.AfterPositionOperation =>
|
||||||
|
RuntimeInitialCreatePositionConstrainPhase.AfterPositionOperation,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(phase),
|
||||||
|
phase,
|
||||||
|
$"Unmapped {nameof(RuntimePositionConstrainPhase)} value - add an explicit arm to {nameof(MapConstrainPhase)} and to the public {nameof(RuntimeInitialCreatePositionConstrainPhase)} projection."),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C3-1: projects an internal <see cref="RuntimeInitialCreateExecutionReceipt"/>
|
||||||
|
/// to the public <see cref="RuntimeInitialCreatePlacementCompletion"/>
|
||||||
|
/// shape exactly once, at completion time (see the
|
||||||
|
/// <c>_completionReceipts</c> assignment in <see cref="ExecuteCore"/>).
|
||||||
|
/// Only Position-kind trace entries carry route facts meaningful for
|
||||||
|
/// constrain/interpolation binding; every other action kind
|
||||||
|
/// (InitialAdoption, TeleportHookRequest, replay, envelope stages, ...)
|
||||||
|
/// is intentionally excluded from <see cref="RuntimeInitialCreatePlacementCompletion.PositionRouteFacts"/> -
|
||||||
|
/// widening this to every trace entry would require making the whole
|
||||||
|
/// internal action-kind vocabulary public, which the pinned contract
|
||||||
|
/// explicitly prefers to avoid.
|
||||||
|
/// </summary>
|
||||||
|
private static RuntimeInitialCreatePlacementCompletion ProjectCompletion(
|
||||||
|
in RuntimeInitialCreateExecutionReceipt receipt)
|
||||||
|
{
|
||||||
|
ImmutableArray<RuntimeInitialCreateExecutedAction> trace = receipt.Trace;
|
||||||
|
int positionCount = 0;
|
||||||
|
for (int i = 0; i < trace.Length; i++)
|
||||||
|
{
|
||||||
|
if (trace[i].Kind == RuntimeInitialCreateExecutedActionKind.Position)
|
||||||
|
positionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmutableArray<RuntimeInitialCreatePositionRouteFact> positionFacts;
|
||||||
|
if (positionCount == 0)
|
||||||
|
{
|
||||||
|
positionFacts = ImmutableArray<RuntimeInitialCreatePositionRouteFact>.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var builder = ImmutableArray.CreateBuilder<RuntimeInitialCreatePositionRouteFact>(
|
||||||
|
positionCount);
|
||||||
|
for (int i = 0; i < trace.Length; i++)
|
||||||
|
{
|
||||||
|
RuntimeInitialCreateExecutedAction action = trace[i];
|
||||||
|
if (action.Kind != RuntimeInitialCreateExecutedActionKind.Position)
|
||||||
|
continue;
|
||||||
|
// Review fix (2026-08-02): PositionDisposition is nullable
|
||||||
|
// on RuntimeInitialCreateExecutedAction because it is only
|
||||||
|
// meaningful for Position/hook-request entries in general -
|
||||||
|
// but BuildPositionTrace is the SOLE constructor of
|
||||||
|
// Kind.Position entries (grep-confirmed, 5 call sites, all
|
||||||
|
// through BuildPositionTrace) and it always passes
|
||||||
|
// route.Disposition, a non-nullable enum, into this slot.
|
||||||
|
// Null is therefore NOT a legitimate state for a Position-
|
||||||
|
// kind entry specifically - a silent `?? NoPositionOperation`
|
||||||
|
// fallback here would have hidden a real bug (a future
|
||||||
|
// Position-trace producer that forgot to set it) behind a
|
||||||
|
// plausible-looking default. Fail loudly instead.
|
||||||
|
if (action.PositionDisposition is not { } disposition)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"A Position-kind executor trace entry must always " +
|
||||||
|
"carry a non-null PositionDisposition - " +
|
||||||
|
"BuildPositionTrace (the sole producer of Kind.Position " +
|
||||||
|
"entries) always supplies route.Disposition.");
|
||||||
|
}
|
||||||
|
builder.Add(new RuntimeInitialCreatePositionRouteFact(
|
||||||
|
action.Sequence,
|
||||||
|
MapDisposition(disposition),
|
||||||
|
MapHookPhase(action.HookPhase),
|
||||||
|
MapConstrainPhase(action.ConstrainPhase),
|
||||||
|
action.StopInterpolating,
|
||||||
|
action.ZeroVelocity,
|
||||||
|
action.PreserveHeading,
|
||||||
|
action.SendPositionImmediately));
|
||||||
|
}
|
||||||
|
positionFacts = builder.MoveToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RuntimeInitialCreatePlacementCompletion(
|
||||||
|
receipt.Entity,
|
||||||
|
receipt.FullCellId,
|
||||||
|
MapHookPhase(receipt.TeleportHookPhase),
|
||||||
|
positionFacts,
|
||||||
|
receipt.ReplayedDeferredChildCount);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// F2: reaps exactly one completion-receipt correlation entry, bound as
|
/// F2: reaps exactly one completion-receipt correlation entry, bound as
|
||||||
/// <see cref="RuntimeSetPositionState.BindExecutorCompletionAcknowledgement"/>'s
|
/// <see cref="RuntimeSetPositionState.BindExecutorCompletionAcknowledgement"/>'s
|
||||||
|
|
@ -461,7 +740,8 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
||||||
{
|
{
|
||||||
if (_completionReceipts.TryGetValue(
|
if (_completionReceipts.TryGetValue(
|
||||||
key,
|
key,
|
||||||
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
|
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
|
||||||
|
RuntimeInitialCreatePlacementCompletion Public) entry)
|
||||||
&& entry.Sequence == sequence)
|
&& entry.Sequence == sequence)
|
||||||
{
|
{
|
||||||
_completionReceipts.Remove(key);
|
_completionReceipts.Remove(key);
|
||||||
|
|
@ -787,6 +1067,13 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
||||||
progress.Trace.ToImmutable(),
|
progress.Trace.ToImmutable(),
|
||||||
progress.ReplayedDeferredChildCount);
|
progress.ReplayedDeferredChildCount);
|
||||||
receipt = completedReceipt;
|
receipt = completedReceipt;
|
||||||
|
// C3-1: project to the public host-consumption shape
|
||||||
|
// exactly once here, alongside the internal receipt -
|
||||||
|
// never recomputed per host read/retry (see
|
||||||
|
// ProjectCompletion's and _completionReceipts's own doc
|
||||||
|
// comments).
|
||||||
|
RuntimeInitialCreatePlacementCompletion publicCompletion =
|
||||||
|
ProjectCompletion(completedReceipt);
|
||||||
_progress.Remove(key);
|
_progress.Remove(key);
|
||||||
// C0-1: bridge the executor's own completion onto the
|
// C0-1: bridge the executor's own completion onto the
|
||||||
// SAME ordered placement receipt stream every
|
// SAME ordered placement receipt stream every
|
||||||
|
|
@ -806,7 +1093,7 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
||||||
canonical,
|
canonical,
|
||||||
beforePublish: token =>
|
beforePublish: token =>
|
||||||
_completionReceipts[key] =
|
_completionReceipts[key] =
|
||||||
(token.Sequence, completedReceipt));
|
(token.Sequence, completedReceipt, publicCompletion));
|
||||||
return RuntimeInitialCreateExecutionStatus.Completed;
|
return RuntimeInitialCreateExecutionStatus.Completed;
|
||||||
}
|
}
|
||||||
case RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised:
|
case RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised:
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,20 @@ public sealed class RuntimePlacementProjectionChannel
|
||||||
{
|
{
|
||||||
private readonly RuntimeEntityObjectEventStream _events;
|
private readonly RuntimeEntityObjectEventStream _events;
|
||||||
private readonly RuntimeSetPositionState _setPosition;
|
private readonly RuntimeSetPositionState _setPosition;
|
||||||
|
private readonly RuntimeInitialCreateContinuationExecutor _initialCreateExecution;
|
||||||
private Func<RuntimeGenerationToken> _generation = static () => default;
|
private Func<RuntimeGenerationToken> _generation = static () => default;
|
||||||
private bool _generationBound;
|
private bool _generationBound;
|
||||||
|
|
||||||
internal RuntimePlacementProjectionChannel(
|
internal RuntimePlacementProjectionChannel(
|
||||||
RuntimeEntityObjectEventStream events,
|
RuntimeEntityObjectEventStream events,
|
||||||
RuntimeSetPositionState setPosition)
|
RuntimeSetPositionState setPosition,
|
||||||
|
RuntimeInitialCreateContinuationExecutor initialCreateExecution)
|
||||||
{
|
{
|
||||||
_events = events ?? throw new ArgumentNullException(nameof(events));
|
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||||
_setPosition = setPosition
|
_setPosition = setPosition
|
||||||
?? throw new ArgumentNullException(nameof(setPosition));
|
?? throw new ArgumentNullException(nameof(setPosition));
|
||||||
|
_initialCreateExecution = initialCreateExecution
|
||||||
|
?? throw new ArgumentNullException(nameof(initialCreateExecution));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -81,6 +85,32 @@ public sealed class RuntimePlacementProjectionChannel
|
||||||
|
|
||||||
public int PendingCount => _setPosition.PendingProjectionCount;
|
public int PendingCount => _setPosition.PendingProjectionCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C3-1: the public host consumption shape for an initial-Create
|
||||||
|
/// continuation-executor drain's completion. Reached with the exact
|
||||||
|
/// <see cref="RuntimePlacementProjectionToken"/> carried by a
|
||||||
|
/// <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/> receipt
|
||||||
|
/// observed through <see cref="Subscribe"/> - the same correlation
|
||||||
|
/// identity (Entity/Sequence) every other placement Kind uses. Exposes
|
||||||
|
/// exactly the facts a cutover host needs to bind presentation off an
|
||||||
|
/// initial placement (the teleport-hook phase, the drained Position
|
||||||
|
/// continuations' route facts for constrain/interpolation binding, and
|
||||||
|
/// the replayed-deferred-child count) without widening any internal
|
||||||
|
/// Runtime type's accessibility. Returns false for a generation
|
||||||
|
/// mismatch or a stale/superseded/unknown token, mirroring every other
|
||||||
|
/// generation-gated method on this channel.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGetInitialCreateCompletion(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimePlacementProjectionToken token,
|
||||||
|
out RuntimeInitialCreatePlacementCompletion completion)
|
||||||
|
{
|
||||||
|
if (IsCurrent(expectedGeneration))
|
||||||
|
return _initialCreateExecution.TryGetCompletion(token, out completion);
|
||||||
|
completion = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsCurrent(RuntimeGenerationToken expectedGeneration) =>
|
private bool IsCurrent(RuntimeGenerationToken expectedGeneration) =>
|
||||||
_generationBound
|
_generationBound
|
||||||
&& expectedGeneration.Value != 0UL
|
&& expectedGeneration.Value != 0UL
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
@ -4319,6 +4320,264 @@ public sealed class RuntimeInitialCreateContinuationExecutorTests
|
||||||
observed[1].Token));
|
observed[1].Token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// C3-1: the public RuntimePlacementProjectionChannel host consumption
|
||||||
|
// surface for an executor completion (TryGetInitialCreateCompletion).
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlacementChannel_TryGetInitialCreateCompletion_ProjectsHookPhaseCellAndReplayCount()
|
||||||
|
{
|
||||||
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
||||||
|
Bind(lifetime, 420UL);
|
||||||
|
const uint guid = 0x70024020u;
|
||||||
|
RuntimeEntityRecord canonical = lifetime
|
||||||
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
||||||
|
.Canonical!;
|
||||||
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
||||||
|
canonical,
|
||||||
|
out RuntimeInitialCreateResidenceLease lease));
|
||||||
|
AttachDormantBody(lifetime, canonical);
|
||||||
|
CompleteInitialPlacement(lifetime, lease);
|
||||||
|
|
||||||
|
var observed = new List<RuntimePlacementProjectionSnapshot>();
|
||||||
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
||||||
|
new PlacementObserver(delta => observed.Add(delta.Placement)));
|
||||||
|
|
||||||
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
||||||
|
lifetime, canonical, lease.Token, NoContact);
|
||||||
|
|
||||||
|
RuntimePlacementProjectionSnapshot completion = Assert.Single(observed);
|
||||||
|
var generation = new RuntimeGenerationToken(420UL);
|
||||||
|
Assert.True(lifetime.Placements.TryGetInitialCreateCompletion(
|
||||||
|
generation,
|
||||||
|
completion.Token,
|
||||||
|
out RuntimeInitialCreatePlacementCompletion publicCompletion));
|
||||||
|
Assert.Equal(canonical.Key, publicCompletion.Entity);
|
||||||
|
Assert.Equal(receipt.Entity, publicCompletion.Entity);
|
||||||
|
Assert.Equal(receipt.FullCellId, publicCompletion.FullCellId);
|
||||||
|
Assert.Equal(receipt.ReplayedDeferredChildCount, publicCompletion.ReplayedDeferredChildCount);
|
||||||
|
// This is a local-player Create (login): retail's init_player path
|
||||||
|
// requests the AfterEnterWorld teleport hook (see RunInitialTail) -
|
||||||
|
// the exact fact route-1/8's cutover caller needs to know whether to
|
||||||
|
// run the after-enter teleport suffix. No Position continuation ran
|
||||||
|
// in this scenario, so the route-fact array projects empty.
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeInitialCreateTeleportHookPhase.AfterEnterWorld,
|
||||||
|
publicCompletion.TeleportHookPhase);
|
||||||
|
Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, receipt.TeleportHookPhase);
|
||||||
|
Assert.Empty(publicCompletion.PositionRouteFacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlacementChannel_TryGetInitialCreateCompletion_ProjectsPositionRouteFactsForConstrainInterpolationBinding()
|
||||||
|
{
|
||||||
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
||||||
|
Bind(lifetime, 421UL);
|
||||||
|
const uint guid = 0x70024021u;
|
||||||
|
RuntimeEntityRecord canonical = lifetime
|
||||||
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
||||||
|
.Canonical!;
|
||||||
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
||||||
|
canonical,
|
||||||
|
out RuntimeInitialCreateResidenceLease lease));
|
||||||
|
AttachDormantBody(lifetime, canonical);
|
||||||
|
CompleteInitialPlacement(lifetime, lease);
|
||||||
|
|
||||||
|
// A teleport-advanced Position continuation performs its own
|
||||||
|
// authored SetPosition and lands a Position trace entry with real
|
||||||
|
// route facts - the same scenario as
|
||||||
|
// ExecutorCompletion_ObservedOnlyAfterAnyContinuationPlacementInFifoOrder.
|
||||||
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
||||||
|
guid, positionSequence: 2, teleportSequence: 1,
|
||||||
|
forcePositionSequence: 0, positionX: 40f);
|
||||||
|
Assert.True(lifetime.TryApplyPosition(
|
||||||
|
update, isLocalPlayer: true, null, null, true, null,
|
||||||
|
out PositionTimestampDisposition disposition, out _, out _));
|
||||||
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
||||||
|
|
||||||
|
var observed = new List<RuntimePlacementProjectionSnapshot>();
|
||||||
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
||||||
|
new PlacementObserver(delta => observed.Add(delta.Placement)));
|
||||||
|
|
||||||
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
||||||
|
lifetime, canonical, lease.Token, NoContact);
|
||||||
|
|
||||||
|
RuntimePlacementProjectionSnapshot executorCompletion = observed[^1];
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimePlacementProjectionKind.ExecutorCompleted,
|
||||||
|
executorCompletion.Kind);
|
||||||
|
|
||||||
|
RuntimeInitialCreateExecutedAction internalPositionTrace = Assert.Single(
|
||||||
|
receipt.Trace.Where(
|
||||||
|
static a => a.Kind == RuntimeInitialCreateExecutedActionKind.Position));
|
||||||
|
|
||||||
|
var generation = new RuntimeGenerationToken(421UL);
|
||||||
|
Assert.True(lifetime.Placements.TryGetInitialCreateCompletion(
|
||||||
|
generation,
|
||||||
|
executorCompletion.Token,
|
||||||
|
out RuntimeInitialCreatePlacementCompletion publicCompletion));
|
||||||
|
RuntimeInitialCreatePositionRouteFact fact = Assert.Single(
|
||||||
|
publicCompletion.PositionRouteFacts);
|
||||||
|
Assert.Equal(internalPositionTrace.Sequence, fact.Sequence);
|
||||||
|
Assert.Equal(internalPositionTrace.StopInterpolating, fact.StopInterpolating);
|
||||||
|
Assert.Equal(internalPositionTrace.ZeroVelocity, fact.ZeroVelocity);
|
||||||
|
Assert.Equal(internalPositionTrace.PreserveHeading, fact.PreserveHeading);
|
||||||
|
Assert.Equal(
|
||||||
|
internalPositionTrace.SendPositionImmediately,
|
||||||
|
fact.SendPositionImmediately);
|
||||||
|
Assert.Equal(
|
||||||
|
internalPositionTrace.PositionDisposition!.Value.ToString(),
|
||||||
|
fact.Disposition.ToString());
|
||||||
|
Assert.Equal(
|
||||||
|
internalPositionTrace.ConstrainPhase.ToString(),
|
||||||
|
fact.ConstrainPhase.ToString());
|
||||||
|
Assert.Equal(
|
||||||
|
internalPositionTrace.HookPhase.ToString(),
|
||||||
|
fact.HookPhase.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlacementChannel_TryGetInitialCreateCompletion_RejectsWrongGeneration()
|
||||||
|
{
|
||||||
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
||||||
|
Bind(lifetime, 422UL);
|
||||||
|
const uint guid = 0x70024022u;
|
||||||
|
RuntimeEntityRecord canonical = lifetime
|
||||||
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
||||||
|
.Canonical!;
|
||||||
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
||||||
|
canonical,
|
||||||
|
out RuntimeInitialCreateResidenceLease lease));
|
||||||
|
AttachDormantBody(lifetime, canonical);
|
||||||
|
CompleteInitialPlacement(lifetime, lease);
|
||||||
|
|
||||||
|
var observed = new List<RuntimePlacementProjectionSnapshot>();
|
||||||
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
||||||
|
new PlacementObserver(delta => observed.Add(delta.Placement)));
|
||||||
|
RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
||||||
|
RuntimePlacementProjectionSnapshot completion = Assert.Single(observed);
|
||||||
|
|
||||||
|
Assert.False(lifetime.Placements.TryGetInitialCreateCompletion(
|
||||||
|
new RuntimeGenerationToken(999UL),
|
||||||
|
completion.Token,
|
||||||
|
out RuntimeInitialCreatePlacementCompletion stale));
|
||||||
|
Assert.Equal(default, stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlacementChannel_TryGetInitialCreateCompletion_ReturnsFalseAfterAcknowledgeReapsTheCorrelationEntry()
|
||||||
|
{
|
||||||
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
||||||
|
Bind(lifetime, 423UL);
|
||||||
|
const uint guid = 0x70024023u;
|
||||||
|
RuntimeEntityRecord canonical = lifetime
|
||||||
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
||||||
|
.Canonical!;
|
||||||
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
||||||
|
canonical,
|
||||||
|
out RuntimeInitialCreateResidenceLease lease));
|
||||||
|
AttachDormantBody(lifetime, canonical);
|
||||||
|
CompleteInitialPlacement(lifetime, lease);
|
||||||
|
RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
||||||
|
out RuntimePlacementProjectionSnapshot completion));
|
||||||
|
var generation = new RuntimeGenerationToken(423UL);
|
||||||
|
Assert.True(lifetime.Placements.TryGetInitialCreateCompletion(
|
||||||
|
generation,
|
||||||
|
completion.Token,
|
||||||
|
out _));
|
||||||
|
|
||||||
|
Assert.True(lifetime.Placements.Acknowledge(generation, completion.Token));
|
||||||
|
|
||||||
|
Assert.False(lifetime.Placements.TryGetInitialCreateCompletion(
|
||||||
|
generation,
|
||||||
|
completion.Token,
|
||||||
|
out RuntimeInitialCreatePlacementCompletion afterAck));
|
||||||
|
Assert.Equal(default, afterAck);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Review fix (2026-08-02): the compiler's exhaustiveness net for the
|
||||||
|
/// three enum-mirror switches (<c>MapHookPhase</c>/<c>MapDisposition</c>/
|
||||||
|
/// <c>MapConstrainPhase</c>) is gone the moment a catch-all arm exists -
|
||||||
|
/// that is exactly why those catch-alls now throw instead of silently
|
||||||
|
/// defaulting. This reflection-based test is the runtime replacement:
|
||||||
|
/// for each (internal enum, public projection enum, private mapper
|
||||||
|
/// method) triple it asserts equal arity AND drives every declared
|
||||||
|
/// internal value through the mapper via reflection (the methods are
|
||||||
|
/// `private static`), asserting the mapped public value's NAME equals
|
||||||
|
/// the internal value's name (every mapper is a literal 1:1 name
|
||||||
|
/// mirror by design - see each public enum's own doc comment). Adding a
|
||||||
|
/// new member to either enum without updating the other and the mapper
|
||||||
|
/// fails this test immediately, mirroring
|
||||||
|
/// <c>OperationResetAllFieldsToDefaultTouchesEveryDeclaredField</c>'s
|
||||||
|
/// reflection-based completeness guard for
|
||||||
|
/// <see cref="RuntimeSetPositionState"/>'s pooled Operation fields.
|
||||||
|
/// Sabotage-verified during development: temporarily adding an extra
|
||||||
|
/// member to <c>RuntimeTeleportHookPhase</c> (with no matching arm in
|
||||||
|
/// <c>MapHookPhase</c> or the public
|
||||||
|
/// <see cref="RuntimeInitialCreateTeleportHookPhase"/> enum) failed this
|
||||||
|
/// test exactly as predicted - both the arity assertion and the
|
||||||
|
/// unhandled-value invocation threw - before the sabotage was reverted.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void EnumProjectionMapsHaveEqualArityAndEveryInternalValueRoundTripsByName()
|
||||||
|
{
|
||||||
|
AssertMapIsCompleteAndNamePreserving(
|
||||||
|
typeof(RuntimeTeleportHookPhase),
|
||||||
|
typeof(RuntimeInitialCreateTeleportHookPhase),
|
||||||
|
"MapHookPhase");
|
||||||
|
AssertMapIsCompleteAndNamePreserving(
|
||||||
|
typeof(RuntimeAuthoritativePositionDisposition),
|
||||||
|
typeof(RuntimeInitialCreatePositionDisposition),
|
||||||
|
"MapDisposition");
|
||||||
|
AssertMapIsCompleteAndNamePreserving(
|
||||||
|
typeof(RuntimePositionConstrainPhase),
|
||||||
|
typeof(RuntimeInitialCreatePositionConstrainPhase),
|
||||||
|
"MapConstrainPhase");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertMapIsCompleteAndNamePreserving(
|
||||||
|
Type internalEnumType,
|
||||||
|
Type publicEnumType,
|
||||||
|
string mapMethodName)
|
||||||
|
{
|
||||||
|
MethodInfo? method = typeof(RuntimeInitialCreateContinuationExecutor)
|
||||||
|
.GetMethod(
|
||||||
|
mapMethodName,
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static);
|
||||||
|
Assert.True(
|
||||||
|
method is not null,
|
||||||
|
$"{nameof(RuntimeInitialCreateContinuationExecutor)} no longer " +
|
||||||
|
$"declares a private static method named {mapMethodName} - " +
|
||||||
|
"update this test's reflection lookup to match.");
|
||||||
|
|
||||||
|
Array internalValues = Enum.GetValues(internalEnumType);
|
||||||
|
Array publicValues = Enum.GetValues(publicEnumType);
|
||||||
|
// Equal arity: every internal value must have exactly one public
|
||||||
|
// counterpart and vice versa. A mismatch here is the first sign
|
||||||
|
// either enum grew without the other (or the mapper) being updated
|
||||||
|
// to match.
|
||||||
|
Assert.True(
|
||||||
|
internalValues.Length == publicValues.Length,
|
||||||
|
$"{internalEnumType.Name} has {internalValues.Length} values " +
|
||||||
|
$"but {publicEnumType.Name} has {publicValues.Length} - keep " +
|
||||||
|
"the internal/public enum pair in lockstep.");
|
||||||
|
|
||||||
|
foreach (object? internalValue in internalValues)
|
||||||
|
{
|
||||||
|
// Invoked via reflection deliberately - a value this mapper
|
||||||
|
// cannot handle now throws ArgumentOutOfRangeException (see
|
||||||
|
// the mapper's own doc comment), which TargetInvocationException
|
||||||
|
// propagates through Invoke and fails this test with a clear
|
||||||
|
// message identifying exactly which enum member is unmapped.
|
||||||
|
object? mapped = method!.Invoke(null, [internalValue]);
|
||||||
|
Assert.Equal(internalValue!.ToString(), mapped!.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExecutorCompletion_ReceiptIsReadableFromWithinTheSameSynchronousOnPlacementDispatch()
|
public void ExecutorCompletion_ReceiptIsReadableFromWithinTheSameSynchronousOnPlacementDispatch()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue