#!/usr/bin/env node import { spawn } from "node:child_process"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; const platformUrl = process.env.LOCAL_DEBUG_PLATFORM_URL || `http://127.0.0.1:${process.env.LOCAL_DEBUG_PLATFORM_PORT || "18080"}`; const webUrl = process.env.LOCAL_DEBUG_WEB_URL || `http://127.0.0.1:${process.env.LOCAL_DEBUG_WEB_PORT || "5173"}`; const localDebugRoot = process.env.LOCAL_DEBUG_ROOT || path.resolve(".local-debug"); const evidenceDir = process.env.BROWSER_ACCEPTANCE_EVIDENCE_DIR || path.join(localDebugRoot, "browser-acceptance"); const apiUrl = `${platformUrl}/api/v1`; const forbiddenFragments = [ { name: "host user path", pattern: /\/Users\// }, { name: "private tmp path", pattern: /\/private\// }, { name: "unix socket", pattern: /unix:\/\// }, { name: "tcp socket", pattern: /tcp:\/\// }, { name: "bearer token", pattern: /Bearer\s+/ }, { name: "raw api key", pattern: /sk-[A-Za-z0-9_-]+/ }, { name: "password query", pattern: /password\s*=/i }, { name: "credential value", pattern: /credential\s*[:=]\s*\S+/i }, { name: "DSN value", pattern: /\b(?:postgres(?:ql)?|mysql|redis|mongodb(?:\+srv)?):\/\//i }, { name: "RCON value", pattern: /\brcon(?:[_-]?(?:password|token))?\s*[:=]\s*\S+/i }, { name: "host path", pattern: /\/(?:home|var|etc|opt|root|tmp)\/|[A-Za-z]:\\/ }, { name: "PID value", pattern: /\bpid\s*[:=#]?\s*\d+/i }, { name: "raw api key ref field", pattern: /apiKeyRef/ }, { name: "raw api key field", pattern: /rawApiKey/ }, { name: "session token", pattern: /sessionToken|run session token/i }, { name: "direct run URL", pattern: /direct run URL/i }, { name: "plugin-owned transport", pattern: /plugin-owned transport/i } ]; const fallbackFragments = [ { name: "English fallback", pattern: /fallback/i }, { name: "demo-only", pattern: /demo-only/i }, { name: "local fallback", pattern: /local fallback/i }, { name: "Chinese fallback", pattern: /本地回退|本地演示数据|演示数据|模拟数据|本地视图/ } ]; async function main() { await mkdir(evidenceDir, { recursive: true }); const smokeSeed = await loadSmokeSeed(); const session = await loginApi(); const authHeaders = { Authorization: `Bearer ${session.sessionId}` }; await ensureAiProvider(authHeaders); const [instances, endpoints, jobs, plugins, marketplace, users, providers, logStreams, artifacts, usage] = await Promise.all([ getJson("/server-instances", authHeaders), getJson("/run/endpoints?status=online", authHeaders), getJson(`/jobs?serverInstanceId=${encodeURIComponent(smokeSeed.serverLocalId)}`, authHeaders), getJson("/game-plugins", authHeaders), getJson("/plugin-marketplace/plugins", authHeaders), getJson("/users", authHeaders), getJson("/ai-providers", authHeaders), getJson("/log-streams", authHeaders), getJson("/artifacts", authHeaders), getJson("/metrics/platform", authHeaders) ]); const server = findRequired(instances.items, (item) => item.id === smokeSeed.serverLocalId, `${smokeSeed.serverLocalId} instance`); const runEndpoint = findRequired(endpoints.items, (item) => item.id === server.runEndpointId, `${server.runEndpointId} generated endpoint`); const plugin = findRequired(plugins.items, (item) => item.id === "game.example", "game.example plugin"); const marketplacePlugin = findRequired(marketplace.items, (item) => item.id === "game.example", "game.example marketplace plugin"); const operator = findRequired(users.items, (item) => item.email === "operator.local@example.test", "operator local user"); const aiProvider = findRequired(providers.items, (item) => item.id === "ai.openai" || item.apiKeyConfigured === true, "redacted AI provider"); assertEqual(server.pluginId, "game.example", "server is backed by game.example"); assertEqual(server.runEndpointId, smokeSeed.generatedRunEndpointId, "server is assigned to its generated Run"); if (runEndpoint.capabilities.includes("distribution.build")) { throw new Error("generated Run unexpectedly advertises distribution.build"); } assertIncludes(runEndpoint.capabilities, "process.install", "run endpoint exposes process.install"); assertIncludes(runEndpoint.capabilities, "process.start", "run endpoint exposes process.start"); assertIncludes(runEndpoint.capabilities, "process.stop", "run endpoint exposes process.stop"); assertIncludes(plugin.requiredRunCapabilities, "process.start", "plugin requires process.start"); assertIncludes(plugin.bridgeActions, "logs.query", "plugin exposes logs.query bridge action"); assertIncludes(plugin.bridgeActions, "artifacts.open", "plugin exposes artifacts.open bridge action"); assertIncludes(plugin.bridgeActions, "plugin-lifecycle.request", "plugin exposes Platform lifecycle bridge action"); assertIncludes(plugin.bridgeActions, "ai.invoke", "plugin exposes Platform AI bridge action"); assertIncludes(plugin.productionLifecycle?.operations, "rollback", "plugin declares rollback governance"); assertIncludes(marketplacePlugin.capabilities, "process.start", "marketplace exposes lifecycle capability"); assertEqual(aiProvider.apiKeyConfigured, true, "AI provider key presence projection"); assertEqual(aiProvider.baseUrlConfigured, true, "AI provider base URL presence projection"); if (Object.hasOwn(aiProvider, "baseUrl") || Object.hasOwn(aiProvider, "apiKeyRef")) { throw new Error("AI provider response exposed Platform-owned endpoint or secret reference"); } const productionSeed = await prepareProductionOperations(authHeaders, server, plugin); const chrome = await startChrome(); const evidence = { checkedAt: new Date().toISOString(), platformUrl, webUrl, localDebugRoot, smokeSeed, seedEvidenceDir: path.join(localDebugRoot, "smoke"), session: { userId: session.user.id, displayName: session.user.displayName, source: "api" }, environment: await verifyFrontendEnvironment(), browser: { chromePath: chrome.chromePath }, apiProof: { server: pick(server, ["id", "name", "pluginId", "pluginVersion", "runEndpointId", "state", "configVersion"]), runEndpoint: pick(runEndpoint, ["id", "displayName", "status", "capabilities"]), plugin: pick(plugin, ["id", "name", "version", "status", "manifestRef", "requiredRunCapabilities", "bridgeActions", "declaredPermissions"]), marketplacePlugin: pick(marketplacePlugin, ["id", "name", "version", "status", "manifestRef", "capabilities", "bridgeActions", "declaredPermissions"]), operator: pick(operator, ["id", "displayName", "email", "status", "roles"]), aiProvider: pick(aiProvider, ["id", "name", "kind", "relayMode", "status", "baseUrlConfigured", "apiKeyConfigured"]), jobs: jobs.items.map((job) => pick(job, ["id", "serverInstanceId", "runEndpointId", "capability", "state", "resultRef"])), logStreams: logStreams.items.map((stream) => pick(stream, ["id", "serverInstanceId", "streamKey", "source"])), artifacts: artifacts.items.map((artifact) => pick(artifact, ["id", "ownerKind", "ownerId", "state", "checksum"])), usage: pick(usage, ["cpuPercent", "memoryPercent", "diskPercent", "source"]), production: productionSeed.apiProof }, routes: [], safety: { forbiddenFragments: forbiddenFragments.map((item) => item.name), fallbackFragments: fallbackFragments.map((item) => item.name) } }; try { await chrome.navigate(webUrl); const loginState = await loginInBrowser(chrome); evidence.login = loginState; const routeChecks = [ { name: "首页", hash: "#/home", markers: ["平台概览", "运营数据已同步", "game.example", "运行节点", "CPU", "生产容量与告警", "运行槽位"] }, { name: "服务器管理", hash: "#/servers", markers: ["服务器管理", server.name, server.id, "创建服务器", "全部", "离线", "运行操作"] }, { name: "插件市场", hash: "#/plugins", markers: [ "插件市场", "平台 API", marketplacePlugin.id, marketplacePlugin.name, "Development plugin", "process.install", "process.start", "process.stop", "server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open" ] }, { name: "用户管理", hash: "#/users", markers: ["用户管理", "账号 API 已连接", operator.displayName, operator.email, "平台数据"] }, { name: "AI 提供商管理", hash: "#/aiProviders", markers: ["AI 提供商管理", "平台 API", aiProvider.name, "密钥状态", "已配置", "AI 配置审查", productionSeed.diff.diffSummary, server.id] }, { name: "系统维护", hash: "#/maintenance", markers: ["系统维护", "容量治理与告警闭环", "运行槽位", productionSeed.alert.title] }, { name: "服务器详情", hash: `#/servers/${encodeURIComponent(server.id)}`, markers: [ server.name, `${server.id} · 插件 ${server.pluginId}@${server.pluginVersion} · 节点 ${server.runEndpointId}`, "概览", "启动", "停止", "日志", "管理终端", "配置", "AI 助手", "操作历史" ] } ]; for (const route of routeChecks) { const state = await verifyBrowserRoute(chrome, route.hash, route.markers, route.name); evidence.routes.push(state); if (route.name === "服务器管理") { const runtimeMenu = await verifyServerQuickRuntimeMenu(chrome, route.name); evidence.routes.push({ name: "服务器管理 / 运行操作菜单", url: await chrome.url(), ...runtimeMenu }); } } const pluginPage = await clickAndVerify(chrome, "概览", ["插件概览", "dev-game-plugin 页面 bundle", "server.instances.read"]); evidence.routes.push({ name: "服务器详情 / 插件声明页面", url: await chrome.url(), ...pluginPage }); evidence.productionInteractions = { alert: await verifyAlertInteraction(chrome, authHeaders, productionSeed.alert), pluginLifecycle: await verifyPluginLifecycleInteraction(chrome, authHeaders, plugin, server), aiDiffApproval: await verifyAIConfigDiffInteraction(chrome, authHeaders, productionSeed.diff) }; evidence.walkthroughs = await verifyResponsiveThemeWalkthroughs(chrome, routeChecks, server); const operationProof = await verifyLifecycleOperation(authHeaders, server, chrome); evidence.operationProof = operationProof; } finally { await chrome.close(); } const evidencePath = path.join(evidenceDir, "browser-acceptance-evidence.json"); await writeFile(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`); console.log("browser acceptance passed"); console.log(`evidence directory: ${evidenceDir}`); console.log(`evidence file: ${evidencePath}`); } async function loadSmokeSeed() { const configPath = path.join(localDebugRoot, "smoke", "run-build-config.env"); const contents = await readFile(configPath, "utf8"); const values = {}; for (const line of contents.split(/\r?\n/)) { const separator = line.indexOf("="); if (separator <= 0) continue; values[line.slice(0, separator)] = line.slice(separator + 1); } for (const key of ["SMOKE_INVOCATION_ID", "SERVER_LOCAL_ID", "SCUM_ALPHA_ID", "SCUM_BETA_ID", "SCUM_DYNAMIC_ID", "GENERATED_RUN_ENDPOINT_ID"]) { if (!values[key]) throw new Error(`smoke seed configuration is missing ${key}`); } return { invocationId: values.SMOKE_INVOCATION_ID, serverLocalId: values.SERVER_LOCAL_ID, scumAlphaId: values.SCUM_ALPHA_ID, scumBetaId: values.SCUM_BETA_ID, scumDynamicId: values.SCUM_DYNAMIC_ID, generatedRunEndpointId: values.GENERATED_RUN_ENDPOINT_ID }; } async function loginApi() { const response = await postJson("/auth/login", { account: "operator.local@example.test", password: "operator-local" }, { "X-Auth-Token-Response": "bearer" }); if (!response.sessionId || response.status !== "authenticated") { throw new Error("local debug API login did not return an active session"); } return response; } async function loginInBrowser(chrome) { await chrome.waitForText(["账号 / 邮箱", "密码", "登录"], "login form"); await chrome.evaluate(() => { const inputs = Array.from(document.querySelectorAll("input")); const account = inputs.find((input) => input.type !== "password"); const password = inputs.find((input) => input.type === "password"); const submit = document.querySelector('button[type="submit"]'); if (!(account instanceof HTMLInputElement) || !(password instanceof HTMLInputElement) || !(submit instanceof HTMLButtonElement)) { throw new Error("login controls not found"); } const setInputValue = (input, value) => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set; setter.call(input, value); input.dispatchEvent(new Event("input", { bubbles: true })); }; setInputValue(account, "operator.local@example.test"); setInputValue(password, "operator-local"); submit.click(); }); await chrome.waitForText(["平台概览", "运营数据已同步"], "post-login home"); const visibleText = await chrome.visibleText(); scanText(visibleText, "登录后首页"); return { url: await chrome.url(), requiredMarkers: ["平台概览", "运营数据已同步"], fallbackScan: "passed", forbiddenFragmentScan: "passed" }; } async function verifyBrowserRoute(chrome, hash, markers, name) { await chrome.evaluate((nextHash) => { window.location.hash = nextHash; }, hash); await chrome.waitForText(markers, name); const visibleText = await chrome.visibleText(); assertMarkers(visibleText, markers, name); scanText(visibleText, name); return { name, url: await chrome.url(), requiredMarkers: markers, fallbackScan: "passed", forbiddenFragmentScan: "passed", textSample: visibleText.slice(0, 1200) }; } async function verifyResponsiveThemeWalkthroughs(chrome, routeChecks, server) { const walkthroughs = []; const scenarios = [ { name: "desktop / black mecha", viewport: { width: 1440, height: 960, mobile: false }, paletteId: "mecha-black", backgroundId: "mecha-grid", themeMarkers: ["Mecha Game Console", "黑色机甲 / OPS"] }, { name: "mobile / black mecha", viewport: { width: 390, height: 844, mobile: true }, paletteId: "mecha-black", backgroundId: "mecha-grid", themeMarkers: ["Mecha Game Console", "黑色机甲 / OPS"] }, { name: "desktop / magical-girl", viewport: { width: 1440, height: 960, mobile: false }, paletteId: "magical-girl", backgroundId: "magic-stage", themeMarkers: ["Mecha Game Console", "魔法少女 / OPS"] }, { name: "mobile / magical-girl", viewport: { width: 390, height: 844, mobile: true }, paletteId: "magical-girl", backgroundId: "magic-stage", themeMarkers: ["Mecha Game Console", "魔法少女 / OPS"] } ]; for (const scenario of scenarios) { await chrome.setViewport(scenario.viewport); await applyBrowserTheme(chrome, scenario.paletteId, scenario.backgroundId); const appliedPalette = await chrome.evaluate(() => document.documentElement.dataset.themePalette || ""); assertEqual(appliedPalette, scenario.paletteId, `${scenario.name} root theme marker`); const routeEvidence = []; if (scenario.viewport.mobile) { const mobileNavigation = await verifyMobileNavigation(chrome, scenario.name, scenario.themeMarkers); routeEvidence.push({ name: "移动导航抽屉", url: await chrome.url(), ...mobileNavigation, layout: await chrome.layoutSnapshot() }); } for (const route of routeChecks) { const routeThemeMarkers = scenario.viewport.mobile ? [] : scenario.themeMarkers; const state = await verifyBrowserRoute(chrome, route.hash, [...route.markers, ...routeThemeMarkers], `${scenario.name} / ${route.name}`); const layout = await chrome.layoutSnapshot(); assertNoVisibleLayoutIssues(layout, `${scenario.name} / ${route.name}`); routeEvidence.push({ name: route.name, url: state.url, requiredMarkers: state.requiredMarkers, fallbackScan: state.fallbackScan, forbiddenFragmentScan: state.forbiddenFragmentScan, layout, textSample: state.textSample }); if (route.name === "服务器管理") { const runtimeMenu = await verifyServerQuickRuntimeMenu(chrome, `${scenario.name} / 服务器管理`); const runtimeMenuLayout = await chrome.layoutSnapshot(); assertNoVisibleLayoutIssues(runtimeMenuLayout, `${scenario.name} / 服务器管理 / 运行操作菜单`); routeEvidence.push({ name: "服务器管理 / 运行操作菜单", url: await chrome.url(), requiredMarkers: runtimeMenu.requiredMarkers, fallbackScan: runtimeMenu.fallbackScan, forbiddenFragmentScan: runtimeMenu.forbiddenFragmentScan, layout: runtimeMenuLayout, textSample: runtimeMenu.textSample }); } } await chrome.evaluate((serverID) => { window.location.hash = `#/servers/${encodeURIComponent(serverID)}`; }, server.id); await chrome.waitForText([server.name, "概览"], `${scenario.name} / server detail tabs`); const pluginControls = await clickAndVerify(chrome, "概览", ["插件概览", "dev-game-plugin 页面 bundle", "server.instances.read"]); const pluginLayout = await chrome.layoutSnapshot(); assertNoVisibleLayoutIssues(pluginLayout, `${scenario.name} / plugin controls`); routeEvidence.push({ name: "服务器详情 / 插件声明页面", url: await chrome.url(), requiredMarkers: pluginControls.requiredMarkers, fallbackScan: pluginControls.fallbackScan, forbiddenFragmentScan: pluginControls.forbiddenFragmentScan, layout: pluginLayout, textSample: pluginControls.textSample }); walkthroughs.push({ name: scenario.name, viewport: scenario.viewport, paletteId: scenario.paletteId, backgroundId: scenario.backgroundId, routeCount: routeEvidence.length, routes: routeEvidence }); } return walkthroughs; } async function applyBrowserTheme(chrome, paletteId, backgroundId) { await chrome.evaluate( ({ paletteId: nextPaletteId, backgroundId: nextBackgroundId }) => { window.localStorage.setItem("platform-web.theme.palette", nextPaletteId); window.localStorage.setItem("platform-web.theme.backgroundPreset", nextBackgroundId); window.localStorage.removeItem("platform-web.theme.background"); window.location.reload(); }, { paletteId, backgroundId } ); await delay(500); } function assertNoVisibleLayoutIssues(layout, label) { if (layout.horizontalOverflow > 1) { throw new Error(`${label} has horizontal overflow: scrollWidth ${layout.scrollWidth} > viewport ${layout.innerWidth}`); } const overlapping = layout.overlappingControls.filter((item) => item.area > 24); if (overlapping.length > 0) { throw new Error(`${label} has overlapping controls: ${JSON.stringify(overlapping.slice(0, 3))}`); } if (layout.visibleTinyTextBoxes > 0) { throw new Error(`${label} has ${layout.visibleTinyTextBoxes} visible clipped/tiny text boxes`); } } async function clickAndVerify(chrome, buttonText, markers) { await chrome.evaluate((label) => { const button = Array.from(document.querySelectorAll("button")).find((item) => item.textContent?.includes(label)); if (!(button instanceof HTMLButtonElement)) { throw new Error(`button not found: ${label}`); } button.click(); }, buttonText); await chrome.waitForText(markers, buttonText); const visibleText = await chrome.visibleText(); assertMarkers(visibleText, markers, buttonText); scanText(visibleText, buttonText); return { requiredMarkers: markers, fallbackScan: "passed", forbiddenFragmentScan: "passed", textSample: visibleText.slice(0, 1200) }; } async function verifyServerQuickRuntimeMenu(chrome, label) { const markers = ["生成 run", "下载 run", "更新 run", "生成客户端", "依赖检查", "依赖安装", "历史日志"]; await chrome.evaluate(() => { const trigger = Array.from(document.querySelectorAll("button")).find((item) => item.textContent?.includes("运行操作")); if (!(trigger instanceof HTMLButtonElement)) { throw new Error("runtime action menu trigger not found"); } trigger.click(); }); await chrome.waitForText(markers, `${label} runtime action menu`); const visibleText = await chrome.visibleText(); assertMarkers(visibleText, markers, `${label} runtime action menu`); scanText(visibleText, `${label} runtime action menu`); return { requiredMarkers: markers, fallbackScan: "passed", forbiddenFragmentScan: "passed", textSample: visibleText.slice(0, 1200) }; } async function verifyMobileNavigation(chrome, label, themeMarkers) { const menuLabels = ["平台概览", "服务器管理", "插件市场", "用户管理", "AI 提供商", "系统工具"]; const closed = await chrome.evaluate(() => { const handle = document.querySelector(".mobile-sidebar-handle"); const sidebar = document.querySelector("#primary-sidebar"); return { handle: handle instanceof HTMLButtonElement, expanded: handle?.getAttribute("aria-expanded") === "true", sidebarOpen: sidebar?.classList.contains("app-sidebar-mobile-open") ?? false, mainTop: document.querySelector("main")?.getBoundingClientRect().top ?? null }; }); if (!closed.handle || closed.expanded || closed.sidebarOpen || closed.mainTop !== 0) { throw new Error(`${label} mobile drawer was not closed and out of page flow: ${JSON.stringify(closed)}`); } await chrome.evaluate(() => { const handle = document.querySelector(".mobile-sidebar-handle"); if (!(handle instanceof HTMLButtonElement)) throw new Error("mobile drawer handle not found"); handle.click(); }); await chrome.waitForText([...menuLabels, ...themeMarkers], `${label} mobile drawer open`); const opened = await chrome.evaluate(() => ({ expanded: document.querySelector(".mobile-sidebar-handle")?.getAttribute("aria-expanded") === "true", sidebarOpen: document.querySelector("#primary-sidebar")?.classList.contains("app-sidebar-mobile-open") ?? false, width: document.querySelector("#primary-sidebar")?.getBoundingClientRect().width ?? 0 })); if (!opened.expanded || !opened.sidebarOpen || opened.width <= 0 || opened.width > 320) { throw new Error(`${label} mobile drawer did not open as a bounded text sidebar: ${JSON.stringify(opened)}`); } const visibleText = await chrome.visibleText(); assertMarkers(visibleText, [...menuLabels, ...themeMarkers], `${label} mobile drawer`); scanText(visibleText, `${label} mobile drawer`); await chrome.evaluate(() => { const backdrop = document.querySelector(".mobile-sidebar-backdrop"); if (!(backdrop instanceof HTMLButtonElement)) throw new Error("mobile drawer backdrop not found"); backdrop.click(); }); await delay(220); const closedAgain = await chrome.evaluate(() => { const backdrop = document.querySelector(".mobile-sidebar-backdrop"); const style = backdrop ? window.getComputedStyle(backdrop) : null; return document.querySelector(".mobile-sidebar-handle")?.getAttribute("aria-expanded") !== "true" && style?.visibility === "hidden" && style?.pointerEvents === "none"; }); if (!closedAgain) throw new Error(`${label} mobile drawer did not close from backdrop`); return { requiredMarkers: [...menuLabels, ...themeMarkers], fallbackScan: "passed", forbiddenFragmentScan: "passed", closed, opened, closedAgain, textSample: visibleText.slice(0, 1200) }; } async function ensureAiProvider(headers) { const providers = await getJson("/ai-providers", headers); if (providers.items.some((item) => item.id === "ai.openai")) { return; } await postJson( "/ai-providers", { id: "ai.openai", name: "OpenAI Relay", kind: "openai-compatible", baseUrl: "https://relay.example.test/v1", apiKeyRef: "secret://providers/openai", models: ["gpt-4.1", "gpt-4.1-mini"], defaultModel: "gpt-4.1-mini", relayMode: "relay", timeoutMs: 30000, redactionPolicy: "default" }, headers ); } async function prepareProductionOperations(headers, server, plugin) { const stamp = Date.now(); const lifecycle = await postJson( `/plugin-lifecycles/${encodeURIComponent(plugin.id)}/actions`, { serverInstanceId: server.id, operation: "install", targetVersion: plugin.version, idempotencyKey: `browser-acceptance-plugin-install-${stamp}`, confirmed: false }, headers ); if (lifecycle.status !== "queued" || !lifecycle.job?.id || !lifecycle.installation?.id) { throw new Error(`plugin lifecycle seed did not create one durable job: ${JSON.stringify(lifecycle)}`); } const aiInvocation = await postJson( "/ai/invocations", { requestId: `browser-acceptance-ai-config-${stamp}`, serverInstanceId: server.id, purpose: "config.suggest", prompt: "Keep existing settings and add a reviewed max players recommendation." }, headers ); if (aiInvocation.status !== "ok" || !aiInvocation.configRecommendation?.diffId) { throw new Error(`AI invocation did not persist a reviewable diff: ${JSON.stringify(aiInvocation)}`); } const admission = await postJson( "/production/capacity/admission", { serverInstanceId: server.id, capability: "process.restart", idempotencyKey: `browser-acceptance-capacity-gap-${stamp}` }, headers ); if (admission.accepted || admission.state !== "denied" || !admission.alertId || !admission.auditEventId) { throw new Error(`capacity admission did not create durable denied evidence: ${JSON.stringify(admission)}`); } const [capacity, alerts, lifecycles, diffs] = await Promise.all([ getJson("/production/capacity", headers), getJson("/alerts", headers), getJson(`/plugin-lifecycles?pluginId=${encodeURIComponent(plugin.id)}&serverInstanceId=${encodeURIComponent(server.id)}`, headers), getJson(`/ai/config-diffs?serverInstanceId=${encodeURIComponent(server.id)}`, headers) ]); const alert = findRequired(alerts.items, (item) => item.id === admission.alertId && item.state === "active", "active capacity alert"); const installation = findRequired(lifecycles.items, (item) => item.id === lifecycle.installation.id && item.jobId === lifecycle.job.id, "durable plugin lifecycle installation"); const diff = findRequired(diffs.items, (item) => item.id === aiInvocation.configRecommendation.diffId && item.state === "pending", "pending AI config diff"); if (capacity.activeAlerts < 1 || !capacity.endpoints.some((item) => item.runEndpointId === server.runEndpointId)) { throw new Error(`production capacity summary did not include seeded state: ${JSON.stringify(capacity)}`); } for (const [label, value] of Object.entries({ admission, capacity, alert, installation, diff, aiInvocation })) { assertNoForbiddenProjection(value, `production seed ${label}`); } return { alert, diff, apiProof: { admission: pick(admission, ["accepted", "state", "reason", "pressureCodes", "alertId", "auditEventId"]), capacity: { ...pick(capacity, ["totalMaxJobs", "totalRunningJobs", "totalQueuedJobs", "activeAlerts", "generatedAt"]), endpoints: capacity.endpoints.map((item) => pick(item, ["runEndpointId", "status", "maxJobs", "runningJobs", "queuedJobs", "logBacklogBatches", "artifactBacklogChunks", "pressureCodes"])) }, alert: pick(alert, ["id", "sourceKind", "sourceId", "ruleKey", "severity", "state", "occurrenceCount", "lastAuditEventId"]), pluginLifecycle: pick(installation, ["id", "pluginId", "serverInstanceId", "currentVersion", "targetVersion", "desiredState", "currentState", "lastOperation", "compatibility", "dependencyState", "jobId", "auditEventId"]), aiConfigDiff: pick(diff, ["id", "requestId", "serverInstanceId", "pluginId", "providerId", "model", "key", "configVersion", "diffSummary", "state", "expiresAt"]) } }; } async function verifyAlertInteraction(chrome, headers, seededAlert) { await chrome.navigate(`${webUrl}/#/home`); await chrome.waitForText(["生产容量与告警", seededAlert.title, "确认"], "production alert interaction"); await chrome.evaluate((title) => { const item = Array.from(document.querySelectorAll(".production-alert-list .operation-item")).find((candidate) => candidate.textContent?.includes(title)); const button = Array.from(item?.querySelectorAll("button") || []).find((candidate) => candidate.textContent?.trim() === "确认"); if (!(button instanceof HTMLButtonElement)) throw new Error("capacity alert acknowledge button not found"); button.click(); }, seededAlert.title); await chrome.waitForText(["确认告警", seededAlert.id, "取消"], "alert confirmation dialog"); await chrome.evaluate(() => { const cancel = document.querySelector(".confirm-panel .confirm-actions button"); if (!(cancel instanceof HTMLButtonElement)) throw new Error("alert confirmation cancel button not found"); cancel.click(); }); await delay(100); if (await chrome.evaluate(() => Boolean(document.querySelector(".confirm-panel")))) { throw new Error("alert confirmation dialog did not close after cancel"); } const afterCancel = await getJson("/alerts", headers); const stillActive = findRequired(afterCancel.items, (item) => item.id === seededAlert.id, "alert after confirmation cancel"); assertEqual(stillActive.state, "active", "cancel keeps durable alert active"); await chrome.evaluate((title) => { const item = Array.from(document.querySelectorAll(".production-alert-list .operation-item")).find((candidate) => candidate.textContent?.includes(title)); const button = Array.from(item?.querySelectorAll("button") || []).find((candidate) => candidate.textContent?.trim() === "确认"); if (!(button instanceof HTMLButtonElement)) throw new Error("capacity alert acknowledge button not found after cancel"); button.click(); }, seededAlert.title); await chrome.waitForText(["确认告警", seededAlert.id], "alert confirmation reopen"); await chrome.evaluate(() => { const confirm = document.querySelector(".confirm-panel .confirm-primary"); if (!(confirm instanceof HTMLButtonElement)) throw new Error("alert confirmation submit button not found"); confirm.click(); }); await chrome.waitForText(["确认已由 Platform 持久化", "acknowledged"], "durable alert acknowledgement"); const alerts = await getJson("/alerts", headers); const acknowledged = findRequired(alerts.items, (item) => item.id === seededAlert.id, "acknowledged capacity alert"); assertEqual(acknowledged.state, "acknowledged", "browser alert acknowledgement persisted"); assertNoForbiddenProjection(acknowledged, "acknowledged alert response"); return { cancelPreservedState: stillActive.state, persisted: pick(acknowledged, ["id", "state", "acknowledgedBy", "acknowledgedAt", "lastAuditEventId"]), forbiddenFragmentScan: "passed", textSample: (await chrome.visibleText()).slice(0, 1200) }; } async function verifyPluginLifecycleInteraction(chrome, headers, plugin, server) { await chrome.navigate(`${webUrl}/#/plugins`); await chrome.waitForText(["插件市场", plugin.id, "查看详情"], "plugin lifecycle marketplace"); await chrome.evaluate((pluginId) => { const card = Array.from(document.querySelectorAll(".catalog-card")).find((candidate) => candidate.textContent?.includes(pluginId)); const button = card?.querySelector("button[title='查看插件详情']"); if (!(button instanceof HTMLButtonElement)) throw new Error("plugin detail button not found"); button.click(); }, plugin.id); await chrome.waitForText([plugin.name, "生产生命周期", server.id, "compatible"], "plugin lifecycle detail"); await chrome.evaluate(() => { const select = document.querySelector("select[aria-label='生命周期操作']"); if (!(select instanceof HTMLSelectElement)) throw new Error("plugin lifecycle operation select not found"); const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set; setter.call(select, "enable"); select.dispatchEvent(new Event("change", { bubbles: true })); }); await chrome.evaluate(() => { const button = document.querySelector(".plugin-lifecycle-controls .primary-command"); if (!(button instanceof HTMLButtonElement)) throw new Error("plugin lifecycle execute button not found"); button.click(); }); await chrome.waitForText(["确认启用", plugin.name, server.id], "plugin lifecycle confirmation"); await chrome.evaluate(() => { const confirm = Array.from(document.querySelectorAll(".confirm-panel .confirm-actions button")).find((item) => item.textContent?.trim() === "启用"); if (!(confirm instanceof HTMLButtonElement)) throw new Error("plugin lifecycle confirmation submit not found"); confirm.click(); }); await chrome.waitForText(["启用:queued", "任务"], "plugin lifecycle durable dispatch"); const response = await getJson(`/plugin-lifecycles?pluginId=${encodeURIComponent(plugin.id)}&serverInstanceId=${encodeURIComponent(server.id)}`, headers); const installation = findRequired(response.items, (item) => item.pluginId === plugin.id && item.serverInstanceId === server.id, "plugin lifecycle after browser dispatch"); assertEqual(installation.lastOperation, "enable", "browser lifecycle operation persisted"); if (!installation.jobId || !installation.auditEventId) { throw new Error(`browser lifecycle dispatch missed job/audit linkage: ${JSON.stringify(installation)}`); } assertNoForbiddenProjection(installation, "browser plugin lifecycle response"); return { persisted: pick(installation, ["id", "pluginId", "serverInstanceId", "currentState", "desiredState", "lastOperation", "dependencyState", "jobId", "auditEventId", "alertId"]), forbiddenFragmentScan: "passed", textSample: (await chrome.visibleText()).slice(0, 1200) }; } async function verifyAIConfigDiffInteraction(chrome, headers, seededDiff) { await chrome.navigate(`${webUrl}/#/aiProviders`); await chrome.waitForText(["AI 配置审查", seededDiff.serverInstanceId, seededDiff.diffSummary, "审查并批准"], "AI config diff review"); await chrome.evaluate(() => { const button = Array.from(document.querySelectorAll(".ai-diff-review-panel button")).find((item) => item.textContent?.includes("审查并批准")); if (!(button instanceof HTMLButtonElement)) throw new Error("AI diff review button not found"); button.click(); }); await chrome.waitForText(["批准 AI 配置差异", seededDiff.id, "取消"], "AI diff approval confirmation"); await chrome.evaluate(() => { const cancel = document.querySelector(".confirm-panel .confirm-actions button"); if (!(cancel instanceof HTMLButtonElement)) throw new Error("AI diff approval cancel button not found"); cancel.click(); }); const pendingResponse = await getJson(`/ai/config-diffs?serverInstanceId=${encodeURIComponent(seededDiff.serverInstanceId)}`, headers); const pending = findRequired(pendingResponse.items, (item) => item.id === seededDiff.id, "AI diff after approval cancel"); assertEqual(pending.state, "pending", "cancel keeps AI diff pending"); await chrome.evaluate(() => { const button = Array.from(document.querySelectorAll(".ai-diff-review-panel button")).find((item) => item.textContent?.includes("审查并批准")); if (!(button instanceof HTMLButtonElement)) throw new Error("AI diff review button not found after cancel"); button.click(); }); await chrome.waitForText(["批准 AI 配置差异", seededDiff.id], "AI diff approval confirmation reopen"); await chrome.evaluate(() => { const confirm = document.querySelector(".confirm-panel .confirm-primary"); if (!(confirm instanceof HTMLButtonElement)) throw new Error("AI diff approval submit button not found"); confirm.click(); }); await chrome.waitForText(["已审批", "写入任务"], "AI diff durable approval"); const approvedResponse = await getJson(`/ai/config-diffs?serverInstanceId=${encodeURIComponent(seededDiff.serverInstanceId)}`, headers); const approved = findRequired(approvedResponse.items, (item) => item.id === seededDiff.id, "approved AI config diff"); assertEqual(approved.state, "approved", "browser AI diff approval persisted"); if (!approved.jobId || !approved.approvedBy || !approved.approvedAt) { throw new Error(`approved AI diff missed durable approval linkage: ${JSON.stringify(approved)}`); } assertNoForbiddenProjection(approved, "approved AI diff response"); return { cancelPreservedState: pending.state, persisted: pick(approved, ["id", "serverInstanceId", "state", "approvedBy", "approvedAt", "jobId", "configVersion", "currentConfigChecksum"]), forbiddenFragmentScan: "passed", textSample: (await chrome.visibleText()).slice(0, 1200) }; } async function verifyFrontendEnvironment() { const [html, packageJsonText, viteConfigText] = await Promise.all([ fetchText(webUrl), readFile(new URL("../package.json", import.meta.url), "utf8"), readFile(new URL("../vite.config.ts", import.meta.url), "utf8") ]); const packageJson = JSON.parse(packageJsonText); const checks = [ { name: "serves app root", passed: html.includes('
') }, { name: "serves module app entry", passed: html.includes('type="module"') }, { name: "Vite proxy includes /api/v1", passed: viteConfigText.includes('"/api/v1"') }, { name: "Vite proxy includes /healthz", passed: viteConfigText.includes('"/healthz"') }, { name: "Vite env uses platform proxy", passed: viteConfigText.includes("PLATFORM_API_PROXY") }, { name: "package has browser acceptance script", passed: Boolean(packageJson.scripts?.["acceptance:browser"]) } ]; const failed = checks.filter((check) => !check.passed); if (failed.length > 0) { throw new Error(`frontend environment checks failed: ${failed.map((item) => item.name).join(", ")}`); } return { webUrl, checks }; } async function verifyLifecycleOperation(headers, server, chrome) { const currentServer = await getJson(`/server-instances/${encodeURIComponent(server.id)}`, headers); if (currentServer.configVersion <= server.configVersion) { throw new Error(`AI config write did not advance the durable server config version: before=${server.configVersion} after=${currentServer.configVersion}`); } const action = currentServer.state === "running" ? "stop" : "start"; const expectedCapability = action === "stop" ? "process.stop" : "process.start"; const request = { expectedConfigVersion: currentServer.configVersion, idempotencyKey: `browser-acceptance-${action}-${Date.now()}` }; const result = await postJson(`/server-instances/${encodeURIComponent(currentServer.id)}/${action}`, request, headers); if (!result.accepted || !result.job?.id) { throw new Error("lifecycle start operation did not return accepted job evidence"); } if (result.job.capability !== expectedCapability) { throw new Error(`lifecycle job used unexpected capability ${result.job.capability}`); } if (result.job.runEndpointId !== currentServer.runEndpointId) { throw new Error(`lifecycle job used unexpected run endpoint ${result.job.runEndpointId}`); } const job = await waitForJob(headers, currentServer.id, result.job.id); await chrome.navigate(`${webUrl}/#/servers/${encodeURIComponent(currentServer.id)}`); await chrome.waitForText([currentServer.name, "操作历史"], "server detail after lifecycle operation"); const historyState = await clickAndVerify(chrome, "操作历史", ["操作历史", "平台任务记录", "server-lifecycle", "process."]); return { action, accepted: result.accepted, request: { expectedConfigVersion: request.expectedConfigVersion, idempotencyKey: request.idempotencyKey, configVersionBeforeAIWrite: server.configVersion, configVersionAfterAIWrite: currentServer.configVersion }, acceptedJob: pick(result.job, ["id", "serverInstanceId", "runEndpointId", "capability", "state", "resultRef"]), job: pick(job, ["id", "serverInstanceId", "runEndpointId", "capability", "state", "resultRef"]), proof: `platform API accepted ${expectedCapability} and platform-owned jobs endpoint returned the same job; browser verified operation history entry point without direct run access`, browserEvidence: { ...historyState, proofMode: "operation-history-entry-point" } }; } async function waitForJob(headers, serverId, jobId) { for (let attempt = 0; attempt < 20; attempt += 1) { const jobs = await getJson(`/jobs?serverInstanceId=${encodeURIComponent(serverId)}`, headers); const job = jobs.items.find((item) => item.id === jobId); if (job) { return job; } await delay(250); } throw new Error(`platform jobs endpoint did not return lifecycle operation job ${jobId}`); } async function startChrome() { const chromePath = findChromePath(); const userDataDir = path.join(evidenceDir, "chrome-profile"); await rm(userDataDir, { recursive: true, force: true }); await mkdir(userDataDir, { recursive: true }); const chrome = spawn(chromePath, [ "--headless=new", "--disable-gpu", "--no-first-run", "--no-default-browser-check", "--disable-background-networking", `--user-data-dir=${userDataDir}`, "--remote-debugging-port=0", "about:blank" ], { stdio: ["ignore", "ignore", "pipe"] }); let stderr = ""; chrome.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); const activePortFile = path.join(userDataDir, "DevToolsActivePort"); let port = ""; for (let attempt = 0; attempt < 80; attempt += 1) { try { const activePort = await readFile(activePortFile, "utf8"); port = activePort.split(/\r?\n/)[0]?.trim(); if (port) { break; } } catch { // keep waiting for Chrome to write DevToolsActivePort } await delay(250); } if (!port) { chrome.kill("SIGTERM"); throw new Error(`Chrome did not expose a DevTools port. ${stderr.slice(0, 500)}`); } const targetResponse = await fetch(`http://127.0.0.1:${port}/json/new?${encodeURIComponent("about:blank")}`, { method: "PUT" }); if (!targetResponse.ok) { chrome.kill("SIGTERM"); throw new Error(`failed to create Chrome target: HTTP ${targetResponse.status}`); } const target = await targetResponse.json(); const client = await CdpClient.connect(target.webSocketDebuggerUrl); await client.send("Page.enable"); await client.send("Runtime.enable"); return { chromePath, async setViewport({ width, height, mobile }) { await client.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: mobile ? 2 : 1, mobile }); }, async navigate(url) { const loaded = client.waitFor("Page.loadEventFired", 15000); await client.send("Page.navigate", { url }); await loaded.catch(() => undefined); }, async evaluate(pageFunction, arg) { const expression = `(${pageFunction.toString()})(${arg === undefined ? "" : JSON.stringify(arg)})`; const result = await client.send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }); if (result.exceptionDetails) { throw new Error(result.exceptionDetails.text || "browser evaluation failed"); } return result.result?.value; }, async visibleText() { return this.evaluate(() => document.body?.innerText || ""); }, async layoutSnapshot() { return this.evaluate(() => { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const scrollWidth = document.documentElement.scrollWidth; const viewportClip = { left: 0, top: 0, right: viewportWidth, bottom: viewportHeight }; const rectFromDomRect = (rect) => ({ left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }); const intersection = (first, second) => ({ left: Math.max(first.left, second.left), top: Math.max(first.top, second.top), right: Math.min(first.right, second.right), bottom: Math.min(first.bottom, second.bottom) }); const clipForElement = (element) => { let clip = viewportClip; let parent = element.parentElement; while (parent) { const style = window.getComputedStyle(parent); const clipsX = ["auto", "scroll", "hidden", "clip"].includes(style.overflowX); const clipsY = ["auto", "scroll", "hidden", "clip"].includes(style.overflowY); if (clipsX || clipsY) { const parentRect = rectFromDomRect(parent.getBoundingClientRect()); clip = intersection(clip, { left: clipsX ? parentRect.left : clip.left, top: clipsY ? parentRect.top : clip.top, right: clipsX ? parentRect.right : clip.right, bottom: clipsY ? parentRect.bottom : clip.bottom }); } parent = parent.parentElement; } return clip; }; const controls = Array.from(document.querySelectorAll("button, a, input, select, textarea, [role='button'], [role='tab']")) .filter((element) => { const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none"; }) .map((element) => { const rect = element.getBoundingClientRect(); const clipped = intersection(rectFromDomRect(rect), clipForElement(element)); const visibleWidth = Math.max(0, clipped.right - clipped.left); const visibleHeight = Math.max(0, clipped.bottom - clipped.top); const floatingMenu = element.closest(".runtime-action-popover"); const floatingMenuRect = floatingMenu?.getBoundingClientRect(); return { tag: element.tagName.toLowerCase(), text: (element.textContent || element.getAttribute("aria-label") || "").trim().slice(0, 60), left: Math.round(clipped.left), top: Math.round(clipped.top), right: Math.round(clipped.right), bottom: Math.round(clipped.bottom), width: Math.round(visibleWidth), height: Math.round(visibleHeight), boundedFloatingMenu: Boolean(floatingMenuRect && floatingMenuRect.width <= 320 && floatingMenuRect.height <= 320) }; }) .filter((control) => control.width > 0 && control.height > 0); const overlappingControls = []; for (let index = 0; index < controls.length; index += 1) { for (let otherIndex = index + 1; otherIndex < controls.length; otherIndex += 1) { if (controls[index].boundedFloatingMenu !== controls[otherIndex].boundedFloatingMenu && (controls[index].boundedFloatingMenu || controls[otherIndex].boundedFloatingMenu)) { continue; } const left = Math.max(controls[index].left, controls[otherIndex].left); const top = Math.max(controls[index].top, controls[otherIndex].top); const right = Math.min(controls[index].right, controls[otherIndex].right); const bottom = Math.min(controls[index].bottom, controls[otherIndex].bottom); const width = right - left; const height = bottom - top; if (width > 0 && height > 0) { overlappingControls.push({ first: controls[index].text || controls[index].tag, second: controls[otherIndex].text || controls[otherIndex].tag, area: Math.round(width * height) }); } } } const visibleTinyTextBoxes = Array.from(document.querySelectorAll("button, a, label, h1, h2, h3, p, span, strong, td, th")) .filter((element) => { const text = (element.textContent || "").trim(); if (!text) { return false; } const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" && rect.width < 4; }).length; return { innerWidth: viewportWidth, innerHeight: viewportHeight, scrollWidth, horizontalOverflow: Math.max(0, scrollWidth - viewportWidth), controlCount: controls.length, overlappingControls: overlappingControls.slice(0, 10), visibleTinyTextBoxes }; }); }, async url() { return this.evaluate(() => window.location.href); }, async waitForText(markers, label) { const required = Array.isArray(markers) ? markers : [markers]; for (let attempt = 0; attempt < 80; attempt += 1) { const text = await this.visibleText(); if (required.every((marker) => text.includes(marker))) { return; } await delay(250); } const text = await this.visibleText(); const missing = required.filter((marker) => !text.includes(marker)); throw new Error(`${label} missing browser markers: ${missing.join(", ")}. Visible sample: ${text.slice(0, 500)}`); }, async close() { await client.close(); await stopChrome(chrome); await rm(userDataDir, { recursive: true, force: true }); } }; } async function stopChrome(chrome) { if (chrome.exitCode !== null || chrome.signalCode !== null) { return; } const exited = new Promise((resolve) => { chrome.once("exit", resolve); }); chrome.kill("SIGTERM"); const stopped = await Promise.race([ exited.then(() => true), delay(3000).then(() => false) ]); if (!stopped && chrome.exitCode === null && chrome.signalCode === null) { chrome.kill("SIGKILL"); await Promise.race([ exited, delay(3000) ]); } } class CdpClient { constructor(socket) { this.socket = socket; this.nextId = 1; this.pending = new Map(); this.waiters = new Map(); socket.addEventListener("message", (event) => this.handleMessage(event)); } static async connect(url) { const socket = new WebSocket(url); await new Promise((resolve, reject) => { socket.addEventListener("open", resolve, { once: true }); socket.addEventListener("error", reject, { once: true }); }); return new CdpClient(socket); } send(method, params = {}) { const id = this.nextId; this.nextId += 1; const promise = new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); }); this.socket.send(JSON.stringify({ id, method, params })); return promise; } waitFor(method, timeoutMs) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { const waiters = this.waiters.get(method) || []; this.waiters.set(method, waiters.filter((item) => item.resolve !== resolve)); reject(new Error(`timed out waiting for ${method}`)); }, timeoutMs); const waiters = this.waiters.get(method) || []; waiters.push({ resolve: (params) => { clearTimeout(timeout); resolve(params); } }); this.waiters.set(method, waiters); }); } handleMessage(event) { const message = JSON.parse(event.data); if (message.id) { const pending = this.pending.get(message.id); if (pending) { this.pending.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message)); } else { pending.resolve(message.result); } } return; } if (message.method && this.waiters.has(message.method)) { const waiters = this.waiters.get(message.method); const waiter = waiters.shift(); if (waiters.length === 0) { this.waiters.delete(message.method); } waiter?.resolve(message.params); } } async close() { this.socket.close(); } } async function getJson(pathname, headers = {}) { return requestJson("GET", pathname, undefined, headers); } async function postJson(pathname, body, headers = {}) { return requestJson("POST", pathname, body, headers); } async function requestJson(method, pathname, body, headers = {}) { const response = await fetch(`${apiUrl}${pathname}`, { method, headers: { Accept: "application/json", ...(body === undefined ? {} : { "Content-Type": "application/json" }), ...headers }, body: body === undefined ? undefined : JSON.stringify(body) }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`${method} ${pathname} failed with HTTP ${response.status}: ${text.slice(0, 300)}`); } return response.json(); } async function fetchText(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`GET ${url} failed with HTTP ${response.status}`); } return response.text(); } function findChromePath() { const candidates = [ process.env.CHROME_BIN, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "google-chrome", "chromium", "chromium-browser" ].filter(Boolean); return candidates.find((candidate) => { if (candidate.startsWith("/")) { return true; } return false; }) || candidates[0]; } function findRequired(items, predicate, label) { const item = items.find(predicate); if (!item) { throw new Error(`missing required ${label}`); } return item; } function assertMarkers(text, markers, routeName) { const missing = markers.filter((marker) => !text.includes(marker)); if (missing.length > 0) { throw new Error(`${routeName} missing required markers: ${missing.join(", ")}`); } } function scanText(text, routeName) { const fallback = fallbackFragments.find((item) => item.pattern.test(text)); if (fallback) { throw new Error(`${routeName} contains fallback/demo marker: ${fallback.name}`); } const forbidden = forbiddenFragments.find((item) => item.pattern.test(text)); if (forbidden) { throw new Error(`${routeName} contains forbidden visible fragment: ${forbidden.name}`); } } function assertNoForbiddenProjection(value, label) { const serialized = JSON.stringify(value); const forbidden = forbiddenFragments.find((item) => item.pattern.test(serialized)); if (forbidden) { throw new Error(`${label} contains forbidden API fragment: ${forbidden.name}`); } const forbiddenField = /"(?:apiKey|apiKeyRef|rawApiKey|baseUrl|hostPath|pid|socket|dsn|rcon|credential|directRunEndpoint)"\s*:/i.exec(serialized); if (forbiddenField) { throw new Error(`${label} contains forbidden API field ${forbiddenField[0]}`); } } function assertEqual(actual, expected, label) { if (actual !== expected) { throw new Error(`${label}: expected ${expected}, got ${actual}`); } } function assertIncludes(values, expected, label) { if (!Array.isArray(values) || !values.includes(expected)) { throw new Error(`${label}: missing ${expected}`); } } function pick(value, keys) { return Object.fromEntries(keys.map((key) => [key, value?.[key]])); } main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });