import fs from "node:fs"; import path from "node:path"; import { createHash } from "node:crypto"; const exactUrl = "https://legacy-parity.mbn.local/index.html"; const allowedArguments = new Set(["port", "output", "screenshot", "timeout-ms"]); const forbiddenCdpMethods = new Set([ "Browser.close", "Page.close", "Target.closeTarget", "Runtime.terminateExecution", "Input.dispatchMouseEvent", "Input.dispatchKeyEvent", "Input.insertText" ]); class SmokeFailure extends Error { constructor(kind, message) { super(`${kind}: ${message}`); this.name = "SmokeFailure"; this.kind = kind; } } function fail(kind, message) { throw new SmokeFailure(kind, message); } function parseArguments(argv) { if (argv.length === 0 || argv.length % 2 !== 0) { fail("KNOWN_FAILURE", "Use --port, --output, --screenshot, and optional --timeout-ms pairs."); } const values = new Map(); for (let index = 0; index < argv.length; index += 2) { const key = argv[index]?.replace(/^--/u, ""); const value = argv[index + 1]; if (!key || !argv[index].startsWith("--") || !value || !allowedArguments.has(key) || values.has(key)) { fail("KNOWN_FAILURE", "Arguments must be unique supported --name value pairs."); } values.set(key, value); } for (const key of ["port", "output", "screenshot"]) { if (!values.has(key)) fail("KNOWN_FAILURE", `Missing --${key}.`); } const port = Number(values.get("port")); if (!Number.isInteger(port) || port < 1024 || port > 65535 || port === 30001) { fail("KNOWN_FAILURE", "--port must be a non-Tornado port from 1024 through 65535."); } const outputPath = path.resolve(values.get("output")); const screenshotPath = path.resolve(values.get("screenshot")); if (!path.isAbsolute(values.get("output")) || path.extname(outputPath).toLowerCase() !== ".json" || !path.isAbsolute(values.get("screenshot")) || path.extname(screenshotPath).toLowerCase() !== ".png" || outputPath.toLowerCase() === screenshotPath.toLowerCase()) { fail("KNOWN_FAILURE", "Output must be a distinct absolute .json and screenshot a distinct absolute .png."); } const timeoutMilliseconds = values.has("timeout-ms") ? Number(values.get("timeout-ms")) : 30000; if (!Number.isInteger(timeoutMilliseconds) || timeoutMilliseconds < 5000 || timeoutMilliseconds > 120000) { fail("KNOWN_FAILURE", "--timeout-ms must be from 5000 through 120000."); } if (fs.existsSync(outputPath) || fs.existsSync(screenshotPath)) { fail("KNOWN_FAILURE", "Evidence files must not already exist."); } fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.mkdirSync(path.dirname(screenshotPath), { recursive: true }); return { port, outputPath, screenshotPath, timeoutMilliseconds }; } const configuration = parseArguments(process.argv.slice(2)); const deadline = Date.now() + configuration.timeoutMilliseconds; const evidence = { schemaVersion: 1, result: "RUNNING", startedAt: new Date().toISOString(), completedAt: null, target: { expectedUrl: exactUrl, port: configuration.port, id: null, url: null }, safety: { noInputDispatched: true, noNativeIntentAfterGuard: true, // app.js emits this empty, read-only bootstrap before CDP can attach. // Seeing native-rendered DRY RUN / IDLE below proves that its state reply arrived; // this harness does not falsely claim to have observed the earlier outbound call. bootstrapReadyContract: { type: "ready", payload: {}, inferredFromAuthoritativeState: true, nativeRoute: "LegacyReadyIntent => _controller.Current" }, preGuardStartupSettle: null, blockedMessages: [], observedMessagesAfterGuard: [] }, shell: null, screenshot: null, error: null }; let socket; let nextId = 1; const pending = new Map(); let guardInstalled = false; function remaining() { const value = deadline - Date.now(); if (value <= 0) fail("OUTCOME_UNKNOWN", "Observation deadline expired; nothing was retried."); return value; } function sleep(milliseconds) { return new Promise(resolve => setTimeout(resolve, Math.min(milliseconds, remaining()))); } async function discoverTarget() { const discoveryUrl = `http://127.0.0.1:${configuration.port}/json/list`; while (Date.now() < deadline) { try { const response = await fetch(discoveryUrl, { redirect: "error", signal: AbortSignal.timeout(1000) }); const targets = await response.json(); const matches = Array.isArray(targets) ? targets.filter(target => target?.type === "page" && target.url === exactUrl) : []; if (matches.length > 1) fail("KNOWN_FAILURE", "More than one exact package document was exposed."); if (matches.length === 1 && matches[0].id && matches[0].webSocketDebuggerUrl) return matches[0]; } catch (error) { if (error instanceof SmokeFailure) throw error; } await sleep(100); } fail("KNOWN_FAILURE", "The exact package WebView target was not found."); } async function connect(target) { 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}`) { fail("KNOWN_FAILURE", "The target CDP endpoint is not the exact loopback page endpoint."); } socket = new WebSocket(endpoint.href); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new SmokeFailure("OUTCOME_UNKNOWN", "CDP open timed out.")), 5000); socket.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true }); socket.addEventListener("error", () => { clearTimeout(timer); reject(new SmokeFailure("OUTCOME_UNKNOWN", "CDP open failed.")); }, { once: true }); }); socket.addEventListener("message", event => { const message = JSON.parse(String(event.data)); if (!Object.hasOwn(message, "id")) return; const request = pending.get(message.id); if (!request) return; pending.delete(message.id); clearTimeout(request.timer); message.error ? request.reject(new SmokeFailure("KNOWN_FAILURE", `${request.method}: ${message.error.message || "CDP error"}`)) : request.resolve(message.result); }); } function rpc(method, params = {}) { if (forbiddenCdpMethods.has(method)) fail("KNOWN_FAILURE", `Forbidden CDP method ${method}.`); if (!socket || socket.readyState !== WebSocket.OPEN) fail("OUTCOME_UNKNOWN", "CDP is unavailable."); const id = nextId++; return new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(id); reject(new SmokeFailure("OUTCOME_UNKNOWN", `${method} timed out; nothing was retried.`)); }, Math.min(10000, remaining())); pending.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) fail("KNOWN_FAILURE", response.exceptionDetails.text || "Runtime evaluation failed."); return response.result?.value; } async function waitForSettledStartup() { const settleDeadline = Math.min(Date.now() + 10_000, deadline); let stableSignature = null; let stableSince = 0; let lastSample = null; while (Date.now() < settleDeadline) { lastSample = await evaluate(`(() => { const currentMenu = Array.from(document.querySelectorAll( "#workspace-navigation .workspace-nav-item[aria-current='page']")); const region = document.getElementById("workspace-region"); return { readyState: document.readyState, bodyBusy: document.body?.getAttribute("aria-busy") || null, bridgeReady: typeof window.chrome?.webview?.postMessage === "function", connectionText: (document.getElementById("playout-connection-state")?.textContent || "").trim(), playoutStatus: (document.getElementById("playout-status")?.textContent || "").trim(), activeWorkspace: region?.dataset?.activeWorkspace || null, activeMarketTab: region?.dataset?.activeMarketTab || null, startWorkspace: document.body?.dataset?.startWorkspace || null, workspaceTitle: (document.getElementById("workspace-title")?.textContent || "").trim(), currentMenu: currentMenu.map(element => element.id || element.dataset?.tabId || null) }; })()`); const ready = lastSample?.readyState === "complete" && lastSample.bodyBusy === "false" && lastSample.bridgeReady === true && lastSample.connectionText === "DRY RUN" && lastSample.playoutStatus.startsWith("IDLE") && ["stock", "catalog", "settings"].includes(lastSample.activeWorkspace) && ["stockCut", "lastWorkspace"].includes(lastSample.startWorkspace) && lastSample.workspaceTitle.length > 0 && lastSample.currentMenu.length === 1; const signature = ready ? JSON.stringify({ activeWorkspace: lastSample.activeWorkspace, activeMarketTab: lastSample.activeMarketTab, startWorkspace: lastSample.startWorkspace, workspaceTitle: lastSample.workspaceTitle, currentMenu: lastSample.currentMenu }) : null; if (signature && signature === stableSignature) { if (Date.now() - stableSince >= 400) { return { stableMilliseconds: Date.now() - stableSince, activeWorkspace: lastSample.activeWorkspace, activeMarketTab: lastSample.activeMarketTab, startWorkspace: lastSample.startWorkspace, workspaceTitle: lastSample.workspaceTitle, currentMenu: lastSample.currentMenu }; } } else { stableSignature = signature; stableSince = signature ? Date.now() : 0; } await sleep(75); } if (Date.now() >= deadline) { fail("OUTCOME_UNKNOWN", "Startup did not settle before the observation deadline; no guard was installed."); } fail("KNOWN_FAILURE", `Startup did not reach a stable DryRun workspace: ${JSON.stringify(lastSample)}`); } async function installNoIntentGuard() { const installed = await evaluate(`(() => { const bridge = window.chrome?.webview; if (!bridge || globalThis.__legacyUiOnlySmoke) return false; const probe = { original: bridge.postMessage, blocked: [], observed: [] }; probe.wrapper = message => { const record = { type: typeof message?.type === "string" ? message.type : null, at: new Date().toISOString() }; probe.observed.push(record); probe.blocked.push(record); return undefined; }; bridge.postMessage = probe.wrapper; if (bridge.postMessage !== probe.wrapper) throw new Error("postMessage guard identity changed"); globalThis.__legacyUiOnlySmoke = probe; return true; })()`); if (installed !== true) fail("KNOWN_FAILURE", "The no-intent WebView guard could not be installed exactly once."); guardInstalled = true; } async function readShell() { return evaluate(`(() => { const probe = globalThis.__legacyUiOnlySmoke; const text = id => document.getElementById(id)?.textContent?.trim() || null; const button = id => { const element = document.getElementById(id); return element ? { present: true, disabled: element.disabled === true, text: element.textContent.trim() } : { present: false }; }; const rectangle = element => { if (!element) return null; const bounds = element.getBoundingClientRect(); const style = getComputedStyle(element); return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom, width: bounds.width, height: bounds.height, display: style.display, visibility: style.visibility }; }; const isVisible = (element, bounds) => Boolean(element && bounds && element.hidden !== true && !element.closest("[hidden]") && bounds.width > 0 && bounds.height > 0 && bounds.right > 0 && bounds.bottom > 0 && bounds.left < document.documentElement.clientWidth && bounds.top < document.documentElement.clientHeight && bounds.display !== "none" && bounds.visibility !== "hidden"); const operatorShell = document.querySelector(".operator-shell"); const workspaceRegion = document.getElementById("workspace-region"); const navigationToggle = document.getElementById("workspace-nav-toggle"); const stockTab = document.getElementById("workspace-stock-tab"); const marketTabs = Array.from(document.querySelectorAll("#market-tabs > button[data-tab-id]")); const settingsTab = document.getElementById("workspace-settings-tab"); const stockWorkspace = document.getElementById("stock-workspace"); const catalogWorkspace = document.getElementById("catalog-workspace"); const settingsWorkspace = document.getElementById("settings-workspace"); const splitter = document.getElementById("workspace-splitter"); const schedule = document.querySelector(".playlist-panel"); const shellBounds = rectangle(operatorShell); const workspaceBounds = rectangle(workspaceRegion); const splitterBounds = rectangle(splitter); const scheduleBounds = rectangle(schedule); const scheduleCenter = scheduleBounds ? { x: scheduleBounds.left + scheduleBounds.width / 2, y: scheduleBounds.top + scheduleBounds.height / 2 } : null; const scheduleHit = scheduleCenter && scheduleCenter.x >= 0 && scheduleCenter.y >= 0 && scheduleCenter.x < document.documentElement.clientWidth && scheduleCenter.y < document.documentElement.clientHeight ? document.elementFromPoint(scheduleCenter.x, scheduleCenter.y) : null; return { url: location.href, readyState: document.readyState, bodyBusy: document.body?.getAttribute("aria-busy") || null, connectionText: text("playout-connection-state"), playoutStatus: text("playout-status"), title: document.title, controls: { search: button("stock-search-button"), prepare: button("playout-prepare"), takeIn: button("playout-take-in"), next: button("playout-next"), takeOut: button("playout-take-out") }, layout: { activeWorkspace: workspaceRegion?.dataset?.activeWorkspace || null, navigationAriaExpanded: navigationToggle?.getAttribute("aria-expanded") || null, navigationCollapsedClass: workspaceRegion?.classList.contains("nav-collapsed") === true, navigationExpanded: navigationToggle?.getAttribute("aria-expanded") === "true" && workspaceRegion?.classList.contains("nav-collapsed") !== true, stockTabCurrent: stockTab?.getAttribute("aria-current") || null, marketTabCount: marketTabs.length, marketTabsCurrent: marketTabs.map(button => ({ tabId: button.dataset.tabId, label: (button.querySelector(".workspace-nav-label")?.textContent || "").trim(), current: button.getAttribute("aria-current") || null, hasIcon: Boolean(button.querySelector("svg")), hasLabel: Boolean(button.querySelector(".workspace-nav-label")) })), settingsTabCurrent: settingsTab?.getAttribute("aria-current") || null, currentMenuItemCount: document.querySelectorAll( "#workspace-navigation .workspace-nav-item[aria-current='page']").length, stockWorkspaceVisible: isVisible(stockWorkspace, rectangle(stockWorkspace)) && stockWorkspace?.getAttribute("aria-hidden") === "false" && stockWorkspace?.hasAttribute("inert") !== true, stockWorkspaceHidden: stockWorkspace?.hidden === true && stockWorkspace?.getAttribute("aria-hidden") === "true" && stockWorkspace?.hasAttribute("inert") === true, catalogWorkspaceVisible: isVisible(catalogWorkspace, rectangle(catalogWorkspace)) && catalogWorkspace?.getAttribute("aria-hidden") === "false" && catalogWorkspace?.hasAttribute("inert") !== true, catalogWorkspaceHidden: catalogWorkspace?.hidden === true && catalogWorkspace?.getAttribute("aria-hidden") === "true" && catalogWorkspace?.hasAttribute("inert") === true, settingsWorkspaceVisible: isVisible(settingsWorkspace, rectangle(settingsWorkspace)) && settingsWorkspace?.getAttribute("aria-hidden") === "false" && settingsWorkspace?.hasAttribute("inert") !== true, settingsWorkspaceHidden: settingsWorkspace?.hidden === true && settingsWorkspace?.getAttribute("aria-hidden") === "true" && settingsWorkspace?.hasAttribute("inert") === true, workspaceTitle: text("workspace-title"), shell: shellBounds, workspace: workspaceBounds, splitter: splitterBounds, splitterState: splitter ? { role: splitter.getAttribute("role"), orientation: splitter.getAttribute("aria-orientation"), controls: splitter.getAttribute("aria-controls"), valueMinimum: Number(splitter.getAttribute("aria-valuemin")), valueMaximum: Number(splitter.getAttribute("aria-valuemax")), valueNow: Number(splitter.getAttribute("aria-valuenow")), tabIndex: splitter.tabIndex } : null, schedule: scheduleBounds, workspaceToScheduleWidthRatio: workspaceBounds && scheduleBounds && scheduleBounds.width > 0 ? workspaceBounds.width / scheduleBounds.width : null, scheduleShare: shellBounds && scheduleBounds && shellBounds.width > 0 ? scheduleBounds.width / shellBounds.width : null, scheduleVisible: isVisible(schedule, scheduleBounds), scheduleHitTestVisible: Boolean(scheduleHit && (scheduleHit === schedule || schedule?.contains(scheduleHit))) }, visibleDialogs: Array.from(document.querySelectorAll("[role='dialog'], [role='alertdialog']")) .filter(element => !element.hidden && !element.closest("[hidden]")).map(element => element.id || element.getAttribute("aria-label") || "dialog"), postMessageGuardInstalled: window.chrome?.webview?.postMessage === probe?.wrapper, observedMessagesAfterGuard: probe?.observed || [], blockedMessages: probe?.blocked || [] }; })()`); } function assertShell(shell) { if (!shell || shell.url !== exactUrl || shell.readyState !== "complete" || shell.bodyBusy !== "false" || shell.connectionText !== "DRY RUN" || !shell.playoutStatus?.startsWith("IDLE") || shell.visibleDialogs.length !== 0 || shell.postMessageGuardInstalled !== true || shell.observedMessagesAfterGuard.length !== 0 || shell.blockedMessages.length !== 0) { fail("KNOWN_FAILURE", "The package shell did not remain rendered DryRun/IDLE with zero guarded native intents."); } const layout = shell.layout; const currentMarkets = layout?.marketTabsCurrent?.filter(tab => tab.current === "page") || []; const navigationExpansionConsistent = layout && ((layout.navigationAriaExpanded === "true" && layout.navigationCollapsedClass === false) || (layout.navigationAriaExpanded === "false" && layout.navigationCollapsedClass === true)); const commonNavigationOk = layout && navigationExpansionConsistent && layout.marketTabCount === 10 && layout.currentMenuItemCount === 1 && layout.marketTabsCurrent.every(tab => tab.hasIcon === true && tab.hasLabel === true); const stockWorkspaceOk = layout?.activeWorkspace === "stock" && layout.stockTabCurrent === "page" && layout.settingsTabCurrent === "false" && currentMarkets.length === 0 && layout.stockWorkspaceVisible === true && layout.catalogWorkspaceHidden === true && layout.settingsWorkspaceHidden === true && layout.workspaceTitle === "종목·컷"; const settingsWorkspaceOk = layout?.activeWorkspace === "settings" && layout.stockTabCurrent === "false" && layout.settingsTabCurrent === "page" && currentMarkets.length === 0 && layout.stockWorkspaceHidden === true && layout.catalogWorkspaceHidden === true && layout.settingsWorkspaceVisible === true && layout.workspaceTitle === "설정"; const catalogWorkspaceOk = layout?.activeWorkspace === "catalog" && layout.stockTabCurrent === "false" && layout.settingsTabCurrent === "false" && currentMarkets.length === 1 && layout.stockWorkspaceHidden === true && layout.catalogWorkspaceVisible === true && layout.settingsWorkspaceHidden === true && layout.workspaceTitle === currentMarkets[0]?.label; if (!commonNavigationOk || [stockWorkspaceOk, settingsWorkspaceOk, catalogWorkspaceOk].filter(Boolean).length !== 1) { fail("KNOWN_FAILURE", "The package did not render one internally consistent initial workspace and menu selection."); } const splitterState = layout.splitterState; if (layout.scheduleVisible !== true || layout.scheduleHitTestVisible !== true || !layout.shell || !layout.workspace || !layout.splitter || !layout.schedule || !Number.isFinite(layout.workspaceToScheduleWidthRatio) || layout.workspaceToScheduleWidthRatio < 1.05 || layout.workspaceToScheduleWidthRatio > 1.95 || !Number.isFinite(layout.scheduleShare) || layout.scheduleShare < 0.33 || layout.scheduleShare > 0.49 || splitterState?.role !== "separator" || splitterState.orientation !== "vertical" || splitterState.controls !== "workspace-region" || splitterState.tabIndex !== 0 || !Number.isFinite(splitterState.valueMinimum) || splitterState.valueMinimum < 34 || !Number.isFinite(splitterState.valueMaximum) || splitterState.valueMaximum > 48 || !Number.isFinite(splitterState.valueNow) || splitterState.valueNow < splitterState.valueMinimum || splitterState.valueNow > splitterState.valueMaximum || Math.abs(layout.workspace.right - layout.splitter.left) > 1.5 || Math.abs(layout.splitter.right - layout.schedule.left) > 1.5 || Math.abs(layout.schedule.right - layout.shell.right) > 1.5 || Math.abs(layout.splitter.top - layout.shell.top) > 1.5 || Math.abs(layout.splitter.bottom - layout.shell.bottom) > 1.5) { fail("KNOWN_FAILURE", "The package did not render a visible right schedule beside the adjustable workspace layout."); } for (const [name, control] of Object.entries(shell.controls || {})) { if (!control?.present) fail("KNOWN_FAILURE", `Required shell control ${name} is missing.`); } } async function captureScreenshot() { const response = await rpc("Page.captureScreenshot", { format: "png", fromSurface: true, captureBeyondViewport: false }); if (typeof response.data !== "string" || response.data.length === 0) fail("KNOWN_FAILURE", "CDP returned no screenshot bytes."); const bytes = Buffer.from(response.data, "base64"); fs.writeFileSync(configuration.screenshotPath, bytes, { flag: "wx" }); return { path: configuration.screenshotPath, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex").toUpperCase() }; } try { const target = await discoverTarget(); evidence.target.id = target.id; evidence.target.url = target.url; await connect(target); await rpc("Runtime.enable"); await rpc("Page.enable"); evidence.safety.preGuardStartupSettle = await waitForSettledStartup(); await installNoIntentGuard(); await sleep(500); evidence.shell = await readShell(); assertShell(evidence.shell); evidence.safety.observedMessagesAfterGuard = evidence.shell.observedMessagesAfterGuard; evidence.safety.blockedMessages = evidence.shell.blockedMessages; evidence.screenshot = await captureScreenshot(); evidence.result = "PASS"; } catch (error) { evidence.result = "FAIL"; evidence.error = { kind: error instanceof SmokeFailure ? error.kind : "HARNESS_ERROR", message: String(error?.message || error) }; process.exitCode = 1; } finally { evidence.completedAt = new Date().toISOString(); if (socket?.readyState === WebSocket.OPEN) socket.close(); fs.writeFileSync(configuration.outputPath, `${JSON.stringify(evidence, null, 2)}\n`, { flag: "wx" }); }