"use strict"; const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const test = require("node:test"); const manualFinancialUiModule = require("../../Web/manual-financial-ui.js"); const manualFinancialWorkflow = require("../../Web/manual-financial-workflow.js"); const manualListsUiModule = require("../../Web/manual-lists-ui.js"); const manualListsWorkflow = require("../../Web/manual-lists-workflow.js"); const repositoryRoot = path.resolve(__dirname, "..", ".."); const read = relativePath => fs.readFileSync(path.join(repositoryRoot, relativePath), "utf8"); const app = read("Web/app.js"); const index = read("Web/index.html"); const manualFinancialUi = read("Web/manual-financial-ui.js"); const mainWindow = read("MainWindow.xaml.cs"); const manualFinancialBridge = read("MainWindow.ManualFinancial.cs"); const manualListsBridge = read("MainWindow.ManualLists.cs"); const namedPlaylistBridge = read("MainWindow.NamedPlaylists.cs"); const operatorCatalogBridge = read("MainWindow.OperatorCatalogs.cs"); const playoutBridge = read("MainWindow.Playout.cs"); const operatorMutationGate = read("src/MBN_STOCK_WEBVIEW.Core/Playout/OperatorMutationGate.cs"); function functionBody(source, name, nextName) { const start = source.indexOf(`function ${name}(`); assert.ok(start >= 0, `${name} must exist`); const end = nextName ? source.indexOf(`function ${nextName}(`, start + 1) : source.length; assert.ok(end > start, `${name} must precede ${nextName || "the end of the file"}`); return source.slice(start, end); } function sliceBetween(source, startMarker, endMarker) { const start = source.indexOf(startMarker); const end = source.indexOf(endMarker, start + startMarker.length); assert.ok(start >= 0, `${startMarker} must exist`); assert.ok(end > start, `${endMarker} must follow ${startMarker}`); return source.slice(start, end); } function assertOrdered(source, values) { let previous = -1; for (const value of values) { const offset = source.indexOf(value); assert.ok(offset >= 0, `${value} must be loaded`); assert.ok(offset > previous, `${value} must preserve dependency order`); assert.equal(source.indexOf(value, offset + value.length), -1, `${value} must be loaded once`); previous = offset; } } test("operator styles and scripts load once in dependency order before app.js", () => { assertOrdered(index, [ 'href="manual-lists-ui.css"', 'href="manual-financial-ui.css"', 'href="operator-catalog-ui.css"' ]); assertOrdered(index, [ 'src="manual-lists-workflow.js"', 'src="manual-financial-workflow.js"', 'src="operator-catalog-workflow.js"', 'src="manual-lists-ui.js"', 'src="manual-financial-ui.js"', 'src="operator-catalog-ui.js"', 'src="app.js"' ]); }); test("legacy GraphE, FSell, VIList, ThemeA and EList controls open their real controllers", () => { assert.match(index, /id="themeCatalogManageButton"/u); assert.match(index, /id="expertCatalogManageButton"/u); const fixedManual = functionBody(app, "openFixedManualAction", "addFixedAction"); assert.match(fixedManual, /action\.builderKey === "s5025"[\s\S]*manualListsUi\?\.openNetSell\(action\.selection\?\.groupCode\)/u); assert.match(fixedManual, /action\.builderKey === "s5074"[\s\S]*action\.selection\?\.groupCode === "PAGED_VI"[\s\S]*manualListsUi\?\.openVi\(\)/u); const stockRender = functionBody(app, "renderStockWorkflow", "requestStockSearch"); assert.match(stockRender, /for \(const action of operatorWorkflow\.manualStockActions\)/u); assert.match(stockRender, /button\.disabled = !search\.selected \|\| operatorUiLocked\(\)/u); assert.match(stockRender, /manualFinancialUi\?\.open\(action\)/u); assert.match(app, /themeCatalogManageButton\.addEventListener\("click", \(\) => \{[\s\S]*operatorCatalogUi\?\.openTheme\(context \|\| undefined\)/u); assert.match(app, /expertCatalogManageButton\.addEventListener\("click", \(\) => \{[\s\S]*operatorCatalogUi\?\.openExpert\(context \|\| undefined\)/u); assert.match(app, /themeCatalogManageButton\.disabled = operatorUiLocked\(\)/u); assert.match(app, /expertCatalogManageButton\.disabled = operatorUiLocked\(\)/u); }); test("first-routing consumes only messages owned by an operator controller", () => { const nativeHandler = functionBody(app, "handleNativeMessage", "bindEvents"); const financialOffset = nativeHandler.indexOf("manualFinancialUi?.handleMessage"); const listsOffset = nativeHandler.indexOf("manualListsUi?.handleMessage"); const catalogOffset = nativeHandler.indexOf("operatorCatalogUi?.handleMessage"); const switchOffset = nativeHandler.indexOf("switch (message.type)"); assert.ok(financialOffset >= 0 && financialOffset < listsOffset); assert.ok(listsOffset < catalogOffset && catalogOffset < switchOffset); let id = 0; const financial = manualFinancialUiModule.createController({ postNative() {}, appendPlaylistEntry() { return true; }, isLocked() { return false; }, getSelectedStock() { return null; }, showToast() {}, addLog() {}, createId() { id += 1; return `test-${id}`; } }); const unownedFinancialTypes = [ manualFinancialWorkflow.bridgeContract.listResultType, manualFinancialWorkflow.bridgeContract.listErrorType, manualFinancialWorkflow.bridgeContract.loadResultType, manualFinancialWorkflow.bridgeContract.loadErrorType, "stock-search-results", "stock-search-error", manualFinancialWorkflow.bridgeContract.selectionResultType, manualFinancialWorkflow.bridgeContract.selectionErrorType, manualFinancialWorkflow.bridgeContract.mutationResultType, manualFinancialWorkflow.bridgeContract.mutationErrorType, manualFinancialWorkflow.bridgeContract.requestErrorType ]; for (const type of unownedFinancialTypes) { assert.equal(financial.handleMessage(type, { requestId: "not-owned" }), false, type); } const lists = manualListsUiModule.createController({ postNative() {}, appendPlaylistEntry() { return true; }, isLocked() { return false; }, showToast() {}, addLog() {}, createId() { id += 1; return `test-${id}`; } }); for (const type of [ manualListsWorkflow.bridgeContract.statusResultType, manualListsWorkflow.bridgeContract.netSellDataType, manualListsWorkflow.bridgeContract.netSellSaveResultType, manualListsWorkflow.bridgeContract.viDataType, manualListsWorkflow.bridgeContract.viSaveResultType, manualListsWorkflow.bridgeContract.errorType, "stock-search-results", "stock-search-error" ]) { assert.equal(lists.handleMessage(type, { requestId: "not-owned" }), false, type); } }); test("operator locks and trusted append checks cover every migrated manual surface", () => { const lock = functionBody(app, "operatorUiLocked", "selectedStockForManualFinancial"); assert.match(lock, /isPlaylistSnapshotLocked\(\)/u); assert.match(lock, /namedPlaylistBusy\(\)/u); assert.match(lock, /state\.manualFinancialRestore\.entries\.size > 0/u); const renderPlayout = functionBody(app, "renderPlayout", "clearPlayoutError"); for (const controller of ["manualListsUi", "manualFinancialUi", "operatorCatalogUi"]) { assert.match(renderPlayout, new RegExp(`${controller}\\?\\.setLocked\\(operatorLocked\\)`)); } const append = functionBody(app, "appendTrustedOperatorEntry", "isPlaylistSnapshotLocked"); assert.match(append, /if \(operatorUiLocked\(\)\) throw/u); assert.match(append, /manualListsWorkflow\.restorePlaylistEntry\(entry\)/u); assert.match(append, /manualFinancialWorkflow\.isPlaylistEntryPlayable\(entry\)/u); assert.match(append, /catalog\.find\(value => value\.builderKey === entry\.builderKey &&[\s\S]*sceneAliases\(value\)\.includes/u); assert.match(append, /state\.playlist\.push\(entry\)/u); assert.match(manualFinancialUi, /if \(!workflow\.isPlaylistEntryPlayable\(entry\)\) throw new TypeError\("Untrusted entry\."\)/u); }); test("stored GraphE entries remain unplayable until exact non-truncated DB revalidation", () => { const normalizeStored = functionBody(app, "normalizeStoredPlaylist", "manualFinancialRestoreRecordForRequest"); assert.match(normalizeStored, /manualFinancialWorkflow\.restorePlaylistEntry\(item\)/u); assert.match(normalizeStored, /options\.manualFinancialRestores\.push\(pendingManualFinancialEntry\)/u); assert.match(normalizeStored, /operatorInputBuilderKeys\.has\(String\(item\.builderKey \|\| ""\)\) \|\| item\.operator !== undefined[\s\S]*return \[\]/u); const stockRestore = functionBody( app, "handleManualFinancialRestoreStockResults", "handleManualFinancialRestoreStockError"); assert.match(stockRestore, /response\.query\s*[!=]==?\s*record\.request\.query|record\.request\.query\s*[!=]==?\s*response\.query/u); assert.match(stockRestore, /response\.truncated/u); assert.match(stockRestore, /response\.totalRowCount[\s\S]*record\.request\.maximumResults/u); assert.match(stockRestore, /const expectedMarket = [\s\S]*record\.pending\.verifiedStock\.market/u); assert.match(stockRestore, /const expectedCode = [\s\S]*record\.pending\.verifiedStock\.code/u); assert.match(stockRestore, /verifyStockForRecord\([\s\S]*expectedMarket,[\s\S]*expectedCode/u); assert.match(stockRestore, /record\.restoreKind === "named-financial"[\s\S]*namedMatch\.length === 1/u); const prepareBlock = functionBody(app, "manualFinancialPrepareBlocked", "loadPlaylist"); assert.match(prepareBlock, /state\.manualFinancialRestore\.entries\.size > 0/u); assert.match(prepareBlock, /manualFinancialWorkflow\.isPlaylistEntryPlayable\(item\)/u); assert.match(app, /prepareButton\.disabled = [^;]*manualFinancialBlocked/u); const requestCommand = functionBody(app, "requestPlayoutCommand", "requestPlayoutStatus"); assert.match(requestCommand, /if \(command === "prepare"\)[\s\S]*if \(manualFinancialPrepareBlocked\(\)\)[\s\S]*return false/u); }); test("native root routes all operator families and invalidates every browser correlation", () => { assert.match(mainWindow, /TryHandleOperatorCatalogRequest\(request\.Type, request\.Payload\)/u); assert.match(mainWindow, /TryHandleManualFinancialRequest\(request\.Type, request\.Payload\)/u); assert.match(mainWindow, /case "request-manual-operator-data-status"/u); assert.match(mainWindow, /case "save-manual-net-sell-data"/u); assert.match(mainWindow, /case "save-vi-manual-list"/u); assert.match(mainWindow, /InvalidateOperatorCatalogBrowserRequests\(\)/u); assert.match(mainWindow, /InvalidateManualOperatorBrowserRequests\(\)/u); assert.match(mainWindow, /InvalidateManualFinancialBrowserRequests\(\)/u); assert.match(manualFinancialBridge, /_playoutCommandGate\.WaitAsync\(0,/u); assert.match(manualFinancialBridge, /_databaseActivityGate\.WaitAsync\(0,/u); assert.match(operatorCatalogBridge, /_playoutCommandGate\.WaitAsync\(0,/u); assert.match(operatorCatalogBridge, /_databaseActivityGate\.WaitAsync\(0,/u); assert.match(manualListsBridge, /TryEnterManualMutationGatesAsync/u); assert.match(manualListsBridge, /_playoutCommandGate\.WaitAsync\(0\)/u); }); test("native operator writes share one fail-closed idle gate at their dispatch boundary", () => { const gateStart = playoutBridge.indexOf("private bool CanStartOperatorMutation("); const gateEnd = playoutBridge.indexOf("private void ShutdownPlayoutRuntime(", gateStart); assert.ok(gateStart >= 0 && gateEnd > gateStart); const gate = playoutBridge.slice(gateStart, gateEnd); assert.match(gate, /familyWriteQuarantined/u); assert.match(gate, /_lifetimeCancellation\.IsCancellationRequested/u); assert.match(gate, /_playoutCommandInFlight/u); assert.match(gate, /_playoutShutdownStarted/u); assert.match(gate, /IsBrowserCorrelationQuarantined\(\)/u); assert.match(gate, /IsEngineAvailable: engine is not null/u); assert.match(gate, /IsWorkflowAvailable: workflow is not null/u); assert.match(gate, /IsCommandAvailable: status\?\.IsCommandAvailable \?\? false/u); assert.match(gate, /IsPlayCompletionPending: status\?\.IsPlayCompletionPending \?\? true/u); assert.match(gate, /PreparedSceneName: status\?\.PreparedSceneName/u); assert.match(gate, /OnAirSceneName: status\?\.OnAirSceneName/u); assert.match(gate, /PreparedCutCode: workflowState\?\.PreparedCutCode/u); assert.match(gate, /OnAirCutCode: workflowState\?\.OnAirCutCode/u); assert.match(gate, /IsRefreshActive: refreshStatus\.IsActive/u); assert.match(gate, /IsRefreshTaskRunning: _legacyRefreshTask is \{ IsCompleted: false \}/u); assert.match(gate, /OperatorMutationGate\.CanStart\(snapshot\)/u); assert.match(operatorMutationGate, /snapshot is not null/u); assert.match(operatorMutationGate, /snapshot\.IsEngineAvailable/u); assert.match(operatorMutationGate, /snapshot\.IsWorkflowAvailable/u); assert.match(manualFinancialBridge, /CanStartOperatorMutation\(IsManualFinancialWriteQuarantined\(\)\)/u); assert.ok((manualFinancialBridge.match(/CanStartManualFinancialWrite\(\)/gu) || []).length >= 3); assert.match(operatorCatalogBridge, /CanStartOperatorMutation\(IsOperatorCatalogWriteQuarantined\(\)\)/u); assert.ok((operatorCatalogBridge.match(/CanStartOperatorCatalogWrite\(\)/gu) || []).length >= 2); assert.match(manualListsBridge, /CanStartOperatorMutation\(IsManualOperatorWriteQuarantined\(\)\)/u); assert.ok((manualListsBridge.match( /CanStartOperatorMutation\(IsManualOperatorWriteQuarantined\(\)\)/gu) || []).length >= 3); for (const write of [ sliceBetween( manualListsBridge, "private async Task HandleManualNetSellWriteAsync(", "private async Task HandleViManualListReadAsync("), sliceBetween( manualListsBridge, "private async Task HandleViManualListWriteAsync(", "private async Task TryEnterManualMutationGatesAsync(") ]) { assert.match(write, /TryEnterManualMutationGatesAsync\([\s\S]*Interlocked\.Exchange\(ref _manualOperatorWriteInFlight, 1\)[\s\S]*await (?:store\.)?Write/su); } }); test("an unknown write outcome globally quarantines GraphE, ThemeA/EList, FSell/VI and named playlists", () => { assert.match(playoutBridge, /private int _operatorMutationQuarantined;[\s\S]*private string\? _operatorMutationQuarantineMessage;/u); assert.match(playoutBridge, /private bool IsOperatorMutationQuarantined\(\) =>[\s\S]*_operatorMutationQuarantined/u); assert.match(playoutBridge, /private void LatchOperatorMutationQuarantine\(string message\)[\s\S]*Interlocked\.CompareExchange\(ref _operatorMutationQuarantined, 1, 0\)/u); for (const [surface, source, familyCheck, familyLatch] of [ ["GraphE", manualFinancialBridge, "IsManualFinancialWriteQuarantined", "LatchManualFinancialWriteQuarantine"], ["ThemeA/EList", operatorCatalogBridge, "IsOperatorCatalogWriteQuarantined", "LatchOperatorCatalogWriteQuarantine"], ["FSell/VI", manualListsBridge, "IsManualOperatorWriteQuarantined", "QuarantineManualOperatorWrites"], ["named playlist", namedPlaylistBridge, "IsNamedPlaylistWriteQuarantined", "LatchNamedPlaylistWriteQuarantine"] ]) { assert.match(source, new RegExp( `private bool ${familyCheck}\\(\\) =>[\\s\\S]{0,180}IsOperatorMutationQuarantined\\(\\)`, "u"), `${surface} must observe the process-wide quarantine`); assert.match(source, new RegExp( `private void ${familyLatch}\\(string message\\)[\\s\\S]{0,260}LatchOperatorMutationQuarantine\\(message\\)`, "u"), `${surface} must latch the process-wide quarantine`); } }); test("malformed operator payloads retain a family-specific fail-closed fallback", () => { assert.match(manualFinancialBridge, /TryHandleManualFinancialRequest\(string\? requestType, JsonElement payload\)[\s\S]*case "create-manual-financial-record"[\s\S]*return true/u); assert.match(manualFinancialBridge, /private static string SafeManualFinancialRequestId\(JsonElement payload\) =>[\s\S]*payload\.ValueKind == JsonValueKind\.Object/u); assert.match(operatorCatalogBridge, /TryHandleOperatorCatalogRequest\(string\? requestType, JsonElement payload\)[\s\S]*case "create-theme-catalog"[\s\S]*case "create-expert-catalog"[\s\S]*return true/u); assert.match(operatorCatalogBridge, /private static string SafeOperatorCatalogRequestId\(JsonElement payload\)[\s\S]*payload\.ValueKind == JsonValueKind\.Object/u); for (const [parser, nextParser, readToken] of [ ["TryParseAudienceRequest", "SafeManualOperatorRequestId", 'GetString(payload, "audience")'], ["TryParseManualNetSellWrite", "TryParseViWrite", 'GetString(payload, "audience")'], ["TryParseViWrite", "TryGetViExpectedVersion", "TryGetViExpectedVersion(payload"] ]) { const body = sliceBetween( manualListsBridge, `private static bool ${parser}(`, `private static ${nextParser === "SafeManualOperatorRequestId" ? "string" : "bool"} ${nextParser}(`); assert.match(body, /requestId = SafeManualOperatorRequestId\(payload\)/u, parser); const objectGuard = body.indexOf("payload.ValueKind"); const firstPropertyRead = body.indexOf(readToken); assert.ok(objectGuard >= 0 && firstPropertyRead > objectGuard, `${parser} must check object kind before reading a property`); } assert.ok((manualListsBridge.match(/"INVALID_REQUEST"/gu) || []).length >= 5); const viVersion = sliceBetween( manualListsBridge, "private static bool TryGetViExpectedVersion(", "private static bool TryGetManualValue("); assert.ok(viVersion.indexOf("payload.ValueKind") < viVersion.indexOf('payload.TryGetProperty("expectedVersion"')); assert.match(mainWindow, /case "request-named-playlist-list":[\s\S]*case "delete-named-playlist":[\s\S]*PostNamedPlaylistError\([\s\S]*"INVALID_REQUEST"/u); }); test("window shutdown detaches and disposes both trusted manual stores after their gate is idle", () => { const closed = sliceBetween(mainWindow, "private void OnClosed(", "private sealed record WebRequest("); assert.match(closed, /ShutdownManualOperatorDataRuntime\(\)/u); const shutdown = sliceBetween( manualListsBridge, "private void ShutdownManualOperatorDataRuntime(", "private async Task DisposeManualOperatorStoresWhenIdleAsync("); assert.match(shutdown, /InvalidateManualOperatorBrowserRequests\(\)/u); assert.match(shutdown, /Interlocked\.Exchange\(ref _manualNetSellStore, null\)/u); assert.match(shutdown, /Interlocked\.Exchange\(ref _viManualListStore, null\)/u); assert.match(shutdown, /DisposeManualOperatorStoresWhenIdleAsync\(netSellStore, viStore\)/u); const dispose = sliceBetween( manualListsBridge, "private async Task DisposeManualOperatorStoresWhenIdleAsync(", "private void InvalidateManualOperatorBrowserRequests("); assert.match(dispose, /await _manualOperatorDataGate\.WaitAsync\(\)/u); assert.match(dispose, /viStore\?\.Dispose\(\)/u); assert.match(dispose, /netSellStore\?\.Dispose\(\)/u); assert.match(dispose, /_manualOperatorDataGate\.Release\(\)/u); }); test("GraphE async UI boundaries retain the dispatcher context before posting WebView replies", () => { assert.doesNotMatch(manualFinancialBridge, /operationBody\(requestToken\)\.ConfigureAwait\(false\)/u); assert.doesNotMatch(manualFinancialBridge, /preflight\(requestToken\)\.ConfigureAwait\(false\)/u); assert.doesNotMatch(manualFinancialBridge, /operationBody\(service,\s*requestToken\)\.ConfigureAwait\(false\)/u); });