Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyPackageInputSmokeContractTests.cs

565 lines
23 KiB
C#

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.");
}
}