feat: advance legacy UI behavior parity

This commit is contained in:
2026-07-18 10:18:29 +09:00
parent d7ec571343
commit 1302b1b92f
71 changed files with 11244 additions and 1153 deletions

View File

@@ -72,6 +72,177 @@ public sealed class LegacyPlayoutNativeContractTests
Assert.Contains("WaitForPlayCompletionAsync", Playout, StringComparison.Ordinal);
}
[Fact]
public void AutomaticRefreshUsesTheCoreSchedulerAndGenerationOwnedState()
{
Assert.Contains("new LegacyRefreshScheduler(", Playout, StringComparison.Ordinal);
Assert.Contains(
"LegacyRefreshEpoch<LegacyRefreshRuntimeStatus>",
Playout,
StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.ReadState()", Playout, StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.Replace(", Playout, StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.TryUpdate(", Playout, StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.TryComplete(", Playout, StringComparison.Ordinal);
Assert.DoesNotContain("Task.Delay(", Playout, StringComparison.Ordinal);
foreach (var obsoleteRawField in new[]
{
"_refreshCancellation",
"_refreshCts",
"_refreshStateGate",
"_refreshActive",
"_refreshCompletedCount",
"_refreshMaximumCount",
"_refreshLimitReached",
"_refreshNextAtUtc",
"_refreshLastSuccessAtUtc",
"_refreshFaultCode",
"_refreshMessage"
})
{
Assert.DoesNotContain(obsoleteRawField, Playout, StringComparison.Ordinal);
}
}
[Fact]
public void ZeroRefreshLimitReturnsBeforeCreatingAnEpochOrLoop()
{
var start = Slice(
Playout,
"private void StartRefreshLoop(LegacyPlayoutWorkflow workflow)",
"private void StopRefreshLoop()");
var limitCheck = RequiredIndex(start, "if (scheduler.HasReachedMaximum)");
var linkedEpoch = RequiredIndex(
start,
"CancellationTokenSource.CreateLinkedTokenSource(");
var loopAssignment = RequiredIndex(
start,
"_refreshTask = RunRefreshLoopAsync(workflow, scheduler, cancellation);");
Assert.True(limitCheck < linkedEpoch);
Assert.True(linkedEpoch < loopAssignment);
var zeroLimitBranch = start[limitCheck..linkedEpoch];
Assert.Contains("_refreshTask = Task.CompletedTask;", zeroLimitBranch,
StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.Stop(", zeroLimitBranch, StringComparison.Ordinal);
Assert.Contains("return;", zeroLimitBranch, StringComparison.Ordinal);
Assert.DoesNotContain("RunRefreshLoopAsync(", zeroLimitBranch,
StringComparison.Ordinal);
}
[Fact]
public void AutomaticRefreshSerializesDbThenPlayoutAndPublishesBothBusyStates()
{
var loop = Slice(
Playout,
"private async Task RunRefreshLoopAsync(",
"private bool TrySetRefreshFault(");
var intentWait = RequiredIndex(loop, "await _intentGate.WaitAsync(token)");
var intentBusy = RequiredIndex(loop, "_intentBusy = true;", intentWait);
var intentBusyState = RequiredIndex(loop, "QueueOperatorState();", intentBusy);
var playoutWait = RequiredIndex(
loop,
"await _playoutCommandGate.WaitAsync(token)",
intentBusyState);
var playoutBusy = RequiredIndex(
loop,
"Interlocked.Exchange(ref _playoutBusy, 1);",
playoutWait);
var playoutBusyState = RequiredIndex(loop, "QueueOperatorState();", playoutBusy);
var refreshDispatch = RequiredIndex(
loop,
"workflow.RefreshOnAirAsync",
playoutBusyState);
Assert.True(intentWait < intentBusy);
Assert.True(intentBusy < intentBusyState);
Assert.True(intentBusyState < playoutWait);
Assert.True(playoutWait < playoutBusy);
Assert.True(playoutBusy < playoutBusyState);
Assert.True(playoutBusyState < refreshDispatch);
}
[Fact]
public void KnownRefreshInputFailuresStopWithoutQuarantineButUnknownsDoQuarantine()
{
var loop = Normalize(Slice(
Playout,
"private async Task RunRefreshLoopAsync(",
"private bool TrySetRefreshFault("));
var databaseFailure = Slice(
loop,
"catch (DatabaseInfrastructureException)",
"catch (LegacySceneDataException)");
var sceneDataFailure = Slice(
loop,
"catch (LegacySceneDataException)",
"catch (ArgumentException)");
var argumentFailure = Slice(
loop,
"catch (ArgumentException)",
"catch\n {");
var unknownFailure = Slice(
loop,
"catch\n {",
"finally");
AssertKnownFailureDoesNotQuarantine(
databaseFailure,
"DATABASE_UNAVAILABLE");
AssertKnownFailureDoesNotQuarantine(
sceneDataFailure,
"SCENE_DATA_INVALID");
AssertKnownFailureDoesNotQuarantine(
argumentFailure,
"SCENE_ARGUMENT_INVALID");
Assert.Contains("REFRESH_OUTCOME_UNKNOWN", unknownFailure,
StringComparison.Ordinal);
Assert.Contains("QuarantinePlayoutAsync", unknownFailure,
StringComparison.Ordinal);
Assert.Contains(
"result.Code is PlayoutResultCode.OutcomeUnknown or\n" +
" PlayoutResultCode.TimedOut",
loop,
StringComparison.Ordinal);
AssertEveryFaultBranchQuarantines(loop, "PLAY_CALLBACK_TIMEOUT");
}
[Fact]
public void ExternalIdleStatusResetsNormalRefreshState()
{
var handler = Slice(
Playout,
"private void OnPlayoutStatusChanged(",
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(");
Assert.Contains(
"string.IsNullOrWhiteSpace(args.Current.PreparedSceneName)",
handler,
StringComparison.Ordinal);
Assert.Contains(
"string.IsNullOrWhiteSpace(args.Current.OnAirSceneName)",
handler,
StringComparison.Ordinal);
Assert.Contains(
"args.Current.State == PlayoutConnectionState.OutcomeUnknown",
handler,
StringComparison.Ordinal);
var stop = RequiredIndex(handler, "StopRefreshLoop();");
var normalElse = RequiredIndex(handler, "else", stop);
var reset = RequiredIndex(handler, "ResetRefreshState();", normalElse);
Assert.True(stop < normalElse && normalElse < reset);
var resetHelper = Slice(
Playout,
"private void ResetRefreshState()",
"private async Task RunRefreshLoopAsync(");
Assert.Contains(
"_refreshEpoch.Stop(_ => LegacyRefreshRuntimeStatus.Empty",
resetHelper,
StringComparison.Ordinal);
}
[Fact]
public void UnknownOutcomeQuarantinesWithoutAutomaticCommandRetry()
{
@@ -143,6 +314,58 @@ public sealed class LegacyPlayoutNativeContractTests
StringComparison.Ordinal);
}
private static void AssertKnownFailureDoesNotQuarantine(
string failureBlock,
string expectedFaultCode)
{
Assert.Contains(expectedFaultCode, failureBlock, StringComparison.Ordinal);
Assert.DoesNotContain(
"QuarantinePlayoutAsync",
failureBlock,
StringComparison.Ordinal);
}
private static void AssertEveryFaultBranchQuarantines(
string source,
string faultCode)
{
var occurrence = 0;
var count = 0;
while ((occurrence = source.IndexOf(
faultCode,
occurrence,
StringComparison.Ordinal)) >= 0)
{
var branchEnd = source.IndexOf("return;", occurrence, StringComparison.Ordinal);
Assert.True(branchEnd > occurrence, $"{faultCode} branch has no return.");
Assert.Contains(
"QuarantinePlayoutAsync",
source[occurrence..branchEnd],
StringComparison.Ordinal);
count++;
occurrence = branchEnd + "return;".Length;
}
Assert.True(count > 0, $"No {faultCode} branch was found.");
}
private static string Slice(string source, string startMarker, string endMarker)
{
var start = RequiredIndex(source, startMarker);
var end = RequiredIndex(source, endMarker, start + startMarker.Length);
return source[start..end];
}
private static int RequiredIndex(string source, string marker, int startIndex = 0)
{
var index = source.IndexOf(marker, startIndex, StringComparison.Ordinal);
Assert.True(index >= 0, $"Required source marker was not found: {marker}");
return index;
}
private static string Normalize(string source) =>
source.Replace("\r\n", "\n", StringComparison.Ordinal);
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);