import fs from "node:fs"; import path from "node:path"; import { createHash, randomBytes } from "node:crypto"; const exactTargetUrl = "https://legacy-parity.mbn.local/index.html"; const ctrlModifier = 2; const shiftModifier = 8; const defaultObservationTimeoutMilliseconds = 45_000; const minimumObservationTimeoutMilliseconds = 5_000; const maximumObservationTimeoutMilliseconds = 300_000; const acceptedArgumentNames = new Set([ "port", "output", "screenshot", "windows-input-request", "windows-input-ack", "timeout-ms" ]); const forbiddenControlIds = Object.freeze([ "playout-prepare", "playout-take-in", "playout-next", "playout-take-out", "named-playlist-create-button", "named-playlist-delete-button", "named-playlist-confirmation-yes", "operator-catalog-save", "operator-catalog-delete" ]); const allowedOutboundMessageTypes = Object.freeze([ "ready", "search-stocks", "select-stock", "cut-pointer-down", "cut-key-down", "drop-selected-cuts", "activate-playlist-row", "select-playlist-row", "reorder-playlist-rows", "select-playlist-boundary", "refresh-named-playlists" ]); const playoutIntentMessageTypes = Object.freeze([ "prepare-playout", "take-in", "next-playout", "take-out", "choose-background", "toggle-background" ]); const databaseWriteIntentMessageTypes = Object.freeze([ "create-named-playlist", "delete-selected-named-playlist", "save-current-named-playlist-to", "save-manual-financial", "delete-manual-financial", "delete-all-manual-financial", "import-manual-lists", "confirm-manual-list", "save-operator-catalog", "delete-operator-catalog", "delete-uc4-selected-theme", "delete-uc6-selected-expert" ]); const forbiddenCdpMethods = new Set([ "Browser.close", "Page.close", "Runtime.terminateExecution", "Target.closeTarget" ]); class HarnessFailure extends Error { constructor(category, message) { super(`${category}: ${message}`); this.name = "HarnessFailure"; this.category = category; } } function failKnown(message) { throw new HarnessFailure("KNOWN_FAILURE", message); } function failUnknown(message) { throw new HarnessFailure("OUTCOME_UNKNOWN", message); } function parseArguments(argv) { if (argv.length === 0 || argv.length % 2 !== 0) { failKnown( "Usage: node scripts/Test-LegacyPackageInput.mjs --port " + "--output --screenshot " + "--windows-input-request " + "--windows-input-ack " + "[--timeout-ms ]"); } const values = new Map(); for (let index = 0; index < argv.length; index += 2) { const name = argv[index]; const value = argv[index + 1]; if (!name?.startsWith("--") || value === undefined || value.length === 0) { failKnown("Arguments must be non-empty --name value pairs."); } const key = name.slice(2); if (!acceptedArgumentNames.has(key)) { failKnown(`Unknown argument --${key}.`); } if (values.has(key)) { failKnown(`Argument --${key} was supplied more than once.`); } values.set(key, value); } if (!values.has("port") || !values.has("output") || !values.has("screenshot") || !values.has("windows-input-request") || !values.has("windows-input-ack")) { failKnown( "--port, --output, --screenshot, --windows-input-request, and " + "--windows-input-ack are required."); } const port = Number(values.get("port")); if (!Number.isInteger(port) || port < 1 || port > 65_535) { failKnown("--port must be an integer from 1 through 65535."); } const outputPath = path.resolve(values.get("output")); if (!path.isAbsolute(values.get("output")) || path.extname(outputPath).toLowerCase() !== ".json") { failKnown("--output must be an absolute .json path."); } const screenshotPath = path.resolve(values.get("screenshot")); if (!path.isAbsolute(values.get("screenshot")) || path.extname(screenshotPath).toLowerCase() !== ".png") { failKnown("--screenshot must be an absolute .png path."); } if (outputPath.toLowerCase() === screenshotPath.toLowerCase()) { failKnown("--output and --screenshot must name different files."); } const windowsInputRequestPath = path.resolve(values.get("windows-input-request")); const windowsInputAckPath = path.resolve(values.get("windows-input-ack")); for (const [name, candidate] of [ ["--windows-input-request", windowsInputRequestPath], ["--windows-input-ack", windowsInputAckPath] ]) { if (!path.isAbsolute(values.get(name.slice(2))) || path.extname(candidate).toLowerCase() !== ".json") { failKnown(`${name} must be an absolute .json path.`); } } const uniqueEvidencePaths = new Set([ outputPath.toLowerCase(), screenshotPath.toLowerCase(), windowsInputRequestPath.toLowerCase(), windowsInputAckPath.toLowerCase() ]); if (uniqueEvidencePaths.size !== 4) { failKnown("All result, screenshot, Windows-input request, and ack paths must differ."); } const timeoutMilliseconds = values.has("timeout-ms") ? Number(values.get("timeout-ms")) : defaultObservationTimeoutMilliseconds; if (!Number.isInteger(timeoutMilliseconds) || timeoutMilliseconds < minimumObservationTimeoutMilliseconds || timeoutMilliseconds > maximumObservationTimeoutMilliseconds) { failKnown( `--timeout-ms must be an integer from ${minimumObservationTimeoutMilliseconds} ` + `through ${maximumObservationTimeoutMilliseconds}.`); } if (fs.existsSync(outputPath) || fs.existsSync(screenshotPath) || fs.existsSync(windowsInputRequestPath) || fs.existsSync(windowsInputAckPath)) { failKnown("An evidence path already exists; evidence is never overwritten."); } fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.mkdirSync(path.dirname(screenshotPath), { recursive: true }); fs.mkdirSync(path.dirname(windowsInputRequestPath), { recursive: true }); fs.mkdirSync(path.dirname(windowsInputAckPath), { recursive: true }); return { port, outputPath, screenshotPath, windowsInputRequestPath, windowsInputAckPath, timeoutMilliseconds }; } function assertWithinHardDeadline(label) { if (externalCancellationRequested && !cleanupMode) { failUnknown(`The wrapper requested cancellation ${label}; no action is retried.`); } const deadline = cleanupMode ? cleanupDeadlineEpochMilliseconds : hardDeadlineEpochMilliseconds; if (Date.now() >= deadline) { failUnknown(`The global input deadline expired ${label}; no action is retried.`); } } async function sleep(milliseconds) { assertWithinHardDeadline("before waiting"); const activeDeadline = cleanupMode ? cleanupDeadlineEpochMilliseconds : hardDeadlineEpochMilliseconds; const remaining = activeDeadline - Date.now(); await new Promise(resolve => setTimeout(resolve, Math.min(milliseconds, remaining))); assertWithinHardDeadline("after waiting"); } function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex").toUpperCase(); } function errorProjection(error) { return { category: error instanceof HarnessFailure ? error.category : "HARNESS_ERROR", name: error?.name || "Error", message: String(error?.message || error) }; } const configuration = parseArguments(process.argv.slice(2)); const hardDeadlineEpochMilliseconds = Date.now() + configuration.timeoutMilliseconds; const startedAt = new Date().toISOString(); const evidence = { schemaVersion: 1, result: "RUNNING", startedAt, completedAt: null, target: { expectedUrl: exactTargetUrl, port: configuration.port, timeoutMilliseconds: configuration.timeoutMilliseconds, id: null, url: null }, safety: { requiredMode: "dryRun", requiredPhase: "idle", forbiddenControlIds, allowedOutboundMessageTypes, appCloseRequested: false, playoutIntentIssued: false, databaseWriteIntentIssued: false, observedOutboundMessageTypes: [], blockedOutboundMessages: [], stateViolations: [], externalCancellationRequested: false }, fixture: { query: "삼성", stockDisplayName: "삼성출판사" }, checkpoints: [], inputs: [], warnings: [], windowsInput: { required: true, requestPath: configuration.windowsInputRequestPath, acknowledgementPath: configuration.windowsInputAckPath, requestSha256: null, acknowledgementSha256: null, result: "PENDING" }, screenshot: null, finalSnapshot: null, error: null }; let socket = null; let nextRequestId = 1; const pendingRequests = new Map(); let externalCancellationRequested = false; let cancellationInputBuffer = ""; let pointerIsPressed = false; let pointerReleasePoint = null; let pointerReleaseModifiers = 0; let probeInstalled = false; let cleanupMode = false; let cleanupDeadlineEpochMilliseconds = 0; process.stdin.setEncoding("utf8"); process.stdin.on("data", chunk => { cancellationInputBuffer += chunk; if (!cancellationInputBuffer.split(/\r?\n/u).some(line => line === "CANCEL")) { if (cancellationInputBuffer.length > 64) cancellationInputBuffer = ""; return; } externalCancellationRequested = true; evidence.safety.externalCancellationRequested = true; for (const pending of pendingRequests.values()) { clearTimeout(pending.timer); pending.reject(new HarnessFailure( "OUTCOME_UNKNOWN", `Wrapper cancellation interrupted ${pending.method}; the command is not retried.`)); } pendingRequests.clear(); }); if (typeof process.stdin.unref === "function") process.stdin.unref(); async function discoverExactTarget() { const deadline = Date.now() + Math.min( configuration.timeoutMilliseconds, 30_000); const discoveryUrl = `http://127.0.0.1:${configuration.port}/json/list`; let lastFailure = null; while (Date.now() < deadline) { try { const response = await fetch(discoveryUrl, { redirect: "error", signal: AbortSignal.timeout(1_000) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const targets = await response.json(); if (!Array.isArray(targets)) { failKnown("CDP discovery did not return a target array."); } const exactTargets = targets.filter(target => target?.type === "page" && target.url === exactTargetUrl); if (exactTargets.length > 1) { failKnown("More than one exact LegacyParityApp WebView target exists."); } if (exactTargets.length === 1) { return exactTargets[0]; } } catch (error) { if (error instanceof HarnessFailure) throw error; lastFailure = String(error?.message || error); } await sleep(100); } failKnown(`The exact package WebView target was not found at ${discoveryUrl}` + (lastFailure ? ` (${lastFailure}).` : ".")); } function validateWebSocketTarget(target) { if (!target?.id || !target.webSocketDebuggerUrl) { failKnown("The exact target does not expose a WebSocket debugger URL."); } const endpoint = new URL(target.webSocketDebuggerUrl); if (endpoint.protocol !== "ws:" || endpoint.hostname !== "127.0.0.1" || Number(endpoint.port) !== configuration.port || endpoint.pathname !== `/devtools/page/${target.id}`) { failKnown("The CDP endpoint is not the exact loopback page endpoint."); } return endpoint; } async function connect(endpoint) { if (typeof WebSocket !== "function") { failKnown("This Node runtime does not provide the WebSocket API."); } socket = new WebSocket(endpoint.href); await new Promise((resolve, reject) => { const timer = setTimeout( () => reject(new HarnessFailure("OUTCOME_UNKNOWN", "CDP socket open timed out.")), 5_000); socket.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true }); socket.addEventListener("error", () => { clearTimeout(timer); reject(new HarnessFailure("OUTCOME_UNKNOWN", "CDP socket failed to open.")); }, { once: true }); }); socket.addEventListener("message", event => { const message = JSON.parse(String(event.data)); if (!Object.hasOwn(message, "id")) return; const pending = pendingRequests.get(message.id); if (!pending) return; pendingRequests.delete(message.id); clearTimeout(pending.timer); if (message.error) { pending.reject(new HarnessFailure( "KNOWN_FAILURE", `${pending.method} failed: ${message.error.message || "CDP error"}`)); } else { pending.resolve(message.result); } }); socket.addEventListener("close", () => { for (const pending of pendingRequests.values()) { clearTimeout(pending.timer); pending.reject(new HarnessFailure( "OUTCOME_UNKNOWN", `CDP socket closed while waiting for ${pending.method}.`)); } pendingRequests.clear(); }); } function rpc(method, params = {}, timeoutMilliseconds = 10_000) { assertWithinHardDeadline(`before ${method}`); if (cleanupMode && method !== "Input.dispatchMouseEvent" && method !== "Runtime.evaluate") { failKnown(`Cleanup refused unexpected CDP method ${method}.`); } if (forbiddenCdpMethods.has(method)) { evidence.safety.appCloseRequested = true; failKnown(`The harness refused forbidden CDP method ${method}.`); } if (!socket || socket.readyState !== WebSocket.OPEN) { failUnknown(`CDP socket is not open for ${method}.`); } const id = nextRequestId++; return new Promise((resolve, reject) => { const activeDeadline = cleanupMode ? cleanupDeadlineEpochMilliseconds : hardDeadlineEpochMilliseconds; const remaining = activeDeadline - Date.now(); const effectiveTimeout = cleanupMode ? Math.min(timeoutMilliseconds, 2_000, remaining) : Math.min(timeoutMilliseconds, remaining); const timer = setTimeout(() => { pendingRequests.delete(id); reject(new HarnessFailure( "OUTCOME_UNKNOWN", `${method} timed out; the command is not retried.`)); }, effectiveTimeout); pendingRequests.set(id, { method, resolve, reject, timer }); socket.send(JSON.stringify({ id, method, params })); }); } async function evaluate(expression) { const response = await rpc("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true, userGesture: false }); if (response.exceptionDetails) { const description = response.exceptionDetails.exception?.description || response.exceptionDetails.text || "Runtime evaluation failed."; failKnown(description); } return response.result?.value; } function probeSnapshotExpression() { return `(() => { const probe = globalThis.__legacyPackageInputProbe; const state = probe?.states?.at(-1) || null; const pointerEvents = probe?.pointerEvents || []; const lastPointerDown = [...pointerEvents].reverse().find(event => event.type === "pointerdown") || null; const activePointerId = Number.isInteger(lastPointerDown?.pointerId) ? lastPointerDown.pointerId : null; const capturedCutPhysicalIndices = activePointerId === null ? [] : Array.from(document.querySelectorAll("#cut-list .cut-row")) .filter(row => typeof row.hasPointerCapture === "function" && row.hasPointerCapture(activePointerId)) .map(row => Number(row.dataset.physicalIndex)); const capturedPlaylistRowIds = activePointerId === null ? [] : Array.from(document.querySelectorAll( "#playlist-rows .playlist-data-row .playlist-row-header")) .filter(header => typeof header.hasPointerCapture === "function" && header.hasPointerCapture(activePointerId)) .map(header => header.closest(".playlist-data-row")?.dataset?.rowId || null) .filter(Boolean); const active = document.activeElement; const rowForActive = active?.closest?.(".playlist-data-row") || null; const visibleModal = Array.from(document.querySelectorAll( "[role='dialog'], [role='alertdialog']")) .filter(element => !element.hidden && !element.closest("[hidden]")).at(-1) || null; const focusables = visibleModal ? Array.from(visibleModal.querySelectorAll( "button:not([disabled]), input:not([disabled]):not([type='hidden']), " + "select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])")) .filter(element => !element.hidden && !element.closest("[hidden]") && getComputedStyle(element).display !== "none" && getComputedStyle(element).visibility !== "hidden") : []; return { capturedAt: new Date().toISOString(), devicePixelRatio: window.devicePixelRatio, stateCount: probe?.stateCount ?? null, safety: { postMessageWrapped: probe?.postMessageWrapper != null && window.chrome?.webview?.postMessage === probe.postMessageWrapper, outboundMessages: (probe?.outboundMessages || []).map(message => ({ ...message })), blockedOutboundMessages: (probe?.blockedOutboundMessages || []) .map(message => ({ ...message })), stateViolations: (probe?.stateViolations || []).map(violation => ({ ...violation, reasons: [...violation.reasons] })) }, pointerEvents: pointerEvents.slice(-40).map(event => ({ ...event })), state: state ? { revision: state.revision, isBusy: state.isBusy, searchText: state.searchText, searchResults: (state.searchResults || []).map(row => ({ index: row.index, displayName: row.displayName })), selectedStockIndex: state.selectedStockIndex, cutRows: (state.cutRows || []).map(row => ({ physicalIndex: row.physicalIndex, displayOrdinal: row.displayOrdinal, rawLabel: row.rawLabel, isSeparator: row.isSeparator, isSelected: row.isSelected, isFocused: row.isFocused })), playlist: (state.playlist || []).map(row => ({ rowId: row.rowId, isEnabled: row.isEnabled, isSelected: row.isSelected, isActive: row.isActive, marketText: row.marketText, stockName: row.stockName, graphicType: row.graphicType, subtype: row.subtype, pageText: row.pageText })), dialog: state.dialog, statusMessage: state.statusMessage, statusKind: state.statusKind, commandResult: state.commandResult, namedPlaylist: state.namedPlaylist ? { revision: state.namedPlaylist.revision, definitions: (state.namedPlaylist.definitions || []).map(definition => ({ definitionId: definition.definitionId, title: definition.title, isSelected: definition.isSelected })), selectedDefinitionId: state.namedPlaylist.selectedDefinitionId, selectedTitle: state.namedPlaylist.selectedTitle, rowCount: state.namedPlaylist.rowCount, isWriteInProgress: state.namedPlaylist.isWriteInProgress, isWriteQuarantined: state.namedPlaylist.isWriteQuarantined, canMutate: state.namedPlaylist.canMutate, canCreate: state.namedPlaylist.canCreate, canLoad: state.namedPlaylist.canLoad, canSave: state.namedPlaylist.canSave, canDelete: state.namedPlaylist.canDelete, lastMutationOutcome: state.namedPlaylist.lastMutationOutcome, statusMessage: state.namedPlaylist.statusMessage, statusKind: state.namedPlaylist.statusKind } : null, interactionMetrics: state.interactionMetrics, playout: state.playout ? { mode: state.playout.mode, connectionState: state.playout.connectionState, phase: state.playout.phase, isConnected: state.playout.isConnected, isCommandAvailable: state.playout.isCommandAvailable, liveTakeInAllowed: state.playout.liveTakeInAllowed, isPlayCompletionPending: state.playout.isPlayCompletionPending, isTakeOutCompletionPending: state.playout.isTakeOutCompletionPending, outcomeUnknown: state.playout.outcomeUnknown, preparedCode: state.playout.preparedCode, onAirCode: state.playout.onAirCode, currentEntryId: state.playout.currentEntryId, isBusy: state.playout.isBusy, refreshActive: state.playout.refreshActive, message: state.playout.message } : null } : null, dom: { readyState: document.readyState, url: location.href, viewport: { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }, bodyBusy: document.body.getAttribute("aria-busy"), connectionText: (document.getElementById("playout-connection-state")?.textContent || "").trim(), playoutStatusText: (document.getElementById("playout-status")?.textContent || "").trim(), operatorStatusText: (document.getElementById("operator-status")?.textContent || "").trim(), legacyDialogHidden: document.getElementById("legacy-dialog")?.hidden === true, legacyDialogText: (document.getElementById("legacy-dialog-message")?.textContent || "").trim(), stockOptions: Array.from(document.querySelectorAll( "#stock-results button[data-result-index]")) .map(button => ({ index: Number(button.dataset.resultIndex), text: (button.textContent || "").trim(), selected: button.getAttribute("aria-selected") === "true", tabIndex: button.tabIndex, focused: button === active })), cuts: Array.from(document.querySelectorAll("#cut-list .cut-row")) .map(row => ({ physicalIndex: Number(row.dataset.physicalIndex), selected: row.getAttribute("aria-selected") === "true", tabIndex: row.tabIndex, focused: row === active, dragging: row.classList.contains("dragging") })), playlist: Array.from(document.querySelectorAll( "#playlist-rows .playlist-data-row")) .map(row => ({ rowId: row.dataset.rowId, selected: row.getAttribute("aria-selected") === "true", active: row.dataset.active === "true", current: row.getAttribute("aria-current") === "true", enabled: row.querySelector("input[type='checkbox']")?.checked === true, dragging: row.classList.contains("dragging"), dragBefore: row.classList.contains("drag-before"), dragAfter: row.classList.contains("drag-after"), rowHeaderText: (row.querySelector(".playlist-row-header")?.textContent || "").trim(), rowHeaderWidth: row.querySelector(".playlist-row-header") ?.getBoundingClientRect().width ?? null, rowHeaderDragEnabled: row.querySelector(".playlist-row-header") ?.dataset?.dragEnabled === "true", focused: row === active || row.contains(active) })), cutDropHighlighted: document.getElementById("playlist-drop-zone") ?.classList.contains("cut-drag-over") === true, cutPointerCapture: { pointerId: activePointerId, physicalIndices: capturedCutPhysicalIndices }, playlistPointerCapture: { pointerId: activePointerId, rowIds: capturedPlaylistRowIds }, namedModalHidden: document.getElementById("named-playlist-modal")?.hidden === true, namedConfirmationHidden: document.getElementById("named-playlist-confirmation")?.hidden === true, namedTitle: (document.getElementById("named-playlist-title")?.textContent || "").trim(), namedConfirmationMessage: (document.getElementById( "named-playlist-confirmation-message")?.textContent || "").trim(), namedConfirmDisabled: document.getElementById( "named-playlist-confirm-button")?.disabled === true, namedDefinitionCount: document.querySelectorAll( "#named-playlist-definitions button[data-named-playlist-definition-id]").length, activeElement: active ? { id: active.id || null, tagName: active.tagName, resultIndex: active.dataset?.resultIndex ?? null, physicalIndex: active.dataset?.physicalIndex ?? null, playlistRowId: rowForActive?.dataset?.rowId ?? null } : null, visibleModalRole: visibleModal?.getAttribute("role") || null, visibleModalLabelledBy: visibleModal?.getAttribute("aria-labelledby") || null, activeInsideVisibleModal: visibleModal ? visibleModal.contains(active) : false, modalFocusableIds: focusables.map(element => element.id || null) } }; })()`; } async function readSnapshot() { return await evaluate(probeSnapshotExpression()); } function assertSafety(snapshot, label, allowUiBusy = true) { if (!snapshot?.state?.playout) { failKnown(`Native playout state is missing at ${label}.`); } const state = snapshot.state; const playout = state.playout; if (snapshot.safety?.postMessageWrapped !== true) { failUnknown(`The outbound WebView safety gate is not installed at ${label}.`); } const outboundMessages = snapshot.safety.outboundMessages || []; const blockedOutboundMessages = snapshot.safety.blockedOutboundMessages || []; const stateViolations = snapshot.safety.stateViolations || []; evidence.safety.observedOutboundMessageTypes = outboundMessages .map(message => message.type); evidence.safety.blockedOutboundMessages = blockedOutboundMessages .map(message => ({ ...message })); evidence.safety.stateViolations = stateViolations.map(violation => ({ ...violation, reasons: [...violation.reasons] })); evidence.safety.playoutIntentIssued = blockedOutboundMessages.some(message => message.category === "playout"); evidence.safety.databaseWriteIntentIssued = blockedOutboundMessages.some(message => message.category === "database-write"); if (blockedOutboundMessages.length > 0) { failKnown(`The outbound WebView safety gate blocked an unapproved message at ${label}.`); } if (stateViolations.length > 0) { failUnknown(`A transient unsafe playout state was observed before ${label}.`); } if (playout.mode !== "dryRun") { failKnown(`Expected DryRun at ${label}, received ${String(playout.mode)}.`); } if (playout.phase !== "idle" || playout.preparedCode != null || playout.onAirCode != null) { failKnown(`Playout left IDLE at ${label}.`); } if (playout.outcomeUnknown === true || playout.isPlayCompletionPending === true || playout.isTakeOutCompletionPending === true || playout.isBusy === true || playout.refreshActive === true) { failUnknown(`Playout became busy, pending, refreshing, or unknown at ${label}.`); } if (!allowUiBusy && state.isBusy === true) { failKnown(`The operator UI is still busy at ${label}.`); } if (snapshot.dom.url !== exactTargetUrl || snapshot.dom.readyState !== "complete") { failKnown(`The exact package document is not ready at ${label}.`); } if (snapshot.dom.connectionText !== "DRY RUN") { failKnown(`The visible connection badge is not DRY RUN at ${label}.`); } if (!snapshot.dom.legacyDialogHidden) { failKnown(`A legacy alert is visible at ${label}: ${snapshot.dom.legacyDialogText}`); } } async function waitFor(label, predicate, options = {}) { const timeoutMilliseconds = options.timeoutMilliseconds || configuration.timeoutMilliseconds; const deadline = Date.now() + timeoutMilliseconds; let lastSnapshot = null; while (Date.now() < deadline) { lastSnapshot = await readSnapshot(); if (lastSnapshot?.state?.playout) { assertSafety(lastSnapshot, label, options.allowUiBusy !== false); } if (predicate(lastSnapshot)) return lastSnapshot; await sleep(100); } failUnknown(`Timed out waiting for ${label}; no action was retried. Last snapshot: ` + JSON.stringify(lastSnapshot)); } async function checkpoint(name, details = null) { const snapshot = await readSnapshot(); assertSafety(snapshot, name, false); evidence.checkpoints.push({ name, details, snapshot }); return snapshot; } async function elementGeometry(expression, label, verticalFraction = 0.5) { const geometry = await evaluate(`(() => { const element = ${expression}; if (!element) return null; const rectangle = element.getBoundingClientRect(); return { x: rectangle.left + rectangle.width / 2, y: rectangle.top + rectangle.height * ${Number(verticalFraction)}, left: rectangle.left, top: rectangle.top, right: rectangle.right, bottom: rectangle.bottom, width: rectangle.width, height: rectangle.height, disabled: element.disabled === true, ariaDisabled: element.getAttribute("aria-disabled") }; })()`); if (!geometry || geometry.disabled || geometry.ariaDisabled === "true" || !Number.isFinite(geometry.x) || !Number.isFinite(geometry.y) || geometry.width <= 0 || geometry.height <= 0 || geometry.right <= 0 || geometry.bottom <= 0) { failKnown(`${label} is missing, disabled, or outside the rendered viewport.`); } return geometry; } async function playlistDropGeometry(label) { const geometry = await evaluate(`(() => { const zone = document.getElementById("playlist-drop-zone"); if (!zone) return null; const zoneRectangle = zone.getBoundingClientRect(); const headerRectangle = zone.querySelector(".playlist-header") ?.getBoundingClientRect() || null; const viewport = { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }; const inset = 8; const left = Math.max(0, zoneRectangle.left + inset); const right = Math.min(viewport.width - 1, zoneRectangle.right - inset); const top = Math.max( 0, (headerRectangle ? headerRectangle.bottom : zoneRectangle.top) + inset); const bottom = Math.min(viewport.height - 1, zoneRectangle.bottom - inset); const fractions = [0.5, 0.25, 0.75]; const points = right > left && bottom > top ? fractions.map(fraction => { const x = left + (right - left) / 2; const y = top + (bottom - top) * fraction; const hit = document.elementFromPoint(x, y); return { x, y, hitId: hit?.id || null, hitClass: hit?.className || null, insideDropZone: hit === zone || zone.contains(hit) }; }) : []; return { zone: { left: zoneRectangle.left, top: zoneRectangle.top, right: zoneRectangle.right, bottom: zoneRectangle.bottom, width: zoneRectangle.width, height: zoneRectangle.height }, headerBottom: headerRectangle?.bottom ?? null, viewport, visibleInterior: { left, top, right, bottom }, points }; })()`); if (!geometry || geometry.zone.width <= 0 || geometry.zone.height <= 0 || geometry.points.length === 0 || geometry.points.some(point => !Number.isFinite(point.x) || !Number.isFinite(point.y) || point.insideDropZone !== true)) { failKnown(`${label} has no verified visible interior drop point.`); } return geometry; } async function moveMouse(point, modifiers = 0, buttons = 0) { await rpc("Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, // Edge releases explicit pointer capture if a CDP mouseMoved event says // no button while its buttons bitmask says the left button is held. // Keep both fields consistent with the physical state during a drag. button: (buttons & 1) === 1 ? "left" : "none", buttons, modifiers, pointerType: "mouse" }); } async function pressMouse(point, modifiers = 0) { if (pointerIsPressed) failKnown("A second pointer press was attempted before release."); await moveMouse(point, modifiers, 0); await rpc("Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, modifiers, pointerType: "mouse" }); pointerIsPressed = true; pointerReleasePoint = point; pointerReleaseModifiers = modifiers; } async function releaseMouse(point, modifiers = 0) { if (!pointerIsPressed) failKnown("A pointer release was attempted without a press."); await rpc("Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, modifiers, pointerType: "mouse" }); pointerIsPressed = false; pointerReleasePoint = null; pointerReleaseModifiers = 0; } async function pointerClick(expression, label, modifiers = 0) { const forbiddenId = await evaluate(`(() => { const element = ${expression}; return element?.closest?.("button")?.id || element?.id || null; })()`); if (forbiddenControlIds.includes(forbiddenId)) { failKnown(`The harness refused forbidden control #${forbiddenId}.`); } const point = await elementGeometry(expression, label); evidence.inputs.push({ type: "pointer-click", label, modifiers, point }); await pressMouse(point, modifiers); await sleep(25); await releaseMouse(point, modifiers); return point; } const keyDefinitions = Object.freeze({ ArrowDown: { key: "ArrowDown", code: "ArrowDown", virtualKey: 40 }, ArrowUp: { key: "ArrowUp", code: "ArrowUp", virtualKey: 38 }, Backspace: { key: "Backspace", code: "Backspace", virtualKey: 8 }, End: { key: "End", code: "End", virtualKey: 35 }, Escape: { key: "Escape", code: "Escape", virtualKey: 27 }, Home: { key: "Home", code: "Home", virtualKey: 36 }, Space: { key: " ", code: "Space", virtualKey: 32 }, Tab: { key: "Tab", code: "Tab", virtualKey: 9 }, SelectAll: { key: "a", code: "KeyA", virtualKey: 65 } }); async function pressKey(definitionName, label, modifiers = 0) { const definition = keyDefinitions[definitionName]; if (!definition) failKnown(`Unknown key definition ${definitionName}.`); if (new Set(["F2", "F3", "F8"]).has(definition.key)) { failKnown(`The harness refused playout shortcut ${definition.key}.`); } if (definition.key === "Escape") { const modalState = await evaluate(`(() => ({ named: document.getElementById("named-playlist-modal")?.hidden === false, confirmation: document.getElementById("named-playlist-confirmation")?.hidden === false }))()`); if (!modalState?.named && !modalState?.confirmation) { failKnown("Escape is allowed only while the named-playlist modal owns input."); } } evidence.inputs.push({ type: "key", label, key: definition.key, modifiers }); const common = { key: definition.key, code: definition.code, windowsVirtualKeyCode: definition.virtualKey, nativeVirtualKeyCode: definition.virtualKey, modifiers }; await rpc("Input.dispatchKeyEvent", { type: "keyDown", ...common }); await rpc("Input.dispatchKeyEvent", { type: "keyUp", ...common }); } async function setSearchText(text) { await pointerClick( 'document.getElementById("stock-search")', "focus stock search"); await pressKey("SelectAll", "select existing stock query", ctrlModifier); await pressKey("Backspace", "clear existing stock query"); evidence.inputs.push({ type: "text", label: "stock query", text }); await rpc("Input.insertText", { text }); } function selectedPhysicalIndices(snapshot) { return snapshot.state.cutRows .filter(row => row.isSelected) .map(row => row.physicalIndex); } function selectedPlaylistIds(snapshot) { return snapshot.state.playlist .filter(row => row.isSelected) .map(row => row.rowId); } function activePlaylistId(snapshot) { return snapshot.state.playlist.find(row => row.isActive)?.rowId || null; } function focusedCutIndex(snapshot) { return snapshot.state.cutRows.find(row => row.isFocused)?.physicalIndex ?? null; } function assertExactArray(actual, expected, label) { if (actual.length !== expected.length || actual.some((value, index) => value !== expected[index])) { failKnown(`${label}: expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}.`); } } function cssDragSize(snapshot) { const metrics = snapshot.state.interactionMetrics; const devicePixelRatio = snapshot.devicePixelRatio; if (!metrics || metrics.coordinateSpace !== "host-dip" || !Number.isInteger(metrics.cutDragWidthDips) || metrics.cutDragWidthDips <= 0 || !Number.isInteger(metrics.cutDragHeightDips) || metrics.cutDragHeightDips <= 0) { failKnown("Valid host-DIP cut drag metrics are missing."); } const hostScale = Number.isFinite(metrics.hostRasterizationScale) && metrics.hostRasterizationScale > 0 ? metrics.hostRasterizationScale : 1; const deviceScale = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : hostScale; const candidateZoom = deviceScale / hostScale; const browserZoom = Number.isFinite(candidateZoom) && candidateZoom >= 0.25 && candidateZoom <= 5 ? candidateZoom : 1; return { width: metrics.cutDragWidthDips / browserZoom, height: metrics.cutDragHeightDips / browserZoom }; } async function dragCutsToPlaylist(physicalIndex, modifiers, expectedAppendCount, label) { const before = await readSnapshot(); assertSafety(before, `${label} preflight`, false); const beforeCount = before.state.playlist.length; const beforeRevision = before.state.revision; const sourceExpression = `document.querySelector('#cut-list .cut-row[data-physical-index="${physicalIndex}"]')`; const source = await elementGeometry(sourceExpression, `${label} source`); const dropGeometry = await playlistDropGeometry(`${label} target`); const dragInput = { type: "cut-drag", label, physicalIndex, modifiers, source, dropGeometry, targetSamples: [], acceptedTarget: null, expectedAppendCount }; evidence.inputs.push(dragInput); await pressMouse(source, modifiers); try { const held = await waitFor(`${label} pointer-down receipt`, snapshot => snapshot.state.revision > beforeRevision && focusedCutIndex(snapshot) === physicalIndex, { allowUiBusy: false }); const dragSize = cssDragSize(held); const inside = { x: source.x - dragSize.width / 2, y: source.y - dragSize.height / 2 }; const threshold = { x: Math.min(source.right - 4, source.x + Math.max(8, dragSize.width + 2)), y: source.y }; pointerReleasePoint = source; pointerReleaseModifiers = modifiers; await moveMouse(inside, modifiers, 1); await sleep(30); const beforeThreshold = await readSnapshot(); const sourceBeforeThreshold = beforeThreshold.dom.cuts.find(row => row.physicalIndex === physicalIndex); if (!sourceBeforeThreshold || sourceBeforeThreshold.dragging || beforeThreshold.dom.cutDropHighlighted) { failKnown(`${label} started inside the original drag rectangle.`); } await moveMouse(threshold, modifiers, 1); await sleep(50); const thresholdSnapshot = await readSnapshot(); const thresholdSource = thresholdSnapshot.dom.cuts.find(row => row.physicalIndex === physicalIndex); const capturedAtThreshold = thresholdSnapshot.dom.cutPointerCapture ?.physicalIndices?.includes(physicalIndex) === true; dragInput.threshold = { point: threshold, dragging: thresholdSource?.dragging === true, captured: capturedAtThreshold, pointerEvents: thresholdSnapshot.pointerEvents.slice(-12) }; if (!thresholdSource?.dragging) { failKnown( `${label} did not cross the Windows drag threshold on the source row; ` + `pointer diagnostics: ${JSON.stringify(dragInput.threshold.pointerEvents)}.`); } if (!capturedAtThreshold) { failKnown( `${label} crossed the drag threshold but WebView2 did not retain pointer ` + `capture on the source row; pointer diagnostics: ` + `${JSON.stringify(dragInput.threshold.pointerEvents)}.`); } if (thresholdSnapshot.dom.cutDropHighlighted) { failKnown(`${label} highlighted the remote drop zone while still on the source row.`); } let overTarget = null; let acceptedTarget = null; for (const candidate of dropGeometry.points) { await moveMouse(candidate, modifiers, 1); await sleep(50); const sample = await readSnapshot(); const sourceAtCandidate = sample.dom.cuts.find(row => row.physicalIndex === physicalIndex); const sampleEvidence = { point: candidate, dragging: sourceAtCandidate?.dragging === true, highlighted: sample.dom.cutDropHighlighted, captured: sample.dom.cutPointerCapture?.physicalIndices ?.includes(physicalIndex) === true, pointerEvents: sample.pointerEvents.slice(-12) }; dragInput.targetSamples.push(sampleEvidence); if (sampleEvidence.dragging && sampleEvidence.highlighted && sampleEvidence.captured) { overTarget = sample; acceptedTarget = candidate; break; } } if (!overTarget || !acceptedTarget) { failKnown( `${label} did not enter the real captured-pointer drop state; samples: ` + `${JSON.stringify(dragInput.targetSamples)}.`); } dragInput.acceptedTarget = acceptedTarget; await releaseMouse(acceptedTarget, modifiers); } finally { if (pointerIsPressed) { const safePoint = pointerReleasePoint || source; await releaseMouse(safePoint, pointerReleaseModifiers); } } const completed = await waitFor(`${label} append`, snapshot => snapshot.state.isBusy !== true && snapshot.state.playlist.length === beforeCount + expectedAppendCount, { allowUiBusy: false }); if (!completed.dom.cuts.every(row => !row.dragging) || completed.dom.cutDropHighlighted) { failKnown(`${label} left drag visuals active after release.`); } return completed; } async function rowCellGeometry(rowId, label) { return await elementGeometry(`(() => { const row = Array.from(document.querySelectorAll( "#playlist-rows .playlist-data-row")) .find(candidate => candidate.dataset.rowId === ${JSON.stringify(rowId)}); return row?.children?.item(3) || null; })()`, label); } async function playlistRowHeaderGeometry(rowId, label, verticalFraction = 0.5) { return await elementGeometry(`(() => { const row = Array.from(document.querySelectorAll( "#playlist-rows .playlist-data-row")) .find(candidate => candidate.dataset.rowId === ${JSON.stringify(rowId)}); return row?.querySelector(".playlist-row-header") || null; })()`, label, verticalFraction); } async function playlistCheckboxGeometry(rowId, label) { return await elementGeometry(`(() => { const row = Array.from(document.querySelectorAll( "#playlist-rows .playlist-data-row")) .find(candidate => candidate.dataset.rowId === ${JSON.stringify(rowId)}); return row?.querySelector("input[type='checkbox']") || null; })()`, label); } async function pointerClickPlaylistRow(rowId, label, modifiers = 0) { const point = await rowCellGeometry(rowId, label); evidence.inputs.push({ type: "pointer-click", label, modifiers, point, rowId }); await pressMouse(point, modifiers); await sleep(25); await releaseMouse(point, modifiers); } async function dragPlaylistRow(sourceRowId, targetRowId, label) { const source = await playlistRowHeaderGeometry(sourceRowId, `${label} source`); const target = await playlistRowHeaderGeometry(targetRowId, `${label} target`, 0.75); evidence.inputs.push({ type: "playlist-row-drag", label, sourceRowId, targetRowId, position: "after", source, target }); await pressMouse(source, 0); try { pointerReleasePoint = source; await moveMouse({ x: source.x + 8, y: source.y }, 0, 1); await sleep(50); await moveMouse({ x: (source.x + target.x) / 2, y: (source.y + target.y) / 2 }, 0, 1); await sleep(50); await moveMouse(target, 0, 1); await sleep(75); const overTarget = await readSnapshot(); const sourceDom = overTarget.dom.playlist.find(row => row.rowId === sourceRowId); const targetDom = overTarget.dom.playlist.find(row => row.rowId === targetRowId); const captured = overTarget.dom.playlistPointerCapture?.rowIds ?.includes(sourceRowId) === true; if (!sourceDom?.dragging || !targetDom?.dragAfter || !captured) { failKnown(`${label} did not produce the captured-pointer row-header drag state.`); } await releaseMouse(target, 0); } finally { if (pointerIsPressed) { await releaseMouse(pointerReleasePoint || source, 0); } } } async function attemptCheckboxOriginDrag(sourceRowId, targetRowId, label) { const source = await playlistCheckboxGeometry(sourceRowId, `${label} checkbox`); const target = await playlistRowHeaderGeometry(targetRowId, `${label} target`, 0.75); evidence.inputs.push({ type: "playlist-checkbox-drag-attempt", label, sourceRowId, targetRowId, source, target }); await pressMouse(source, 0); try { pointerReleasePoint = source; await moveMouse({ x: source.x + 8, y: source.y }, 0, 1); await sleep(50); await moveMouse(target, 0, 1); await sleep(75); const during = await readSnapshot(); if (during.dom.playlist.some(row => row.dragging || row.dragBefore || row.dragAfter)) { failKnown(`${label} exposed a row drag visual from a checkbox origin.`); } await releaseMouse(target, 0); } finally { if (pointerIsPressed) { await releaseMouse(pointerReleasePoint || source, 0); } } } function readSmallJsonFile(filePath, label) { let stat; try { stat = fs.lstatSync(filePath); } catch (_) { failKnown(`${label} is unavailable.`); } if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > 16_384) { failKnown(`${label} is not a small regular file.`); } try { const bytes = fs.readFileSync(filePath); if (bytes.length !== stat.size) failKnown(`${label} changed while it was read.`); return { value: JSON.parse(bytes.toString("utf8")), bytes }; } catch (_) { failKnown(`${label} is not valid UTF-8 JSON.`); } } async function requestWindowsInput( current, sourceRowId, targetRowId, expectedFirstRowId, label) { assertSafety(current, `${label} preflight`, false); const preflightSignature = JSON.stringify({ revision: current.state.revision, order: current.state.playlist.map(row => row.rowId), selected: selectedPlaylistIds(current), active: activePlaylistId(current), focused: current.dom.activeElement?.playlistRowId ?? null }); await sleep(350); let stabilized = await readSnapshot(); assertSafety(stabilized, `${label} stabilized preflight`, false); const stabilizedSignature = JSON.stringify({ revision: stabilized.state.revision, order: stabilized.state.playlist.map(row => row.rowId), selected: selectedPlaylistIds(stabilized), active: activePlaylistId(stabilized), focused: stabilized.dom.activeElement?.playlistRowId ?? null }); if (stabilizedSignature !== preflightSignature) { failKnown(`${label} state changed before the one-shot request was published.`); } current = stabilized; const source = await playlistRowHeaderGeometry(sourceRowId, `${label} source`); const target = await playlistRowHeaderGeometry(targetRowId, `${label} target`, 0.75); await sleep(100); const geometryGuard = await readSnapshot(); const guardedSource = await playlistRowHeaderGeometry( sourceRowId, `${label} guarded source`); const guardedTarget = await playlistRowHeaderGeometry( targetRowId, `${label} guarded target`, 0.75); const geometryStable = ["x", "y", "left", "top", "right", "bottom"] .every(key => Math.abs(source[key] - guardedSource[key]) <= 0.5 && Math.abs(target[key] - guardedTarget[key]) <= 0.5); const guardSignature = JSON.stringify({ revision: geometryGuard.state.revision, order: geometryGuard.state.playlist.map(row => row.rowId), selected: selectedPlaylistIds(geometryGuard), active: activePlaylistId(geometryGuard), focused: geometryGuard.dom.activeElement?.playlistRowId ?? null }); if (!geometryStable || guardSignature !== stabilizedSignature) { failKnown(`${label} DOM geometry or native state changed before publication.`); } current = geometryGuard; const token = randomBytes(16).toString("hex").toUpperCase(); const beforeOrder = current.state.playlist.map(row => row.rowId); const afterOrder = beforeOrder.filter(rowId => rowId !== sourceRowId); const targetIndex = afterOrder.indexOf(targetRowId); if (targetIndex < 0 || sourceRowId === targetRowId || new Set(beforeOrder).size !== beforeOrder.length) { failKnown(`${label} row identities are not safe for a one-row move.`); } afterOrder.splice(targetIndex + 1, 0, sourceRowId); if (afterOrder[0] !== expectedFirstRowId) { failKnown(`${label} computed an unexpected Home boundary row.`); } const request = { schemaVersion: 1, token, targetUrl: exactTargetUrl, operation: "row-header-drag-after-then-home", sourceRowId, targetRowId, position: "after", source: { x: source.x, y: source.y }, target: { x: target.x, y: target.y }, viewport: { ...current.dom.viewport }, devicePixelRatio: current.devicePixelRatio, beforeOrder, afterOrder, expectedSelectedRowId: sourceRowId, expectedActiveRowIdAfterHome: expectedFirstRowId, expectedFocusedRowId: sourceRowId, mouseDownCalls: 1, mouseUpCalls: 1, homeKeyCalls: 1 }; const requestBytes = Buffer.from(`${JSON.stringify(request, null, 2)}\n`, "utf8"); fs.writeFileSync(configuration.windowsInputRequestPath, requestBytes, { flag: "wx" }); evidence.windowsInput.requestSha256 = sha256(requestBytes); evidence.inputs.push({ type: "windows-send-input-request", label, sourceRowId, targetRowId, position: "after" }); while (!fs.existsSync(configuration.windowsInputAckPath)) { await sleep(50); } const acknowledgement = readSmallJsonFile( configuration.windowsInputAckPath, "Windows input acknowledgement"); const ack = acknowledgement.value; if (!ack || ack.schemaVersion !== 1 || ack.token !== token || ack.result !== "PASS" || ack.operation !== request.operation || ack.foregroundValidated !== true || ack.packageIdentityValidated !== true || ack.mouseDownCalls !== 1 || ack.mouseUpCalls !== 1 || ack.homeKeyCalls !== 1 || ack.inputRetryCount !== 0) { failKnown("Windows input acknowledgement does not match the one-shot request."); } evidence.windowsInput.acknowledgementSha256 = sha256(acknowledgement.bytes); const completed = await waitFor(label, snapshot => JSON.stringify(snapshot.state.playlist.map(row => row.rowId)) === JSON.stringify(afterOrder) && JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([sourceRowId]) && activePlaylistId(snapshot) === expectedFirstRowId && snapshot.dom.activeElement?.playlistRowId === sourceRowId, { timeoutMilliseconds: Math.min(configuration.timeoutMilliseconds, 15_000), allowUiBusy: false }); evidence.windowsInput.result = "PASS"; evidence.windowsInput.beforeOrder = beforeOrder; evidence.windowsInput.afterOrder = afterOrder; evidence.windowsInput.activeRowIdAfterHome = expectedFirstRowId; await checkpoint("windows-sendinput-row-header-drag-and-home", { sourceRowId, targetRowId, beforeOrder, afterOrder, expectedFirstRowId }); return completed; } async function installProbe() { const token = `legacy-package-input-${Date.now()}-${Math.random().toString(16).slice(2)}`; const installed = await evaluate(`(() => { if (!window.chrome?.webview) { throw new Error("WebView2 bridge is unavailable."); } if (globalThis.__legacyPackageInputProbe) { throw new Error("A legacy package input probe is already installed; restart the app."); } const probe = { token: ${JSON.stringify(token)}, stateCount: 0, states: [], listener: null, pointerEventTypes: [ "pointerdown", "gotpointercapture", "pointermove", "pointerup", "pointercancel", "lostpointercapture" ], pointerEvents: [], pointerListener: null, originalPostMessage: null, postMessageWrapper: null, outboundMessages: [], blockedOutboundMessages: [], stateViolations: [] }; probe.listener = event => { if (event.data?.type !== "state" || !event.data.payload) return; const payload = event.data.payload; probe.stateCount += 1; probe.states.push(structuredClone(payload)); if (probe.states.length > 750) probe.states.shift(); const playout = payload.playout; const reasons = []; if (!playout) { reasons.push("missing-playout"); } else { if (playout.mode !== "dryRun") reasons.push("mode"); if (playout.phase !== "idle") reasons.push("phase"); if (playout.preparedCode != null) reasons.push("prepared-code"); if (playout.onAirCode != null) reasons.push("on-air-code"); if (playout.isConnected === true) reasons.push("connected"); if (playout.outcomeUnknown === true) reasons.push("outcome-unknown"); if (playout.isPlayCompletionPending === true) reasons.push("play-pending"); if (playout.isTakeOutCompletionPending === true) reasons.push("take-out-pending"); if (playout.isBusy === true) reasons.push("playout-busy"); if (playout.refreshActive === true) reasons.push("refresh-active"); } if (reasons.length > 0) { probe.stateViolations.push({ at: new Date().toISOString(), revision: payload.revision ?? null, reasons }); } }; probe.pointerListener = event => { const target = event.target instanceof Element ? event.target : null; const cut = target?.closest?.("#cut-list .cut-row") || null; const playlistRow = target?.closest?.("#playlist-rows .playlist-data-row") || null; const capturedCuts = Number.isInteger(event.pointerId) ? Array.from(document.querySelectorAll("#cut-list .cut-row")) .filter(row => typeof row.hasPointerCapture === "function" && row.hasPointerCapture(event.pointerId)) .map(row => Number(row.dataset.physicalIndex)) : []; probe.pointerEvents.push({ type: event.type, pointerId: event.pointerId, pointerType: event.pointerType, clientX: event.clientX, clientY: event.clientY, button: event.button, buttons: event.buttons, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, targetId: target?.id || null, targetClass: target?.className || null, cutPhysicalIndex: cut ? Number(cut.dataset.physicalIndex) : null, playlistRowId: playlistRow?.dataset?.rowId || null, capturedCutPhysicalIndices: capturedCuts, timeStamp: event.timeStamp }); if (probe.pointerEvents.length > 200) probe.pointerEvents.shift(); }; const allowedTypes = new Set(${JSON.stringify(allowedOutboundMessageTypes)}); const playoutTypes = new Set(${JSON.stringify(playoutIntentMessageTypes)}); const databaseWriteTypes = new Set(${JSON.stringify(databaseWriteIntentMessageTypes)}); probe.originalPostMessage = window.chrome.webview.postMessage; probe.postMessageWrapper = message => { const type = typeof message?.type === "string" ? message.type : null; const record = { type, at: new Date().toISOString() }; probe.outboundMessages.push(record); if (probe.outboundMessages.length > 250) probe.outboundMessages.shift(); if (!type || !allowedTypes.has(type)) { const blocked = { ...record, category: playoutTypes.has(type) ? "playout" : databaseWriteTypes.has(type) ? "database-write" : "unapproved" }; probe.blockedOutboundMessages.push(blocked); return; } return probe.originalPostMessage.call(window.chrome.webview, message); }; try { window.chrome.webview.postMessage = probe.postMessageWrapper; if (window.chrome.webview.postMessage !== probe.postMessageWrapper) { throw new Error("The outbound WebView safety gate could not be installed."); } globalThis.__legacyPackageInputProbe = probe; window.chrome.webview.addEventListener("message", probe.listener); probe.pointerEventTypes.forEach(type => document.addEventListener(type, probe.pointerListener, true)); window.chrome.webview.postMessage({ type: "ready", payload: {} }); } catch (error) { window.chrome.webview.postMessage = probe.originalPostMessage; delete globalThis.__legacyPackageInputProbe; throw error; } return { token: probe.token }; })()`); if (installed?.token !== token) failKnown("The state observer was not installed exactly once."); } async function removeProbe() { if (!socket || socket.readyState !== WebSocket.OPEN) return; const removed = await evaluate(`(() => { const probe = globalThis.__legacyPackageInputProbe; if (!probe) return false; if (probe.listener) { window.chrome.webview.removeEventListener("message", probe.listener); } if (probe.pointerListener) { probe.pointerEventTypes.forEach(type => document.removeEventListener(type, probe.pointerListener, true)); } if (probe.originalPostMessage) { if (window.chrome?.webview?.postMessage !== probe.postMessageWrapper) { throw new Error("The outbound WebView safety gate identity changed."); } window.chrome.webview.postMessage = probe.originalPostMessage; } delete globalThis.__legacyPackageInputProbe; return true; })()`); if (removed !== true) { failUnknown("The outbound WebView safety probe was not restored exactly once."); } probeInstalled = false; } async function captureScreenshot() { const response = await rpc("Page.captureScreenshot", { format: "png", fromSurface: true, captureBeyondViewport: false }, 20_000); if (!response?.data) failKnown("CDP did not return screenshot data."); const bytes = Buffer.from(response.data, "base64"); fs.writeFileSync(configuration.screenshotPath, bytes, { flag: "wx" }); evidence.screenshot = { path: configuration.screenshotPath, bytes: bytes.length, sha256: sha256(bytes) }; } async function executeSafeSequence() { await rpc("Runtime.enable"); await rpc("Page.enable"); await installProbe(); probeInstalled = true; const preflight = await waitFor("DryRun IDLE preflight", snapshot => snapshot?.state?.playout?.mode === "dryRun" && snapshot.state.playout.phase === "idle" && snapshot.state.isBusy !== true && snapshot.dom.connectionText === "DRY RUN", { allowUiBusy: false }); assertSafety(preflight, "DryRun IDLE preflight", false); if (preflight.state.playlist.length !== 0) { failKnown("The package input test requires an initially empty in-memory playlist."); } if (!preflight.dom.namedModalHidden || !preflight.dom.namedConfirmationHidden) { failKnown("A child modal was already open at preflight."); } await checkpoint("preflight"); await setSearchText(evidence.fixture.query); await pointerClick( 'document.getElementById("stock-search-button")', "submit stock search"); let current = await waitFor("stock search result", snapshot => snapshot?.state?.isBusy === false && snapshot.state.searchText === evidence.fixture.query && snapshot.state.searchResults.some(row => row.displayName === evidence.fixture.stockDisplayName), { timeoutMilliseconds: 60_000, allowUiBusy: true }); const exactStocks = current.state.searchResults.filter(row => row.displayName === evidence.fixture.stockDisplayName); if (exactStocks.length !== 1) { failKnown(`Expected exactly one ${evidence.fixture.stockDisplayName} row, received ${exactStocks.length}.`); } const exactStock = exactStocks[0]; const exactStockExpression = `document.querySelector('#stock-results button[data-result-index="${exactStock.index}"]')`; await pointerClick(exactStockExpression, `select ${evidence.fixture.stockDisplayName}`); current = await waitFor("exact stock selection", snapshot => snapshot.state.isBusy === false && snapshot.state.selectedStockIndex === exactStock.index, { allowUiBusy: false }); const selectedPosition = current.state.searchResults.findIndex(row => row.index === exactStock.index); const forward = selectedPosition < current.state.searchResults.length - 1; const firstKey = forward ? "ArrowDown" : "ArrowUp"; const returnKey = forward ? "ArrowUp" : "ArrowDown"; const neighbor = current.state.searchResults[selectedPosition + (forward ? 1 : -1)]; if (!neighbor) failKnown("The exact stock has no adjacent keyboard result."); await pressKey(firstKey, "move stock result selection to adjacent row"); current = await waitFor("adjacent stock keyboard selection", snapshot => snapshot.state.selectedStockIndex === neighbor.index && snapshot.dom.stockOptions.filter(row => row.selected).length === 1 && snapshot.dom.stockOptions.some(row => row.index === neighbor.index && row.selected && row.tabIndex === 0 && row.focused), { allowUiBusy: false }); await pressKey(returnKey, "return stock result selection"); current = await waitFor("returned stock keyboard selection", snapshot => snapshot.state.selectedStockIndex === exactStock.index && snapshot.dom.stockOptions.filter(row => row.selected).length === 1 && snapshot.dom.stockOptions.some(row => row.index === exactStock.index && row.selected && row.tabIndex === 0 && row.focused), { allowUiBusy: false }); await checkpoint("stock-search-and-keyboard", { resultCount: current.state.searchResults.length, exactStockIndex: exactStock.index }); const firstThreeCuts = current.state.cutRows.slice(0, 3); if (firstThreeCuts.length !== 3 || firstThreeCuts.some((row, index) => row.physicalIndex !== index || row.isSeparator === true)) { failKnown("The first three physical cuts are not the expected non-separator fixture."); } assertExactArray( firstThreeCuts.map(row => row.rawLabel), [ "1열판기본_예상체결가", "1열판기본_현재가", "1열판기본_시간외단일가" ], "first three physical cut labels"); await pointerClick( 'document.querySelector("#cut-list .cut-row[data-physical-index=\'0\']")', "select first cut"); current = await waitFor("first cut selection", snapshot => focusedCutIndex(snapshot) === 0 && JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([0]), { allowUiBusy: false }); await pressKey("ArrowDown", "move cut focus without changing selection", ctrlModifier); current = await waitFor("Ctrl ArrowDown cut focus", snapshot => focusedCutIndex(snapshot) === 1 && JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([0]), { allowUiBusy: false }); await pressKey("Space", "select focused cut with Space"); current = await waitFor("Space cut selection", snapshot => focusedCutIndex(snapshot) === 1 && JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([1]), { allowUiBusy: false }); await pressKey("ArrowDown", "extend cut selection with Shift ArrowDown", shiftModifier); current = await waitFor("Shift ArrowDown cut range", snapshot => focusedCutIndex(snapshot) === 2 && JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([1, 2]), { allowUiBusy: false }); await checkpoint("cut-keyboard", { selectedPhysicalIndices: selectedPhysicalIndices(current), focusedPhysicalIndex: focusedCutIndex(current) }); current = await dragCutsToPlaylist( 2, ctrlModifier | shiftModifier, 2, "multi-cut captured-pointer drag"); assertExactArray( current.state.playlist.map(row => row.stockName), [evidence.fixture.stockDisplayName, evidence.fixture.stockDisplayName], "two-cut playlist stock order"); assertExactArray( current.state.playlist.map(row => `${row.graphicType}|${row.subtype}`), ["1열판기본|현재가", "1열판기본|시간외단일가"], "two-cut playlist mapping"); await checkpoint("multi-cut-drag", { playlistRowIds: current.state.playlist.map(row => row.rowId) }); current = await dragCutsToPlaylist( 0, 0, 1, "single-cut captured-pointer drag"); if (current.state.playlist.length !== 3) { failKnown("The two cut drags did not create exactly three in-memory rows."); } assertExactArray( current.state.playlist.map(row => `${row.graphicType}|${row.subtype}`), [ "1열판기본|현재가", "1열판기본|시간외단일가", "1열판기본|예상체결가" ], "three-cut playlist mapping"); assertExactArray( current.dom.playlist.map(row => row.rowHeaderText), ["1", "2", "3"], "playlist row-header ordinals"); if (current.dom.playlist.some(row => !Number.isFinite(row.rowHeaderWidth) || Math.abs(row.rowHeaderWidth - 30) > 0.5 || row.rowHeaderDragEnabled !== true)) { failKnown("Playlist row-header geometry or drag availability differs from FarPoint."); } await checkpoint("single-cut-drag", { playlistRowIds: current.state.playlist.map(row => row.rowId) }); const [rowA, rowB, rowC] = current.state.playlist.map(row => row.rowId); await pointerClickPlaylistRow(rowA, "select playlist row A"); current = await waitFor("playlist row A selection", snapshot => activePlaylistId(snapshot) === rowA && JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([rowA]), { allowUiBusy: false }); await pointerClickPlaylistRow(rowB, "Ctrl-select playlist row B", ctrlModifier); current = await waitFor("playlist A+B selection", snapshot => activePlaylistId(snapshot) === rowB && JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([rowA, rowB]), { allowUiBusy: false }); await dragPlaylistRow(rowA, rowC, "single-source playlist row drag"); current = await waitFor("single-source playlist reorder", snapshot => JSON.stringify(snapshot.state.playlist.map(row => row.rowId)) === JSON.stringify([rowB, rowC, rowA]), { allowUiBusy: false }); assertExactArray(selectedPlaylistIds(current), [rowA], "post-drag deletion selection"); if (activePlaylistId(current) !== rowA || current.dom.activeElement?.playlistRowId !== rowA) { failKnown("Playlist drag did not retain the source as active and focused."); } await checkpoint("playlist-single-source-drag", { beforeOrder: [rowA, rowB, rowC], afterOrder: [rowB, rowC, rowA] }); assertExactArray( current.dom.playlist.map(row => row.rowHeaderText), ["1", "2", "3"], "reordered playlist row-header ordinals"); const orderBeforeCheckboxAttempt = current.state.playlist.map(row => row.rowId); const enabledBeforeCheckboxAttempt = current.state.playlist.map(row => ({ rowId: row.rowId, enabled: row.isEnabled })); await attemptCheckboxOriginDrag(rowB, rowA, "checkbox-origin drag rejection"); current = await waitFor("checkbox-origin drag rejection", snapshot => snapshot.state.isBusy === false && activePlaylistId(snapshot) === rowB, { allowUiBusy: false }); assertExactArray( current.state.playlist.map(row => row.rowId), orderBeforeCheckboxAttempt, "checkbox-origin playlist order"); if (JSON.stringify(current.state.playlist.map(row => ({ rowId: row.rowId, enabled: row.isEnabled }))) !== JSON.stringify(enabledBeforeCheckboxAttempt)) { failKnown("Checkbox-origin drag changed a row's enabled state."); } assertExactArray(selectedPlaylistIds(current), [rowA], "checkbox-origin deletion selection"); if (current.dom.playlist.some(row => row.dragging || row.dragBefore || row.dragAfter)) { failKnown("Checkbox-origin drag left playlist drag visuals active."); } await checkpoint("playlist-checkbox-drag-rejected"); const focusBeforeBoundary = current.dom.activeElement; if (focusBeforeBoundary?.playlistRowId !== rowB) { failKnown("Checkbox-origin pointer did not leave focus inside source row B."); } await pressKey("End", "select last playlist boundary without moving focus"); current = await waitFor("playlist End boundary", snapshot => activePlaylistId(snapshot) === rowA, { allowUiBusy: false }); assertExactArray(selectedPlaylistIds(current), [rowA], "End deletion selection"); if (JSON.stringify(current.dom.activeElement) !== JSON.stringify(focusBeforeBoundary)) { failKnown("End moved DOM focus instead of only changing the F8 candidate."); } await pressKey("Home", "select first playlist boundary without moving focus"); current = await waitFor("playlist Home boundary", snapshot => activePlaylistId(snapshot) === rowB, { allowUiBusy: false }); assertExactArray(selectedPlaylistIds(current), [rowA], "Home deletion selection"); if (JSON.stringify(current.dom.activeElement) !== JSON.stringify(focusBeforeBoundary)) { failKnown("Home moved DOM focus instead of only changing the F8 candidate."); } await checkpoint("playlist-keyboard-focus-split"); current = await requestWindowsInput( current, rowB, rowA, rowC, "Windows SendInput row-header drag and Home key"); await pointerClick( 'document.getElementById("db-load-button")', "open DB playlist load modal"); current = await waitFor("read-only named playlist load modal", snapshot => snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === false && snapshot.dom.namedConfirmationHidden === true && snapshot.dom.activeInsideVisibleModal === true, { timeoutMilliseconds: 60_000, allowUiBusy: true }); if (current.dom.namedTitle !== "DB 재생목록 불러오기") { failKnown(`Unexpected load-modal title ${current.dom.namedTitle}.`); } await pressKey("Escape", "child load modal Escape no-op"); await sleep(100); current = await readSnapshot(); assertSafety(current, "load modal Escape", false); if (current.dom.namedModalHidden || !current.dom.namedConfirmationHidden || !current.dom.activeInsideVisibleModal) { failKnown("Escape closed the read-only named-playlist child modal."); } const focusableCount = current.dom.modalFocusableIds.length; if (focusableCount < 2) failKnown("The named-playlist modal has fewer than two focusable controls."); for (let index = 0; index <= focusableCount; index += 1) { await pressKey("Tab", `load modal Shift+Tab cycle ${index + 1}`, shiftModifier); const tabSnapshot = await readSnapshot(); assertSafety(tabSnapshot, `load modal Shift+Tab cycle ${index + 1}`, false); if (!tabSnapshot.dom.activeInsideVisibleModal) { failKnown("Shift+Tab escaped the named-playlist modal focus trap."); } } await pointerClick( 'document.getElementById("named-playlist-cancel-button")', "close load modal with Cancel"); current = await waitFor("load modal close and focus restore", snapshot => snapshot.dom.namedModalHidden === true && snapshot.dom.activeElement?.id === "db-load-button", { allowUiBusy: false }); await checkpoint("named-playlist-load-modal-focus", { definitionCount: current.state.namedPlaylist?.definitions?.length ?? 0, focusableCount }); await pointerClick( 'document.getElementById("db-save-button")', "open DB playlist save modal"); current = await waitFor("read-only named playlist save modal", snapshot => snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === false && snapshot.dom.namedConfirmationHidden === true && snapshot.dom.activeInsideVisibleModal === true, { timeoutMilliseconds: 60_000, allowUiBusy: true }); if (current.dom.namedTitle !== "DB 재생목록 저장") { failKnown(`Unexpected save-modal title ${current.dom.namedTitle}.`); } if (current.dom.namedConfirmDisabled) { evidence.warnings.push( "Nested save confirmation was skipped because no existing definition was safely selectable."); await pointerClick( 'document.getElementById("named-playlist-cancel-button")', "close save modal without an existing definition"); current = await waitFor("save modal fixture skip close", snapshot => snapshot.dom.namedModalHidden === true && snapshot.dom.activeElement?.id === "db-save-button", { allowUiBusy: false }); } else { const namedBeforeConfirmation = { revision: current.state.namedPlaylist?.revision ?? null, lastMutationOutcome: current.state.namedPlaylist?.lastMutationOutcome ?? null, isWriteInProgress: current.state.namedPlaylist?.isWriteInProgress ?? null }; await pointerClick( 'document.getElementById("named-playlist-confirm-button")', "open save confirmation without confirming write"); current = await waitFor("nested save confirmation", snapshot => snapshot.dom.namedModalHidden === false && snapshot.dom.namedConfirmationHidden === false && snapshot.dom.namedConfirmationMessage === "저장하겠습니까?" && snapshot.dom.activeElement?.id === "named-playlist-confirmation-yes", { allowUiBusy: false }); await pressKey("Escape", "nested save confirmation Escape no-op"); await sleep(100); current = await readSnapshot(); assertSafety(current, "nested confirmation Escape", false); if (current.dom.namedConfirmationHidden || current.dom.activeElement?.id !== "named-playlist-confirmation-yes") { failKnown("Escape changed the nested save confirmation."); } await pressKey("Tab", "move nested confirmation focus to No"); current = await readSnapshot(); if (current.dom.activeElement?.id !== "named-playlist-confirmation-no") { failKnown("Tab did not move nested confirmation focus from Yes to No."); } await pressKey("Tab", "wrap nested confirmation focus to Yes"); current = await readSnapshot(); if (current.dom.activeElement?.id !== "named-playlist-confirmation-yes") { failKnown("Tab did not wrap nested confirmation focus back to Yes."); } await pointerClick( 'document.getElementById("named-playlist-confirmation-no")', "decline save confirmation"); current = await waitFor("declined save confirmation close", snapshot => snapshot.dom.namedModalHidden === true && snapshot.dom.namedConfirmationHidden === true && snapshot.dom.activeElement?.id === "db-save-button", { allowUiBusy: false }); const namedAfterConfirmation = { revision: current.state.namedPlaylist?.revision ?? null, lastMutationOutcome: current.state.namedPlaylist?.lastMutationOutcome ?? null, isWriteInProgress: current.state.namedPlaylist?.isWriteInProgress ?? null }; if (JSON.stringify(namedAfterConfirmation) !== JSON.stringify(namedBeforeConfirmation)) { failKnown("Declining the save confirmation changed named-playlist mutation state."); } } await checkpoint("named-playlist-save-confirmation-no-write"); current = await checkpoint("final", { expectedPlaylistOrder: [rowC, rowA, rowB] }); assertExactArray( current.state.playlist.map(row => row.rowId), [rowC, rowA, rowB], "final playlist order"); assertExactArray(selectedPlaylistIds(current), [rowB], "final deletion selection"); if (!current.dom.namedModalHidden || !current.dom.namedConfirmationHidden) { failKnown("A named-playlist modal remained open at final state."); } evidence.finalSnapshot = current; } try { const target = await discoverExactTarget(); const endpoint = validateWebSocketTarget(target); evidence.target.id = target.id; evidence.target.url = target.url; await connect(endpoint); await executeSafeSequence(); await captureScreenshot(); const sealedSnapshot = await readSnapshot(); assertSafety(sealedSnapshot, "post-screenshot safety seal", false); evidence.finalSnapshot = sealedSnapshot; evidence.result = evidence.warnings.length === 0 ? "PASS" : "PASS_WITH_WARNING"; } catch (error) { evidence.result = "FAIL"; evidence.error = errorProjection(error); process.exitCode = 1; if (socket?.readyState === WebSocket.OPEN) { try { evidence.finalSnapshot = await readSnapshot(); } catch (snapshotError) { evidence.warnings.push(`Failure snapshot unavailable: ${snapshotError.message}`); } try { await captureScreenshot(); } catch (screenshotError) { evidence.warnings.push(`Failure screenshot unavailable: ${screenshotError.message}`); } } } finally { cleanupMode = true; cleanupDeadlineEpochMilliseconds = Date.now() + 5_000; const cleanupFailures = []; if (pointerIsPressed && socket?.readyState === WebSocket.OPEN) { try { await releaseMouse(pointerReleasePoint || { x: 1, y: 1 }, pointerReleaseModifiers); } catch (releaseError) { evidence.warnings.push(`Pointer release cleanup failed: ${releaseError.message}`); cleanupFailures.push(`pointer release: ${releaseError.message}`); } } if (socket?.readyState === WebSocket.OPEN) { try { await removeProbe(); } catch (probeError) { evidence.warnings.push(`Probe cleanup failed: ${probeError.message}`); cleanupFailures.push(`probe restore: ${probeError.message}`); } socket.close(); } else if (probeInstalled) { evidence.warnings.push( "Probe cleanup failed: the CDP socket was closed before bridge restoration."); cleanupFailures.push("probe restore: CDP socket closed"); } if (cleanupFailures.length > 0 && evidence.result !== "FAIL") { evidence.result = "FAIL"; evidence.error = { category: "OUTCOME_UNKNOWN", name: "CleanupFailure", message: "The one-shot input cleanup did not complete." }; process.exitCode = 1; } evidence.completedAt = new Date().toISOString(); fs.writeFileSync( configuration.outputPath, `${JSON.stringify(evidence, null, 2)}\n`, { flag: "wx" }); } if (evidence.result === "FAIL") { console.error(JSON.stringify({ result: evidence.result, output: configuration.outputPath, screenshot: evidence.screenshot?.path || null, error: evidence.error })); } else { console.log(JSON.stringify({ result: evidence.result, output: configuration.outputPath, screenshot: evidence.screenshot })); }