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 expectedSourceSha256 = "1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2"; const expectedInitialDestinationSha256 = "13C07BA64587549EDA9C1A694F3DFA19875CB1F888F02C9E071F17C47739E087"; const expectedImportConfirmation = "현재 저장된 비교쌍의 순서는 그대로 유지하고, 원본 프로그램의 비교쌍 중 없는 항목만 뒤에 추가할까요?"; const allowedOutboundTypes = new Set([ "ready", "select-tab", "choose-operator-folder", "import-legacy-comparison-pairs", "confirm-native-dialog" ]); const forbiddenCdpMethods = new Set([ "Browser.close", "Page.close", "Runtime.terminateExecution", "Target.closeTarget", "Input.dispatchMouseEvent", "Input.dispatchKeyEvent" ]); const targetExpressions = Object.freeze({ "settings-navigation": 'document.getElementById("workspace-settings-tab")', "design-folder-picker": 'document.getElementById("operator-design-folder-select")', "resource-folder-picker": 'document.getElementById("operator-resource-folder-select")', "background-folder-picker": 'document.getElementById("operator-background-folder-select")', "comparison-navigation": 'document.querySelector("button[data-tab-id=\\"comparison\\"]")', "comparison-import": 'document.querySelector("button[data-comparison-import=\\"true\\"]")', "native-confirmation-yes": 'document.getElementById("legacy-dialog-ok")' }); class SmokeFailure extends Error { constructor(category, message) { super(`${category}: ${message}`); this.name = "SmokeFailure"; this.category = category; } } function failKnown(message) { throw new SmokeFailure("KNOWN_FAILURE", message); } function failUnknown(message) { throw new SmokeFailure("OUTCOME_UNKNOWN", message); } function parseArguments(argv) { const accepted = new Set([ "port", "output", "screenshot", "exchange-directory", "timeout-ms" ]); if (argv.length === 0 || argv.length % 2 !== 0) { failKnown( "Usage: node scripts/Test-LegacyPackageLocalStateSmoke.mjs " + "--port --output --screenshot " + "--exchange-directory [--timeout-ms ]"); } const values = new Map(); for (let index = 0; index < argv.length; index += 2) { const rawName = argv[index]; const value = argv[index + 1]; if (!rawName?.startsWith("--") || !value) failKnown("Arguments must be --name value pairs."); const name = rawName.slice(2); if (!accepted.has(name) || values.has(name)) failKnown(`Invalid argument --${name}.`); values.set(name, value); } for (const required of ["port", "output", "screenshot", "exchange-directory"]) { if (!values.has(required)) failKnown(`--${required} is required.`); } const port = Number(values.get("port")); const timeoutMilliseconds = Number(values.get("timeout-ms") || "180000"); if (!Number.isInteger(port) || port < 1024 || port > 65535) { failKnown("--port must be from 1024 through 65535."); } if (!Number.isInteger(timeoutMilliseconds) || timeoutMilliseconds < 30_000 || timeoutMilliseconds > 900_000) { failKnown("--timeout-ms must be from 30000 through 900000."); } const outputPath = path.resolve(values.get("output")); const screenshotPath = path.resolve(values.get("screenshot")); const exchangeDirectory = path.resolve(values.get("exchange-directory")); if (!path.isAbsolute(values.get("output")) || path.extname(outputPath).toLowerCase() !== ".json" || !path.isAbsolute(values.get("screenshot")) || path.extname(screenshotPath).toLowerCase() !== ".png" || !path.isAbsolute(values.get("exchange-directory"))) { failKnown("Output, screenshot and exchange paths must be absolute and typed correctly."); } const unique = new Set([ outputPath.toLowerCase(), screenshotPath.toLowerCase(), exchangeDirectory.toLowerCase() ]); if (unique.size !== 3) failKnown("Evidence paths must be distinct."); if (fs.existsSync(outputPath) || fs.existsSync(screenshotPath)) { failKnown("Evidence files are never overwritten."); } if (!fs.existsSync(exchangeDirectory) || !fs.statSync(exchangeDirectory).isDirectory() || fs.readdirSync(exchangeDirectory).length !== 0) { failKnown("The wrapper must provide one existing empty exchange directory."); } return { port, timeoutMilliseconds, outputPath, screenshotPath, exchangeDirectory }; } const configuration = parseArguments(process.argv.slice(2)); const hardDeadline = Date.now() + configuration.timeoutMilliseconds; const startedAt = new Date().toISOString(); let socket = null; let nextRpcId = 1; let nextInputSequence = 1; let probeInstalled = false; let cancellationRequested = false; const pendingRpc = new Map(); const evidence = { schemaVersion: 1, profile: "local-settings-comparison-import", result: "RUNNING", startedAt, completedAt: null, target: { expectedUrl: exactTargetUrl, port: configuration.port, id: null, url: null }, safety: { requiredMode: "dryRun", requiredPhase: "idle", allowedOutboundTypes: [...allowedOutboundTypes], observedOutboundTypes: [], blockedOutboundMessages: [], stateViolations: [], inputRetryCount: 0, appCloseRequested: false, playoutIntentIssued: false }, files: { expectedSourceSha256, expectedSourceRowCount: 8, expectedInitialDestinationSha256, expectedInitialDestinationRowCount: 1, settingsBaseline: null, destinationAfterFirstImport: null, destinationAfterSecondImport: null }, settings: { pickerKinds: [], allCancelled: false, settingsFileUnchanged: false }, comparison: { initialPair: null, afterFirstImport: null, afterSecondImport: null, initialPairPreserved: false, secondImportIdempotent: false }, exchanges: [], checkpoints: [], finalSnapshot: null, screenshot: null, error: null }; process.stdin.setEncoding("utf8"); process.stdin.on("data", chunk => { if (String(chunk).split(/\r?\n/u).includes("CANCEL")) cancellationRequested = true; }); // The wrapper keeps stdin open so it can fail-stop an in-flight helper. Do not // let that control channel keep Node alive after PASS or a sealed early failure. if (typeof process.stdin.unref === "function") process.stdin.unref(); function assertDeadline(label) { if (cancellationRequested) failUnknown(`The wrapper cancelled ${label}; no action is retried.`); if (Date.now() >= hardDeadline) failUnknown(`The global deadline expired ${label}; no action is retried.`); } async function sleep(milliseconds) { assertDeadline("before waiting"); await new Promise(resolve => setTimeout(resolve, Math.min(milliseconds, hardDeadline - Date.now()))); assertDeadline("after waiting"); } function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex").toUpperCase(); } function stablePair(pair) { if (!pair) return null; return { rowId: pair.rowId, rowNumber: pair.rowNumber, displayLabel: pair.displayLabel, isSelected: pair.isSelected === true }; } async function discoverTarget() { const discoveryUrl = `http://127.0.0.1:${configuration.port}/json/list`; let lastError = null; while (Date.now() < Math.min(hardDeadline, Date.now() + 15_000)) { try { const response = await fetch(discoveryUrl, { cache: "no-store" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const targets = await response.json(); const exact = targets.filter(target => target?.type === "page" && target.url === exactTargetUrl); if (exact.length > 1) failKnown("More than one exact package WebView target exists."); if (exact.length === 1) return exact[0]; } catch (error) { if (error instanceof SmokeFailure) throw error; lastError = String(error?.message || error); } await sleep(100); } failKnown(`The exact package target was not found${lastError ? ` (${lastError})` : ""}.`); } function validateEndpoint(target) { if (!target?.id || !target.webSocketDebuggerUrl) failKnown("The target has no 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 debugger endpoint is not the exact loopback page endpoint."); } return endpoint; } async function connect(endpoint) { if (typeof WebSocket !== "function") failKnown("This Node runtime has no WebSocket API."); socket = new WebSocket(endpoint.href); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new SmokeFailure( "OUTCOME_UNKNOWN", "CDP socket open timed out.")), 5_000); socket.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true }); socket.addEventListener("error", () => { clearTimeout(timer); reject(new SmokeFailure( "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 = pendingRpc.get(message.id); if (!pending) return; pendingRpc.delete(message.id); clearTimeout(pending.timer); if (message.error) pending.reject(new SmokeFailure( "KNOWN_FAILURE", `${pending.method} failed: ${message.error.message || "CDP error"}`)); else pending.resolve(message.result); }); socket.addEventListener("close", () => { for (const pending of pendingRpc.values()) { clearTimeout(pending.timer); pending.reject(new SmokeFailure( "OUTCOME_UNKNOWN", `CDP closed while waiting for ${pending.method}.`)); } pendingRpc.clear(); }); } function rpc(method, params = {}, timeoutMilliseconds = 10_000) { assertDeadline(`before ${method}`); if (forbiddenCdpMethods.has(method)) { if (method.includes("close") || method.includes("terminate")) { evidence.safety.appCloseRequested = true; } failKnown(`Forbidden CDP method ${method} was refused.`); } if (!socket || socket.readyState !== WebSocket.OPEN) failUnknown("CDP socket is not open."); const id = nextRpcId++; return new Promise((resolve, reject) => { const timer = setTimeout(() => { pendingRpc.delete(id); reject(new SmokeFailure("OUTCOME_UNKNOWN", `${method} timed out; no retry was attempted.`)); }, Math.min(timeoutMilliseconds, hardDeadline - Date.now())); pendingRpc.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) { failKnown(response.exceptionDetails.exception?.description || response.exceptionDetails.text || "Runtime evaluation failed."); } return response.result?.value; } async function installProbe() { const token = `local-state-${Date.now()}-${randomBytes(8).toString("hex")}`; const installed = await evaluate(`(() => { if (!window.chrome?.webview) throw new Error("WebView2 bridge is unavailable."); if (globalThis.__legacyLocalStateProbe) throw new Error("Probe already exists."); const allowed = new Set(${JSON.stringify([...allowedOutboundTypes])}); const probe = { token: ${JSON.stringify(token)}, states: [], outboundMessages: [], pointerEvents: [], blockedOutboundMessages: [], stateViolations: [], originalPostMessage: null, postMessageWrapper: null, listener: null, pointerListener: null }; probe.listener = event => { if (event.data?.type !== "state" || !event.data.payload) return; const payload = event.data.payload; probe.states.push(structuredClone(payload)); if (probe.states.length > 500) 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.isConnected === true) reasons.push("connected"); if (playout.preparedCode != null) reasons.push("prepared"); if (playout.onAirCode != null) reasons.push("on-air"); if (playout.outcomeUnknown === true) reasons.push("outcome-unknown"); if (playout.isPlayCompletionPending === true) reasons.push("play-pending"); if (playout.isTakeOutCompletionPending === true) reasons.push("takeout-pending"); if (playout.isBusy === true) reasons.push("playout-busy"); if (playout.refreshActive === true) reasons.push("refresh-active"); } if (reasons.length) probe.stateViolations.push({ at: new Date().toISOString(), revision: payload.revision ?? null, reasons }); }; probe.originalPostMessage = window.chrome.webview.postMessage; probe.pointerListener = event => { const element = event.target instanceof Element ? event.target : null; let targetId = null; if (element?.closest("#workspace-settings-tab")) targetId = "settings-navigation"; else if (element?.closest("#operator-design-folder-select")) { targetId = "design-folder-picker"; } else if (element?.closest("#operator-resource-folder-select")) { targetId = "resource-folder-picker"; } else if (element?.closest("#operator-background-folder-select")) { targetId = "background-folder-picker"; } else if (element?.closest('button[data-tab-id="comparison"]')) { targetId = "comparison-navigation"; } else if (element?.closest('button[data-comparison-import="true"]')) { targetId = "comparison-import"; } else if (element?.closest("#legacy-dialog-ok")) { targetId = "native-confirmation-yes"; } probe.pointerEvents.push({ sequence: probe.pointerEvents.length + 1, type: event.type, targetId, isTrusted: event.isTrusted === true, pointerType: event.pointerType, button: event.button, buttons: event.buttons, at: new Date().toISOString() }); if (probe.pointerEvents.length > 100) probe.pointerEvents.shift(); }; probe.postMessageWrapper = message => { const record = { sequence: probe.outboundMessages.length + 1, type: typeof message?.type === "string" ? message.type : null, at: new Date().toISOString() }; probe.outboundMessages.push(record); if (!record.type || !allowed.has(record.type)) { probe.blockedOutboundMessages.push(record); 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 safety gate could not be installed."); } globalThis.__legacyLocalStateProbe = probe; window.chrome.webview.addEventListener("message", probe.listener); document.addEventListener("pointerdown", probe.pointerListener, true); document.addEventListener("pointerup", probe.pointerListener, true); window.chrome.webview.postMessage({ type: "ready", payload: {} }); } catch (error) { window.chrome.webview.postMessage = probe.originalPostMessage; delete globalThis.__legacyLocalStateProbe; throw error; } return probe.token; })()`); if (installed !== token) failKnown("The probe was not installed exactly once."); probeInstalled = true; } async function removeProbe() { if (!probeInstalled || !socket || socket.readyState !== WebSocket.OPEN) return; const removed = await evaluate(`(() => { const probe = globalThis.__legacyLocalStateProbe; if (!probe) return false; window.chrome.webview.removeEventListener("message", probe.listener); document.removeEventListener("pointerdown", probe.pointerListener, true); document.removeEventListener("pointerup", probe.pointerListener, true); if (window.chrome.webview.postMessage !== probe.postMessageWrapper) { throw new Error("The outbound safety gate identity changed."); } window.chrome.webview.postMessage = probe.originalPostMessage; delete globalThis.__legacyLocalStateProbe; return true; })()`); if (removed !== true) failUnknown("The safety probe was not restored exactly once."); probeInstalled = false; } async function readSnapshot() { return await evaluate(`(() => { const probe = globalThis.__legacyLocalStateProbe; const state = probe?.states?.at(-1) || null; const active = document.activeElement; return { state: state ? structuredClone(state) : null, safety: { wrapped: probe?.postMessageWrapper != null && window.chrome?.webview?.postMessage === probe.postMessageWrapper, outboundMessages: (probe?.outboundMessages || []).map(value => ({ ...value })), blockedOutboundMessages: (probe?.blockedOutboundMessages || []).map(value => ({ ...value })), stateViolations: (probe?.stateViolations || []).map(value => ({ ...value, reasons: [...value.reasons] })) }, dom: { url: location.href, readyState: document.readyState, bodyBusy: document.body.getAttribute("aria-busy"), connectionText: (document.getElementById("playout-connection-state")?.textContent || "").trim(), settingsVisible: document.getElementById("settings-workspace")?.hidden === false && document.getElementById("settings-workspace")?.inert !== true, settingsCurrent: document.getElementById("workspace-settings-tab") ?.getAttribute("aria-current"), activeTabId: state?.tabs?.find(tab => tab.isActive === true)?.id || null, legacyDialogHidden: document.getElementById("legacy-dialog")?.hidden === true, legacyDialogText: (document.getElementById("legacy-dialog-message")?.textContent || "").trim(), legacyDialogOkText: (document.getElementById("legacy-dialog-ok")?.textContent || "").trim(), importStatus: (document.querySelector(".comparison-import-status")?.textContent || "").trim(), activeElementId: active?.id || null } }; })()`); } function assertSafety(snapshot, label, { allowBusy = false, allowDialog = false } = {}) { if (!snapshot?.state?.playout) failKnown(`Native state is missing at ${label}.`); const playout = snapshot.state.playout; if (snapshot.safety.wrapped !== true) failUnknown(`The safety gate is absent at ${label}.`); evidence.safety.observedOutboundTypes = snapshot.safety.outboundMessages.map(row => row.type); evidence.safety.blockedOutboundMessages = snapshot.safety.blockedOutboundMessages; evidence.safety.stateViolations = snapshot.safety.stateViolations; evidence.safety.playoutIntentIssued = snapshot.safety.outboundMessages.some(row => ["prepare-playout", "take-in", "next-playout", "take-out", "choose-background", "toggle-background"].includes(row.type)); if (snapshot.safety.blockedOutboundMessages.length) { failKnown(`An unapproved outbound message was blocked at ${label}.`); } if (snapshot.safety.stateViolations.length) failUnknown(`DryRun left its safe state at ${label}.`); if (playout.mode !== "dryRun" || playout.phase !== "idle" || playout.isConnected === true || playout.preparedCode != null || playout.onAirCode != null || playout.outcomeUnknown === true || playout.isPlayCompletionPending === true || playout.isTakeOutCompletionPending === true || playout.isBusy === true || playout.refreshActive === true) { failUnknown(`Playout is not exact DryRun IDLE at ${label}.`); } if (!allowBusy && snapshot.state.isBusy === true) failKnown(`The UI is busy at ${label}.`); if (!allowDialog && snapshot.dom.legacyDialogHidden !== true) { failKnown(`An unexpected native dialog is visible at ${label}.`); } if (snapshot.dom.url !== exactTargetUrl || snapshot.dom.readyState !== "complete" || snapshot.dom.connectionText !== "DRY RUN") { failKnown(`The exact DryRun document is not ready at ${label}.`); } } async function waitFor(label, predicate, options = {}) { const allowMissingState = options.allowMissingState === true; if (allowMissingState && label !== "DryRun IDLE preflight") { failKnown("Only the input-free initial preflight may await the first native state."); } const deadline = Math.min(hardDeadline, Date.now() + (options.timeoutMilliseconds || 45_000)); let last = null; while (Date.now() < deadline) { last = await readSnapshot(); if (!last?.state?.playout && allowMissingState) { if (last?.safety?.wrapped !== true) { failUnknown("The safety gate is absent while awaiting the first native state."); } evidence.safety.observedOutboundTypes = last.safety.outboundMessages.map(row => row.type); evidence.safety.blockedOutboundMessages = last.safety.blockedOutboundMessages; evidence.safety.stateViolations = last.safety.stateViolations; if (last.safety.blockedOutboundMessages.length) { failKnown("An unapproved outbound message was blocked before initial state."); } if (last.safety.stateViolations.length) { failUnknown("A native state violation was observed before initial state."); } if (last.dom.url !== exactTargetUrl || last.dom.readyState !== "complete" || last.dom.connectionText !== "DRY RUN") { failKnown("The exact DryRun document changed while awaiting initial state."); } await sleep(100); continue; } assertSafety(last, label, options); if (predicate(last)) return last; await sleep(100); } failUnknown(`Timed out waiting for ${label}; no input was retried.`); } function exchangePath(sequence, suffix) { return path.join(configuration.exchangeDirectory, `${String(sequence).padStart(2, "0")}.${suffix}.json`); } function readSmallJson(filePath, label) { const stat = fs.statSync(filePath); if (!stat.isFile() || stat.size <= 0 || stat.size > 64 * 1024) { failKnown(`${label} is empty or too large.`); } const bytes = fs.readFileSync(filePath); let value; try { value = JSON.parse(bytes.toString("utf8")); } catch { failKnown(`${label} is not strict JSON.`); } return { bytes, value, sha256: sha256(bytes) }; } async function elementGeometry(targetId) { const expression = targetExpressions[targetId]; if (!expression) failKnown(`Closed target ${targetId} is unavailable.`); const value = await evaluate(`(() => { const element = ${expression}; if (!element) return null; element.scrollIntoView({ block: "nearest", inline: "nearest" }); const rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, width: rect.width, height: rect.height, viewport: { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }, devicePixelRatio: window.devicePixelRatio, disabled: element.disabled === true, hidden: element.hidden === true || element.closest("[hidden]") != null, text: (element.textContent || "").trim(), ariaCurrent: element.getAttribute("aria-current") }; })()`); if (!value || value.disabled || value.hidden || value.width <= 0 || value.height <= 0 || !Number.isFinite(value.x) || !Number.isFinite(value.y) || value.x < 0 || value.y < 0 || value.x >= value.viewport.width || value.y >= value.viewport.height || value.devicePixelRatio < 0.25 || value.devicePixelRatio > 5) { failKnown(`${targetId} is not a unique visible input target.`); } return value; } async function requestExchange(operation, targetId = null, folderKind = null) { const sequence = nextInputSequence++; const token = randomBytes(16).toString("hex").toUpperCase(); const geometry = targetId ? await elementGeometry(targetId) : null; const pointerEventStart = operation === "observe-local-files" ? null : await evaluate("globalThis.__legacyLocalStateProbe?.pointerEvents?.length ?? null"); if (operation !== "observe-local-files" && !Number.isInteger(pointerEventStart)) { failUnknown("The trusted pointer evidence baseline is unavailable."); } const request = { schemaVersion: 1, token, sequence, targetUrl: exactTargetUrl, operation, targetId, folderKind, point: geometry ? { x: geometry.x, y: geometry.y } : null, viewport: geometry ? geometry.viewport : null, devicePixelRatio: geometry ? geometry.devicePixelRatio : null, mouseDownCalls: operation === "observe-local-files" ? 0 : 1, mouseUpCalls: operation === "observe-local-files" ? 0 : 1, escapeKeyCalls: operation === "folder-picker-click-cancel" ? 1 : 0, inputRetryCount: 0 }; const requestPath = exchangePath(sequence, "request"); const acknowledgementPath = exchangePath(sequence, "ack"); const requestBytes = Buffer.from(`${JSON.stringify(request)}\n`, "utf8"); fs.writeFileSync(requestPath, requestBytes, { flag: "wx" }); const deadline = Math.min(hardDeadline, Date.now() + 45_000); while (!fs.existsSync(acknowledgementPath)) { if (Date.now() >= deadline) failUnknown(`Input exchange ${sequence} timed out; no retry was attempted.`); await sleep(50); } const acknowledgement = readSmallJson(acknowledgementPath, `Input acknowledgement ${sequence}`); const ack = acknowledgement.value; if (!ack || ack.schemaVersion !== 1 || ack.token !== token || ack.sequence !== sequence || ack.result !== "PASS" || ack.operation !== operation || ack.targetId !== targetId || ack.folderKind !== folderKind || ack.packageIdentityValidated !== true || ack.dryRunValidated !== true || ack.tornadoConnectionCount !== 0 || ack.inputRetryCount !== 0 || ack.mouseDownCalls !== request.mouseDownCalls || ack.mouseUpCalls !== request.mouseUpCalls || ack.escapeKeyCalls !== request.escapeKeyCalls || !ack.files) { failKnown(`Input acknowledgement ${sequence} does not match its one-shot request.`); } const physicalPointer = operation === "observe-local-files" ? null : await waitForTrustedPointerClick(targetId, pointerEventStart); evidence.exchanges.push({ sequence, operation, targetId, folderKind, requestPath, requestSha256: sha256(requestBytes), acknowledgementPath, acknowledgementSha256: acknowledgement.sha256, picker: ack.picker || null, physicalPointer, files: ack.files }); return ack; } async function waitForTrustedPointerClick(targetId, startIndex) { const deadline = Math.min(hardDeadline, Date.now() + 10_000); while (Date.now() < deadline) { const events = await evaluate(`(() => { const events = globalThis.__legacyLocalStateProbe?.pointerEvents || []; return events.slice(${Number(startIndex)}).map(value => ({ ...value })); })()`); const relevant = events.filter(event => event.targetId === targetId); if (relevant.length >= 2) { const down = relevant.find(event => event.type === "pointerdown"); const up = relevant.find(event => event.type === "pointerup"); if (!down || !up || down.sequence >= up.sequence || down.isTrusted !== true || up.isTrusted !== true || down.pointerType !== "mouse" || up.pointerType !== "mouse" || down.button !== 0 || up.button !== 0 || down.buttons !== 1 || up.buttons !== 0) { failKnown(`The ${targetId} click did not produce one trusted mouse down/up pair.`); } return { down, up }; } await sleep(25); } failUnknown(`Trusted pointer evidence for ${targetId} did not appear; no click was retried.`); } function assertExactPreflightFiles(files) { if (files.source?.sha256 !== expectedSourceSha256 || files.source?.rowCount !== 8 || files.source?.isReadOnlySource !== true || files.destination?.sha256 !== expectedInitialDestinationSha256 || files.destination?.rowCount !== 1 || files.destination?.firstPair !== "KRX100지수|코스피 지수") { failKnown("The original source or one-row current comparison baseline changed."); } evidence.files.settingsBaseline = files.settings; } function assertSettingsUnchanged(files, label) { if (JSON.stringify(files.settings) !== JSON.stringify(evidence.files.settingsBaseline)) { failKnown(`The settings JSON changed ${label}.`); } } async function runSequence() { await rpc("Runtime.enable"); await rpc("Page.enable"); await installProbe(); const initial = await waitFor("DryRun IDLE preflight", snapshot => snapshot.state?.playout?.mode === "dryRun" && snapshot.state.isBusy !== true, { allowBusy: false, allowMissingState: true }); const preflight = await requestExchange("observe-local-files"); assertExactPreflightFiles(preflight.files); evidence.checkpoints.push({ name: "exact-file-preflight", files: preflight.files }); await requestExchange("application-click", "settings-navigation"); await waitFor("settings workspace", snapshot => snapshot.dom.settingsVisible === true && snapshot.dom.settingsCurrent === "page", { allowBusy: false }); for (const [targetId, folderKind] of [ ["design-folder-picker", "design"], ["resource-folder-picker", "resource"], ["background-folder-picker", "background"] ]) { const ack = await requestExchange("folder-picker-click-cancel", targetId, folderKind); if (!ack.picker || ack.picker.uniqueNewPicker !== true || ack.picker.ownerLinkedToApplication !== true || ack.picker.escapeCancelled !== true || ack.picker.closedAfterEscape !== true) { failKnown(`The ${folderKind} folder picker was not proven as the app-owned cancelled picker.`); } assertSettingsUnchanged(ack.files, `after cancelling ${folderKind}`); evidence.settings.pickerKinds.push(folderKind); await waitFor(`${folderKind} picker cancellation return`, snapshot => snapshot.state.isBusy !== true && snapshot.dom.settingsVisible === true && snapshot.dom.legacyDialogHidden === true, { allowBusy: false }); } const settingsObservation = await requestExchange("observe-local-files"); assertSettingsUnchanged(settingsObservation.files, "after all three picker cancellations"); evidence.settings.allCancelled = true; evidence.settings.settingsFileUnchanged = true; await requestExchange("application-click", "comparison-navigation"); let comparison = await waitFor("comparison one-row baseline", snapshot => snapshot.state.isBusy !== true && snapshot.dom.activeTabId === "comparison" && snapshot.state.comparison?.isBusy !== true && snapshot.state.comparison?.savedPairs?.length === 1 && snapshot.state.comparison?.canImportLegacyPairs === true, { allowBusy: false }); const initialPair = stablePair(comparison.state.comparison.savedPairs[0]); if (typeof initialPair?.rowId !== "string" || initialPair.rowId.length === 0 || initialPair.rowNumber !== 1 || initialPair.displayLabel !== "KRX100지수 ↔ 코스피 지수") { failKnown("The visible one-row comparison baseline is not the expected known pair."); } evidence.comparison.initialPair = initialPair; await requestExchange("application-click", "comparison-import"); await waitFor("first import confirmation", snapshot => snapshot.state.dialog?.isConfirmation === true && snapshot.state.dialog?.message === expectedImportConfirmation && snapshot.dom.legacyDialogHidden === false && snapshot.dom.legacyDialogText === expectedImportConfirmation && snapshot.dom.legacyDialogOkText === "예", { allowBusy: false, allowDialog: true }); await requestExchange("application-click", "native-confirmation-yes"); comparison = await waitFor("first import completion", snapshot => { const model = snapshot.state.comparison; const receipt = model?.lastImport; return snapshot.state.isBusy !== true && model?.isBusy !== true && snapshot.dom.legacyDialogHidden === true && model?.savedPairs?.length === 9 && receipt?.sourceSha256 === expectedSourceSha256 && receipt?.sourceRowCount === 8 && receipt?.addedPairCount === 8 && receipt?.skippedDuplicateCount === 0 && receipt?.totalPairCount === 9; }, { allowBusy: true, allowDialog: true, timeoutMilliseconds: 90_000 }); const afterFirstPairs = comparison.state.comparison.savedPairs.map(stablePair); if (JSON.stringify(afterFirstPairs[0]) !== JSON.stringify(initialPair)) { failKnown("The original current comparison pair was not preserved at the front."); } evidence.comparison.initialPairPreserved = true; evidence.comparison.afterFirstImport = { receipt: comparison.state.comparison.lastImport, pairCount: afterFirstPairs.length, pairs: afterFirstPairs }; const afterFirstFiles = await requestExchange("observe-local-files"); assertSettingsUnchanged(afterFirstFiles.files, "after the first comparison import"); if (afterFirstFiles.files.destination?.rowCount !== 9 || afterFirstFiles.files.destination?.firstPair !== "KRX100지수|코스피 지수" || afterFirstFiles.files.destination?.sha256 === expectedInitialDestinationSha256) { failKnown("The first comparison import file is not an appended nine-row result."); } evidence.files.destinationAfterFirstImport = afterFirstFiles.files.destination; await requestExchange("application-click", "comparison-import"); await waitFor("second import confirmation", snapshot => snapshot.state.dialog?.isConfirmation === true && snapshot.state.dialog?.message === expectedImportConfirmation && snapshot.dom.legacyDialogHidden === false, { allowBusy: false, allowDialog: true }); await requestExchange("application-click", "native-confirmation-yes"); comparison = await waitFor("second import idempotent completion", snapshot => { const model = snapshot.state.comparison; const receipt = model?.lastImport; return snapshot.state.isBusy !== true && model?.isBusy !== true && snapshot.dom.legacyDialogHidden === true && model?.savedPairs?.length === 9 && receipt?.sourceSha256 === expectedSourceSha256 && receipt?.sourceRowCount === 8 && receipt?.addedPairCount === 0 && receipt?.skippedDuplicateCount === 8 && receipt?.totalPairCount === 9; }, { allowBusy: true, allowDialog: true, timeoutMilliseconds: 90_000 }); const afterSecondPairs = comparison.state.comparison.savedPairs.map(stablePair); if (JSON.stringify(afterSecondPairs) !== JSON.stringify(afterFirstPairs)) { failKnown("The second import changed the ordered comparison pair list."); } const finalFiles = await requestExchange("observe-local-files"); assertSettingsUnchanged(finalFiles.files, "after the second comparison import"); if (JSON.stringify(finalFiles.files.destination) !== JSON.stringify(afterFirstFiles.files.destination)) { failKnown("The second import changed the comparison file hash or contents."); } if (finalFiles.files.source?.sha256 !== expectedSourceSha256 || finalFiles.files.source?.rowCount !== 8) { failUnknown("The read-only original source changed during the smoke."); } evidence.files.destinationAfterSecondImport = finalFiles.files.destination; evidence.comparison.afterSecondImport = { receipt: comparison.state.comparison.lastImport, pairCount: afterSecondPairs.length, pairs: afterSecondPairs }; evidence.comparison.secondImportIdempotent = true; const finalSnapshot = await readSnapshot(); assertSafety(finalSnapshot, "final state", { allowBusy: false }); evidence.finalSnapshot = finalSnapshot; await captureScreenshot(); await removeProbe(); if (nextInputSequence !== 14 || evidence.exchanges.length !== 13) { failUnknown("The closed thirteen-step exchange sequence did not complete exactly once."); } if (evidence.safety.observedOutboundTypes.filter(type => type === "import-legacy-comparison-pairs").length !== 2 || evidence.safety.observedOutboundTypes.filter(type => type === "confirm-native-dialog").length !== 2 || evidence.safety.observedOutboundTypes.filter(type => type === "choose-operator-folder").length !== 3) { failKnown("The two import confirmations did not emit exactly two request/Yes pairs."); } evidence.result = "PASS"; } async function captureScreenshot() { const response = await rpc("Page.captureScreenshot", { format: "png", fromSurface: true, captureBeyondViewport: false }, 20_000); if (!response?.data) failKnown("CDP returned no screenshot bytes."); 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) }; } function errorProjection(error) { return { category: error instanceof SmokeFailure ? error.category : "HARNESS_ERROR", name: error?.name || "Error", message: String(error?.message || error) }; } let exitCode = 0; try { const target = await discoverTarget(); evidence.target.id = target.id; evidence.target.url = target.url; await connect(validateEndpoint(target)); await runSequence(); } catch (error) { exitCode = 1; evidence.result = error instanceof SmokeFailure && error.category === "OUTCOME_UNKNOWN" ? "OUTCOME_UNKNOWN" : "FAIL"; evidence.error = errorProjection(error); try { if (!evidence.screenshot && socket?.readyState === WebSocket.OPEN) await captureScreenshot(); } catch { /* Preserve the first failure. */ } try { if (probeInstalled && socket?.readyState === WebSocket.OPEN) await removeProbe(); } catch { /* Preserve the first failure and leave state observable. */ } } finally { evidence.completedAt = new Date().toISOString(); try { const bytes = Buffer.from(`${JSON.stringify(evidence, null, 2)}\n`, "utf8"); fs.writeFileSync(configuration.outputPath, bytes, { flag: "wx" }); } catch { exitCode = 1; } if (socket?.readyState === WebSocket.OPEN) socket.close(); } process.exitCode = exitCode;