namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyPackageInputSmokeContractTests { private static readonly string Runner = File.ReadAllText(Path.Combine( FindRepositoryRoot(), "scripts", "Invoke-LegacyPackageInputSmoke.ps1")); private static readonly string Harness = File.ReadAllText(Path.Combine( FindRepositoryRoot(), "scripts", "Test-LegacyPackageInput.mjs")); [Fact] public void WindowsInputEvidenceIsMaterializedBeforePowerShellArrayWrapping() { Assert.Contains( "WindowsInput = @($windowsInputEvidence.ToArray())", Runner, StringComparison.Ordinal); Assert.DoesNotContain( "WindowsInput = @($windowsInputEvidence)", Runner, StringComparison.Ordinal); } [Fact] public void DryRunFinalSealKeepsWindowsDragOrderAndRequiresTheSafeTailAppend() { var seal = Slice( Runner, "$expectedAfterOrder = @(", "$expectedFullUiDbPlaylistCount ="); Assert.Contains("$expectedFinalOrder = @($expectedAfterOrder)", seal, StringComparison.Ordinal); Assert.Contains("if ($HarnessProfile -ceq 'DryRunPlayout')", seal, StringComparison.Ordinal); Assert.Contains("[string]$evidence.dryRunPlayout.tailAppendedEntryId", seal, StringComparison.Ordinal); Assert.Equal( 2, seal.Split( "Test-ExactStringSequence $statePlaylistIds $expectedFinalOrder", StringSplitOptions.None).Length - 1 + seal.Split( "Test-ExactStringSequence $domPlaylistIds $expectedFinalOrder", StringSplitOptions.None).Length - 1); Assert.DoesNotContain( "Test-ExactStringSequence $statePlaylistIds $expectedAfterOrder", seal, StringComparison.Ordinal); Assert.DoesNotContain( "Test-ExactStringSequence $domPlaylistIds $expectedAfterOrder", seal, StringComparison.Ordinal); } [Fact] public void DryRunSealPinsProgramEnableAndShortcutEvidenceExactly() { var seal = Slice( Runner, "$playoutIntentContractOk =", "if ([int]$evidence.schemaVersion"); foreach (var token in new[] { "programEnableParity.currentEntryId", "programEnableParity.skippedEntryId", "programEnableParity.nextEntryId", "programEnableParity.setAllIntentCount -eq 2", "programEnableParity.toggleActiveIntentCount -eq 1", "programEnableParity.endOfPlaylistObserved -eq $true", "shortcutParity.f8IntentCount -eq 1", "shortcutParity.escapeIntentCount -eq 1", "shortcutParity.finalPhase -ceq 'idle'", "[string]$_ -ceq 'set-all-playlist-enabled'", "[string]$_ -ceq 'toggle-active-playlist-enabled'" }) { Assert.Contains(token, seal, StringComparison.Ordinal); } var allowlist = Slice( Runner, "$allowedOutbound = @(", "foreach ($messageType"); Assert.Contains("'set-all-playlist-enabled'", allowlist, StringComparison.Ordinal); Assert.Contains("'toggle-active-playlist-enabled'", allowlist, StringComparison.Ordinal); } [Fact] public void NamedPlaylistRecoveryIsStrictlyScopedForwardedAndSealed() { Assert.Contains("[string]$RecoverNamedPlaylistTitle", Runner, StringComparison.Ordinal); Assert.Contains("'^CDX_P_[0-9]{13}_[0-9a-f]{6}$'", Runner, StringComparison.Ordinal); Assert.Contains("$RecoverNamedPlaylistTitle.Length -ne 26", Runner, StringComparison.Ordinal); Assert.Contains("$Profile -cne 'FullUiDb'", Runner, StringComparison.Ordinal); Assert.Contains("$FullUiDbScope -cne 'PList'", Runner, StringComparison.Ordinal); Assert.Contains("$FullUiDbScope -cne 'All'", Runner, StringComparison.Ordinal); Assert.Contains("'--recover-named-playlist-title'", Runner, StringComparison.Ordinal); var seal = Slice( Runner, "$expectedRecoveryRequested =", "$databaseWriteContractOk ="); AssertInOrder( seal, "$recoveryEvidence.targetTitle -cne $ExpectedRecoveryTitle", "$recoveryEvidence.freshListVerified -ne $true", "$recoveryEvidence.exactAbsenceVerified -ne $true", "$sealedRecoveryMatchCount -ne 0", "$recoveryEvidence.result -ceq 'alreadyAbsent'", "$recoveryEvidence.writeCount -eq 0", "$namedPlaylistDeleteWriteCount -eq 1", "$recoveryEvidence.result -ceq 'deleted'", "$recoveryEvidence.writeCount -eq 1", "@('committedFresh', 'committedOptimistic')", "$namedPlaylistDeleteWriteCount -eq 2"); Assert.Contains("-not $namedPlaylistRecoveryContractOk", Runner, StringComparison.Ordinal); } [Fact] public void NamedPlaylistRecoveryUsesOneConfirmedDeleteWithoutRetryThenRunsCrud() { Assert.Contains("\"recover-named-playlist-title\"", Harness, StringComparison.Ordinal); Assert.Contains("/^CDX_P_[0-9]{13}_[0-9a-f]{6}$/", Harness, StringComparison.Ordinal); Assert.Contains("recoverNamedPlaylistTitle.length !== 26", Harness, StringComparison.Ordinal); Assert.Contains("new Set([\"all\", \"plist\"])", Harness, StringComparison.Ordinal); Assert.Contains( "completedNamedPlaylistRefreshAfter(snapshot, recoveryRefreshBaseline)", Harness, StringComparison.Ordinal); var recovery = Slice( Harness, "async function recoverNamedPlaylistFixture", "async function executeNamedPlaylistCrud"); AssertInOrder( recovery, "if (initialWriteCount !== 0)", "PList recovery fresh list open", "completedNamedPlaylistRefreshAfter(snapshot, recoveryRefreshBaseline)", "current.state.namedPlaylist.isListTruncated !== false", "recovery.freshListVerified = true", "const exactMatches =", "if (exactMatches.length > 1)", "if (exactMatches.length === 0)", "recovery.result = \"alreadyAbsent\"", "PList recovery exact target select", "snapshot.state.namedPlaylist.selectedDefinitionId === target.definitionId", "PList recovery open existing delete confirmation", "snapshot.dom.namedConfirmationHidden === false", "PList recovery confirm exact delete once", "writeCount === 1", "outcome === \"committedFresh\" || outcome === \"committedOptimistic\"", "recovery.result = \"deleted\"", "recovery.writeCount = recoveryWriteCount", "recovery.exactAbsenceVerified = true"); Assert.Equal( 1, recovery.Split( "document.getElementById(\"named-playlist-confirmation-yes\")", StringSplitOptions.None).Length - 1); Assert.Contains("failUnknown(`Timed out waiting for ${label}; no action was retried.", Harness, StringComparison.Ordinal); Assert.Contains("named.isWriteQuarantined === true", Harness, StringComparison.Ordinal); Assert.Contains("named.lastMutationOutcome === \"outcomeUnknown\"", Harness, StringComparison.Ordinal); var sequence = Slice( Harness, "async function executeFullUiDbSequence", "try {"); AssertInOrder( sequence, "await recoverNamedPlaylistFixture();", "await executeNamedPlaylistCrud();"); } [Fact] public void FullUiDbSafetyGateAllowsAndClassifiesEveryManualFinancialWriteIntent() { var allowed = Slice( Harness, "const fullUiDbAllowedOutboundMessageTypes", "const playoutIntentMessageTypes"); var databaseWrites = Slice( Harness, "const databaseWriteIntentMessageTypes", "const forbiddenCdpMethods"); foreach (var intent in new[] { "save-manual-financial", "save-manual-financial-raw", "delete-manual-financial", "delete-all-manual-financial" }) { Assert.Contains($"\"{intent}\"", allowed, StringComparison.Ordinal); Assert.Contains($"\"{intent}\"", databaseWrites, StringComparison.Ordinal); } } [Fact] public void FullUiDbCatalogChecksUseTheOriginalDirectInputGestures() { Assert.Contains("async function addOperatorCatalogStockByDoubleClick", Harness, StringComparison.Ordinal); Assert.Contains("interClickDelayMilliseconds", Harness, StringComparison.Ordinal); Assert.Contains("market === \"krx\" ? 350 : 50", Harness, StringComparison.Ordinal); Assert.Contains("await pressKey(\"Delete\"", Harness, StringComparison.Ordinal); Assert.Contains("async function dragOperatorCatalogRow", Harness, StringComparison.Ordinal); Assert.Contains("text/x-mbn-operator-catalog-row", Harness, StringComparison.Ordinal); var theme = Slice( Harness, "async function createReadEditDeleteTheme", "async function searchAndSelectMainExpert"); AssertInOrder( theme, "addOperatorCatalogStockByDoubleClick(", "third row Up", "third row Down", "focus third row for Delete key", "pressKey(\"Delete\""); var expert = Slice( Harness, "async function createReadEditDeleteExpert", "async function executeOperatorCatalogCrud"); AssertInOrder( expert, "addOperatorCatalogStockByDoubleClick(", "second row Up", "second row Down", "dragOperatorCatalogRow(", "focus second row for Delete key", "pressKey(\"Delete\""); var fixedCatalog = Slice( Harness, "async function verifyFixedAndIndustryScreens", "async function verifyComparisonScreen"); Assert.Contains("UC1 fixed parent batch", fixedCatalog, StringComparison.Ordinal); Assert.Contains("fixedSection.actions.length", fixedCatalog, StringComparison.Ordinal); Assert.Contains("beforeSection.safety.outboundMessages.length + 1", fixedCatalog, StringComparison.Ordinal); Assert.DoesNotContain("original no-op", fixedCatalog, StringComparison.Ordinal); } [Fact] public void FullUiDbKrxThemeCreatePhysicallyRoundTripsTheMarketWithoutLosingTheName() { var allowed = Slice( Harness, "const fullUiDbAllowedOutboundMessageTypes", "const playoutIntentMessageTypes"); Assert.Contains("\"change-create-theme-market\"", allowed, StringComparison.Ordinal); var roundTrip = Slice( Harness, "async function verifyKrxThemeCreateMarketRoundTrip", "async function createReadEditDeleteTheme"); AssertInOrder( roundTrip, "operatorCatalogEditorMarketValue !== \"krx\"", "draftRows?.some(row => row.rowId === draftRowId)", "switch editor market to NXT", "newThemeMarket === \"nxt\"", "editorName === title", "draftRows?.length === 0", "statusKind === \"warning\"", "operatorCatalogNameValue === title", "change-create-theme-market", "switch editor market back to KRX", "newThemeMarket === \"krx\"", "operatorCatalogNameValue === title", "removedDraftRowId: draftRowId"); var create = Slice( Harness, "async function createReadEditDeleteTheme", "async function searchAndSelectMainExpert"); AssertInOrder( create, "let added = await addOperatorCatalogStockByDoubleClick(", "await verifyKrxThemeCreateMarketRoundTrip(", "duplicate recheck after market round-trip", "name available after market round-trip", "stock re-add after market round-trip", "let expectedSavedRows = 1"); } [Fact] public void DryRunProgramParityUsesActualKeysAndTheOriginalEnabledStateSplit() { var dryRunAllowed = Slice( Harness, ": dryRunPlayoutProfile", ": readOnlyAllowedOutboundMessageTypes;"); Assert.Contains("\"set-all-playlist-enabled\"", dryRunAllowed, StringComparison.Ordinal); Assert.Contains("\"toggle-active-playlist-enabled\"", dryRunAllowed, StringComparison.Ordinal); Assert.DoesNotContain("\"set-playlist-enabled\"", dryRunAllowed, StringComparison.Ordinal); var authorization = Slice( Harness, "function authorizeDryRunPlayoutShortcut", "async function setSearchText"); AssertInOrder( authorization, "new Set([\"F8\", \"Escape\"])", "const expectedPhase = definitionName === \"F8\" ? \"idle\" : \"program\"", "playout.isCommandAvailable !== true", "playout.outcomeUnknown === true", "playout.isPlayCompletionPending === true", "playout.isTakeOutCompletionPending === true", "authorization.consumed === true", "Date.now() - authorization.issuedAt > 5_000", "authorization.consumed = true"); var sequence = Slice( Harness, "async function executeDryRunPlayoutSequence", "function recordFullUiDbCheck"); AssertInOrder( sequence, "const f8Authorization = authorizeDryRunPlayoutShortcut(", "\"rapid DryRun F8 after row selection\"", "PROGRAM clear every enabled row", "snapshot.state.playlist.every(row => row.isEnabled === false)", "snapshot.state.playout.nextKind === \"endOfPlaylist\"", "PROGRAM restore every enabled row", "snapshot.state.playlist.every(row => row.isEnabled === true)", "snapshot.state.playout.nextKind === \"playlistNext\"", "pointerClickDisabledPlaylistCheckbox(", "checkboxAriaDisabled === true", "pressKey(\"Space\"", "[\"toggle-active-playlist-enabled\"]", "DryRun NEXT skips the Space-disabled future row", "currentEntryId === nextEntryId", "const escapeAuthorization = authorizeDryRunPlayoutShortcut(", "\"DryRun Escape TAKE OUT after NEXT\"", "snapshot.state.playout.phase === \"idle\""); Assert.Contains("[\"set-all-playlist-enabled\"]", sequence, StringComparison.Ordinal); Assert.Contains("mouseCheckboxDisabled: true", sequence, StringComparison.Ordinal); Assert.Contains("setAllIntentCount: 2", sequence, StringComparison.Ordinal); Assert.Contains("toggleActiveIntentCount: 1", sequence, StringComparison.Ordinal); } [Fact] public void CatalogBlankStockSearchUsesButtonAndEnterAndSealsTheBoundedDisplay() { var boundedSearch = Slice( Harness, "async function verifyBlankOperatorCatalogStockSearch", "async function addOperatorCatalogStock"); AssertInOrder( boundedSearch, "replaceText(", "`${label} blank query`", "trigger === \"button\"", "operator-catalog-stock-search", "pressKey(\"Enter\"", "currentCatalog?.stockQuery === \"\"", "resultCount > 1 && resultCount <= 10_000", "statusMessage === \"종목 검색을 완료했습니다.\"", "operatorCatalogStockResultCount === resultCount", "[\"search-operator-catalog-stocks\"]", "messages[0].emptyStockQuery !== true", "resultCount !== 10_000 || !truncationVisible", "draftRowsPreserved: true"); var theme = Slice( Harness, "async function createReadEditDeleteTheme", "async function searchAndSelectMainExpert"); AssertInOrder( theme, "`${label} ThemeA editor`", "addOperatorCatalogStockByDoubleClick("); var expert = Slice( Harness, "async function createReadEditDeleteExpert", "async function executeOperatorCatalogCrud"); AssertInOrder( expert, "snapshot.state.operatorCatalog?.mode === \"edit\"", "`${label} existing UC6 editor`", "addOperatorCatalogStockByDoubleClick("); } [Fact] public void PhysicalPlaylistExchangeShiftSelectsDeletesAndRestoresTheExactThreeRows() { var request = Slice( Harness, "async function requestWindowsInput(", "async function installProbe"); AssertInOrder( request, "beforeOrder.length !== 5", "reorderedOrder.at(-2) !== rangeStartRowId", "const afterOrder = reorderedOrder.filter", "row-header-drag-home-shift-range-delete-restore", "expectedRangeSelectedRowIds: [rangeStartRowId, rangeEndRowId]", "mouseDownCalls: 4", "shiftKeyDownCalls: 1", "shiftKeyUpCalls: 1", "deleteKeyCalls: 1", "delete-selected-playlist-rows", "selections[1].shift !== true", "shiftRangeDeletedRowIds = [rangeStartRowId, rangeEndRowId]"); Assert.DoesNotContain("Input.dispatchMouseEvent", request, StringComparison.Ordinal); var native = Slice( Runner, "public static void SendShiftClickRangeThenDeleteAndRestore(", "public static void SendApplicationClick("); AssertInOrder( native, "SendKeyboard(VirtualKeyShift, false)", "SendMouseButton(MouseLeftDown)", "SendKeyboard(VirtualKeyShift, true)", "SendKeyboard(VirtualKeyDelete, false)", "SendKeyboard(VirtualKeyDelete, true)", "SendMouseMove(restore.X, restore.Y)"); } [Fact] public void FullUiDbPListUsesASecondPhysicalDoubleClickExchangeWithContentRestoration() { var request = Slice( Harness, "async function requestNamedPlaylistLoadDoubleClick", "function namedPlaylistDeleteWriteCount"); AssertInOrder( request, "configuration.windowsInputExchangeCount !== 2", "DB 재생목록 불러오기", "const beforeContent = playlistContentProjection(current)", "named-playlist-load-double-click", "mouseDownCalls: 2", "doubleClickIntervalMilliseconds: 100", "load-named-playlist-by-id", "JSON.stringify(playlistContentProjection(snapshot))", "loads.length !== 1", "approvedDatabaseWriteMessages.length !== beforeApprovedWriteCount", "contentRestored: true"); Assert.DoesNotContain("pointerDoubleClick", request, StringComparison.Ordinal); Assert.DoesNotContain("Input.dispatchMouseEvent", request, StringComparison.Ordinal); var runnerExchange = Slice( Runner, "function Get-ExpectedWindowsInputExchangeCount", "function Invoke-NodeHarnessUnderTcpWatch"); AssertInOrder( runnerExchange, "return 2", "named-playlist-load-double-click", "Invoke-ExactNamedPlaylistDoubleClickRequest", "Write-WindowsInputAcknowledgement"); Assert.Contains("public static void SendApplicationDoubleClick(", Runner, StringComparison.Ordinal); Assert.Contains("for (int click = 0; click < 2; click++)", Runner, StringComparison.Ordinal); Assert.Contains("PList physical double-click fresh readback", Runner, StringComparison.Ordinal); } [Fact] public void DebugAppXMustMatchTheLatestMsixAndCurrentWebSourceBeforeLaunch() { var guard = Slice( Runner, "function Assert-DebugAppXMatchesLatestPackage", "function Get-ExactDevelopmentPackage"); Assert.Contains("$BuildFlavor -cne 'DebugAppX'", guard, StringComparison.Ordinal); Assert.Contains("[IO.Compression.ZipFile]::OpenRead($msix)", guard, StringComparison.Ordinal); Assert.Contains("'Web/app.js'", guard, StringComparison.Ordinal); Assert.Contains("Get-StreamSha256Hex $entryStream", guard, StringComparison.Ordinal); Assert.Contains("registered Debug AppX is stale", guard, StringComparison.Ordinal); Assert.Contains("Web\\app.js", guard, StringComparison.Ordinal); Assert.Contains( "Assert-DebugAppXMatchesLatestPackage ([string]$package.InstallLocation)", Runner, StringComparison.Ordinal); } private static string Slice(string source, string start, string end) { var startIndex = source.IndexOf(start, StringComparison.Ordinal); var endIndex = source.IndexOf(end, startIndex, StringComparison.Ordinal); Assert.True(startIndex >= 0 && endIndex > startIndex); return source[startIndex..endIndex]; } private static void AssertInOrder(string source, params string[] markers) { var cursor = 0; foreach (var marker in markers) { var index = source.IndexOf(marker, cursor, StringComparison.Ordinal); Assert.True(index >= cursor, $"Marker was missing or out of order: {marker}"); cursor = index + marker.Length; } } private static string FindRepositoryRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "MBN_STOCK_WEBVIEW.sln"))) { return directory.FullName; } directory = directory.Parent; } throw new DirectoryNotFoundException("Repository root was not found."); } }