namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyGateAAppIntegrationContractTests { private static readonly string RepositoryRoot = FindRepositoryRoot(); private static readonly string AppRoot = Path.Combine( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp"); private static readonly string Window = Read(AppRoot, "MainWindow.xaml.cs"); private static readonly string Playout = Read(AppRoot, "MainWindow.Playout.cs"); private static readonly string IntentParser = Read( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyBridge", "LegacyPlayoutUiIntents.cs"); private static readonly string Factory = Read( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.Playout", "PlayoutEngineFactory.cs"); private static readonly string LaunchAuthorization = Read( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.Playout", "Safety", "PlayoutLaunchAuthorization.cs"); private static readonly string InfrastructureRoot = Path.Combine( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.Infrastructure", "Execution"); [Fact] public void Launch_authorization_is_captured_before_WebView_creation_and_forwarded_unchanged() { var constructor = Slice( Window, " public MainWindow()", " private async void OnRootLoaded("); var capture = RequiredIndex( constructor, "PlayoutLaunchAuthorization.CaptureFromEnvironment()"); var initializeComponent = RequiredIndex(constructor, "InitializeComponent();"); Assert.True(capture < initializeComponent); Assert.Contains( "_playoutLaunchAuthorization =\n" + " PlayoutLaunchAuthorization.CaptureFromEnvironment();", Normalize(constructor), StringComparison.Ordinal); var initialization = Normalize(Slice( Playout, " private void InitializePlayoutRuntime()", " private async Task ConnectPlayoutAsync(")); Assert.Contains( "_playoutEngine = PlayoutEngineFactory.Create(\n" + " _playoutOptions,\n" + " _playoutLaunchAuthorization);", initialization, StringComparison.Ordinal); var factoryCreate = Slice( Normalize(Factory), " public static IPlayoutEngine Create(\n" + " PlayoutOptions options,\n" + " PlayoutLaunchAuthorization launchAuthorization)", " internal static void ValidateGateAOptions("); Assert.Contains("dispatcher,\n launchAuthorization,", factoryCreate, StringComparison.Ordinal); Assert.Contains("launchAuthorization: launchAuthorization", factoryCreate, StringComparison.Ordinal); } [Fact] public void Gate_A_has_a_dedicated_capability_intent_and_exact_Samsung_5001_prepare_handler() { Assert.Contains( "public sealed record LegacyGateAPrepareIntent(string Capability)", IntentParser, StringComparison.Ordinal); Assert.Contains("\"gate-a-prepare\" => ParseGateAPrepare(payload)", IntentParser, StringComparison.Ordinal); var parser = Slice( IntentParser, " private static LegacyUiIntent? ParseGateAPrepare(JsonElement payload)", " private static LegacyUiIntent? ParseFadeDuration(JsonElement payload)"); Assert.Contains("payload.EnumerateObject().Count() != 1", parser, StringComparison.Ordinal); Assert.Contains("capability.Length != 64", parser, StringComparison.Ordinal); Assert.Contains("not (>= 'A' and <= 'F')", parser, StringComparison.Ordinal); var dispatch = Normalize(Slice( Window, " private async Task HandleIntentAsync(", " private async Task RefreshNamedPlaylistsForDispatchAsync(")); Assert.Contains( "LegacyGateAPrepareIntent prepare =>\n" + " await ExecuteGateAPrepareAsync(\n" + " prepare.Capability,", dispatch, StringComparison.Ordinal); var handler = Slice( Playout, " private async Task ExecuteGateAPrepareAsync(", " private bool TryCreateExactGateAPrepareRequest("); Assert.Contains("TryClaimGateAPrepare(capability)", handler, StringComparison.Ordinal); Assert.Contains("TryCreateExactGateAPrepareRequest(out var request)", handler, StringComparison.Ordinal); Assert.Contains("gateAPrepareClaimed: true", handler, StringComparison.Ordinal); Assert.Contains("gateAPrepareRequest: request", handler, StringComparison.Ordinal); Assert.Contains("CompleteGateAPrepare()", handler, StringComparison.Ordinal); var exact = Slice( Playout, " private bool TryCreateExactGateAPrepareRequest(", " private async Task SetFadeDurationAsync("); foreach (var required in new[] { "snapshot.Playlist.Count != 1", "composition.FadeDuration != 6", "composition.BackgroundKind != LegacySceneBackgroundKind.None", "row.MarketText, \"코스피\"", "row.StockName, \"삼성전자\"", "row.PageText, \"1/1\"", "row.StockCode, \"005930\"", "_controller.CreatePlayoutRequest(6)", "candidate.SelectedIndexZeroBased != 0", "entry.CutCode, \"5001\"", "entry.FadeDuration != 6", "entry.PageNavigation?.IsEnabled == true", "selection.GroupCode, \"코스피\"", "selection.Subject, \"삼성전자\"", "selection.DataCode, \"005930\"" }) { Assert.Contains(required, exact, StringComparison.Ordinal); } } [Fact] public void Ordinary_playout_intents_are_rejected_in_Gate_A_before_workflow_access() { var execute = Slice( Playout, " private async Task ExecuteOperatorPlayoutAsync(", " private async Task ExecuteGateAPrepareAsync("); var workflowRead = RequiredIndex(execute, "var workflow = _playoutWorkflow;"); var preWorkflowGuard = execute[..workflowRead]; Assert.Contains("_playoutLaunchAuthorization.IsGateA", preWorkflowGuard, StringComparison.Ordinal); Assert.Contains("!gateAPrepareClaimed", preWorkflowGuard, StringComparison.Ordinal); Assert.Contains("command != LegacyOperatorPlayoutCommand.Prepare", preWorkflowGuard, StringComparison.Ordinal); Assert.Contains("gateAPrepareRequest is null", preWorkflowGuard, StringComparison.Ordinal); Assert.Contains("return _controller.ReportError(", preWorkflowGuard, StringComparison.Ordinal); Assert.DoesNotContain("_playoutWorkflow", preWorkflowGuard, StringComparison.Ordinal); Assert.DoesNotContain("CreatePlayoutRequest", preWorkflowGuard, StringComparison.Ordinal); Assert.DoesNotContain("_playoutCommandGate.WaitAsync", preWorkflowGuard, StringComparison.Ordinal); var dispatch = Slice( Window, " private async Task HandleIntentAsync(", " private async Task RefreshNamedPlaylistsForDispatchAsync("); Assert.Contains("LegacyExecutePlayoutIntent playout =>", dispatch, StringComparison.Ordinal); Assert.Contains("ExecuteOperatorPlayoutAsync(\n playout.Command,", Normalize(dispatch), StringComparison.Ordinal); } [Fact] public void All_database_mutation_executors_receive_the_same_launch_denial_policy() { var constructor = Slice( Window, " public MainWindow()", " private async void OnRootLoaded("); Assert.Contains( "new LaunchDatabaseMutationAuthorization(_playoutLaunchAuthorization)", constructor, StringComparison.Ordinal); Assert.True( CountOccurrences( constructor, "mutationAuthorization: databaseMutationAuthorization") == 3, "The same launch authorization must be injected into all three DB mutation executors."); var adapter = Slice( Window, " private sealed class LaunchDatabaseMutationAuthorization", " private void PostState("); Assert.Contains(": IDatabaseMutationAuthorization", adapter, StringComparison.Ordinal); Assert.Contains("return _authorization.AllowsDatabaseWrites;", adapter, StringComparison.Ordinal); Assert.Contains("_authorization = authorization ??", adapter, StringComparison.Ordinal); foreach (var executorFile in new[] { "OracleManualFinancialMutationExecutor.cs", "OracleNamedPlaylistMutationExecutor.cs", "OperatorCatalogMutationExecutor.cs" }) { var executor = File.ReadAllText(Path.Combine(InfrastructureRoot, executorFile)); var demand = RequiredIndex(executor, "DatabaseMutationAuthorizationGuard.Demand("); var open = RequiredIndex(executor, "await connection.OpenAsync("); Assert.True(demand < open, $"{executorFile} must deny before opening a DB connection."); Assert.Contains("_mutationAuthorization", executor, StringComparison.Ordinal); Assert.Contains("IDatabaseMutationAuthorization mutationAuthorization", executor, StringComparison.Ordinal); } } [Fact] public void Gate_A_blocks_persistent_write_intents_before_dispatch() { var process = Normalize(Slice( Window, " private async Task ProcessIntentAsync(LegacyUiIntent intent)", " private static bool IsNamedPlaylistModalIntent(")); var guard = RequiredIndex( process, "_playoutLaunchAuthorization.IsGateA &&\n IsPersistentWriteIntent(intent)"); var dispatchStarted = RequiredIndex(process, "dispatchStarted = true;"); var dispatch = RequiredIndex(process, "HandleIntentAsync(intent"); Assert.True(guard < dispatchStarted); Assert.True(guard < dispatch); var guardEnd = process.IndexOf("return;", guard, StringComparison.Ordinal); Assert.True(guardEnd > guard); Assert.Contains("ReportIntentFailure(", process[guard..guardEnd], StringComparison.Ordinal); var persistent = Slice( Playout, " private static bool IsPersistentWriteIntent(LegacyUiIntent intent)", " private static bool IsFixedSectionBatchMutationIntent("); foreach (var intent in new[] { "LegacyDeleteUc4SelectedThemeIntent", "LegacyDeleteUc6SelectedExpertIntent", "LegacySaveOperatorCatalogIntent", "LegacyConfirmNativeDialogIntent", "LegacyAddComparisonPairIntent", "LegacySaveManualFinancialIntent", "LegacySaveManualNetSellIntent", "LegacySaveManualViIntent", "LegacyImportManualListsIntent", "LegacyCreateNamedPlaylistIntent", "LegacySaveCurrentNamedPlaylistIntent", "LegacyDeleteSelectedNamedPlaylistIntent" }) { Assert.Contains(intent, persistent, StringComparison.Ordinal); } } [Fact] public void Gate_A_blocks_fade_and_background_mutation_before_any_command_gate() { Assert.Contains("public bool AllowsCompositionMutation => !IsGateA;", LaunchAuthorization, StringComparison.Ordinal); var fade = Slice( Playout, " private async Task SetFadeDurationAsync(", " private LegacyOperatorSnapshot SelectPlaylistBoundaryAndStopRefresh("); AssertCompositionMutationGuard( fade, "Gate A 검증 회차에서는 Fade 설정을 변경할 수 없습니다."); var toggle = Slice( Playout, " private async Task ToggleBackgroundAsync(", " private async Task ChooseBackgroundAsync("); AssertCompositionMutationGuard( toggle, "Gate A 검증 회차에서는 배경 설정을 변경할 수 없습니다."); var choose = Slice( Playout, " private async Task ChooseBackgroundAsync(", " private bool CanChangeComposition()"); AssertCompositionMutationGuard( choose, "Gate A 검증 회차에서는 배경 파일을 선택할 수 없습니다."); } private static void AssertCompositionMutationGuard(string method, string message) { var authorization = RequiredIndex( method, "if (!_playoutLaunchAuthorization.AllowsCompositionMutation)"); var commandGate = RequiredIndex(method, "_playoutCommandGate.WaitAsync"); Assert.True(authorization < commandGate); Assert.Contains(message, method[authorization..commandGate], StringComparison.Ordinal); Assert.Contains("return _controller.ReportError(", method[authorization..commandGate], StringComparison.Ordinal); } private static int CountOccurrences(string source, string marker) { var count = 0; var index = 0; while ((index = source.IndexOf(marker, index, StringComparison.Ordinal)) >= 0) { count++; index += marker.Length; } return count; } 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 Read(params string[] parts) => File.ReadAllText(Path.Combine(parts)); private static string FindRepositoryRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory); while (current is not null) { if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln"))) { return current.FullName; } current = current.Parent; } throw new DirectoryNotFoundException("Repository root was not found."); } }