import { createRequire } from "node:module"; import { randomUUID } from "node:crypto"; import { execFileSync } from "node:child_process"; import { mkdir } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url); const { chromium } = require("playwright"); const outputDirectory = new URL("./screenshots/", import.meta.url); const webURL = process.env.VISUAL_WEB_URL ?? "http://localhost:3100"; const webHostPattern = new RegExp(new URL(webURL).host.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); const oidcURLPattern = new RegExp(process.env.VISUAL_OIDC_URL_PATTERN ?? "localhost:58180"); const username = process.env.VISUAL_USERNAME ?? "owner"; const password = process.env.VISUAL_PASSWORD ?? "LocalOnly123!"; const configID = randomUUID(); const configPublicID = randomUUID(); const configKey = `visual-evaluation-${configPublicID}`.slice(0, 90); const activeConfigID = randomUUID(); const activeConfigPublicID = randomUUID(); const activeConfigKey = `visual-active-${activeConfigPublicID}`.slice(0, 90); const pausedConfigID = randomUUID(); const pausedConfigPublicID = randomUUID(); const pausedConfigKey = `visual-paused-${pausedConfigPublicID}`.slice(0, 90); const activeRolloutID = randomUUID(); const activeRolloutPublicID = randomUUID(); const pausedRolloutID = randomUUID(); const pausedRolloutPublicID = randomUUID(); const resourceID = randomUUID(); const resourcePublicID = randomUUID(); const resourceReference = `kms://visual/${resourcePublicID}`; const resourceDisplayName = `页面资源撤销夹具-${resourcePublicID.slice(0, 8)}`; await mkdir(outputDirectory, { recursive: true }); function psql(statement) { return execFileSync("docker", ["compose", "exec", "-T", "postgres", "psql", "-v", "ON_ERROR_STOP=1", "-U", "voice", "-d", "voice_platform", "-Atc", statement], { cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); } function cleanup() { psql(`BEGIN; DELETE FROM ops_config_evaluation WHERE config_version_id='${configID}'::uuid; DELETE FROM runtime_config_rollout WHERE id IN ('${activeRolloutID}'::uuid,'${pausedRolloutID}'::uuid); DELETE FROM ops_resource_reference WHERE id='${resourceID}'::uuid; DELETE FROM runtime_config_version WHERE id IN ('${configID}'::uuid,'${activeConfigID}'::uuid,'${pausedConfigID}'::uuid); COMMIT;`); } psql(`BEGIN; INSERT INTO runtime_config_version(id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,secret_refs,change_summary,validation_result,created_by,row_version) VALUES('${configID}'::uuid,'${configPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${configKey}','页面评测登记夹具','ROUTING',1,'DRAFT','{"default_engine_mode":"s2s_hybrid","routes":[{"goal":"open_conversation","engine_mode":"s2s_hybrid"}]}'::jsonb,'{}'::jsonb,'验证页面内通用离线评测登记门禁','{}'::jsonb,'10000000-0000-0000-0000-000000000101'::uuid,4); INSERT INTO runtime_config_version(id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,secret_refs,change_summary,validation_result,created_by,submitted_by,submitted_at,approved_by,approved_at,published_by,published_at,row_version) VALUES ('${activeConfigID}'::uuid,'${activeConfigPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${activeConfigKey}','页面暂停投放夹具','ROUTING',1,'PUBLISHED','{"default_engine_mode":"s2s_hybrid"}'::jsonb,'{}'::jsonb,'验证页面内暂停投放门禁','{}'::jsonb,'10000000-0000-0000-0000-000000000101'::uuid,'10000000-0000-0000-0000-000000000101'::uuid,now()-interval '3 hours','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '2 hours','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '2 hours',5), ('${pausedConfigID}'::uuid,'${pausedConfigPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${pausedConfigKey}','页面恢复投放夹具','ROUTING',1,'PUBLISHED','{"default_engine_mode":"s2s_hybrid"}'::jsonb,'{}'::jsonb,'验证页面内恢复投放门禁','{}'::jsonb,'10000000-0000-0000-0000-000000000101'::uuid,'10000000-0000-0000-0000-000000000101'::uuid,now()-interval '3 hours','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '2 hours','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '2 hours',6); INSERT INTO runtime_config_rollout(id,public_id,tenant_id,config_version_id,config_key,scope_type,rollout_percent,max_concurrent_calls,status,reason,activated_by,activated_at,paused_at,row_version) VALUES ('${activeRolloutID}'::uuid,'${activeRolloutPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${activeConfigID}'::uuid,'${activeConfigKey}','TENANT',35,20,'ACTIVE','页面暂停投放无提交验证','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '1 hour',NULL,7), ('${pausedRolloutID}'::uuid,'${pausedRolloutPublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${pausedConfigID}'::uuid,'${pausedConfigKey}','TENANT',60,40,'PAUSED','页面恢复投放无提交验证','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '2 hours',now()-interval '30 minutes',8); INSERT INTO ops_resource_reference(id,public_id,tenant_id,resource_type,reference_name,display_name,status,verified_at,created_by,row_version) VALUES('${resourceID}'::uuid,'${resourcePublicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'PROVIDER_CREDENTIAL','${resourceReference}','${resourceDisplayName}','ACTIVE',now(),'10000000-0000-0000-0000-000000000101'::uuid,9); COMMIT;`); const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? "C:/Program Files/Google/Chrome/Application/chrome.exe" }); const page = await browser.newPage({ viewport: { width: 1440, height: 1100 }, deviceScaleFactor: 1 }); const consoleErrors = []; const failedRequests = []; const evaluationMutations = []; const configControlMutations = []; page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); page.on("request", (request) => { if (request.method() === "POST" && /\/api\/v1\/ops\/config-versions\/[^/]+\/evaluations$/.test(request.url())) evaluationMutations.push(`${request.method()} ${request.url()}`); if (request.method() === "PATCH" && /\/api\/v1\/ops\/(resource-references\/[^/]+\/revoke|config-rollouts\/[^/]+\/(pause|resume))$/.test(request.url())) configControlMutations.push(`${request.method()} ${request.url()}`); }); page.on("requestfailed", (request) => { const failure = request.failure()?.errorText ?? "failed"; const requestURL = new URL(request.url()); if (request.method() === "GET" && requestURL.pathname === "/api/v1/events" && failure.includes("ERR_ABORTED")) return; failedRequests.push(`${request.method()} ${request.url()} ${failure}`); }); try { await page.goto(`${webURL}/?workspace=config`, { waitUntil: "networkidle" }); await page.getByRole("button", { name: "登录研发环境" }).click(); await page.waitForURL(oidcURLPattern); await page.locator("#username").fill(username); await page.locator("#password").fill(password); await page.locator("#kc-login").click(); await page.waitForURL(webHostPattern, { timeout: 15000 }); await page.getByRole("heading", { name: "配置发布与版本控制" }).waitFor(); const versionGroup = page.locator(".ops-version-group").filter({ hasText: "页面评测登记夹具" }); await versionGroup.waitFor(); const versionCard = versionGroup.locator(".ops-version").first(); await versionCard.getByRole("button", { name: "登记通用离线评测" }).click(); const form = versionCard.getByRole("form", { name: "通用离线评测登记" }); await form.waitFor(); const submit = form.getByRole("button", { name: "登记评测证据" }); const targetBound = (await form.textContent())?.includes(configPublicID) === true && (await form.textContent())?.includes("行版本 v4") === true && (await form.textContent())?.includes(configKey) === true; const emptyFieldsDisabled = await submit.isDisabled(); await form.getByLabel("评测套件版本").fill("voice-routing-regression-v3"); await form.getByLabel("评测报告引用").fill("report://visual/evaluation/no-submit"); const missingResultDisabled = await submit.isDisabled(); await form.getByLabel("通过").check(); const passedNeedsAcknowledgement = await submit.isDisabled(); const acknowledgement = form.getByLabel("我确认报告内容和指标支持“通过”结论,并理解该记录可能参与当前配置的发布门禁"); await acknowledgement.check(); const passedEnabledAfterAcknowledgement = await submit.isEnabled(); await form.getByLabel("失败").check(); const failedExplicitlyEnabled = await submit.isEnabled(); const acknowledgementRemovedForFailure = await acknowledgement.count() === 0; await page.screenshot({ path: fileURLToPath(new URL("ops-config-evaluation-form.png", outputDirectory)), fullPage: true }); await form.getByRole("button", { name: "取消" }).click(); const cancelClosedForm = await form.count() === 0; const resourceRow = page.locator(".ops-ledger").filter({ hasText: "资源引用状态" }).locator("li").filter({ hasText: resourceDisplayName }); await resourceRow.getByRole("button", { name: "撤销" }).click(); let controlForm = page.getByRole("form", { name: "运营配置受控操作" }); await controlForm.waitFor(); const resourceTargetBound = (await controlForm.textContent())?.includes(resourcePublicID) === true && (await controlForm.textContent())?.includes(resourceReference) === true && (await controlForm.textContent())?.includes("行版本 v9") === true; let controlSubmit = controlForm.getByRole("button", { name: "确认撤销引用" }); const resourceEmptyDisabled = await controlSubmit.isDisabled(); await controlForm.getByLabel("撤销原因").fill("供应商凭据轮换完成,后续发布改用替代引用"); const resourceNeedsAcknowledgement = await controlSubmit.isDisabled(); await controlForm.getByLabel(/我确认后续依赖该引用的配置发布将被阻断/).check(); const resourceEnabledAfterAcknowledgement = await controlSubmit.isEnabled(); await controlForm.getByRole("button", { name: "取消" }).click(); const resourceCancelClosed = await controlForm.count() === 0; const activeRolloutRow = page.locator(".ops-ledger").filter({ hasText: "配置投放控制" }).locator("li").filter({ hasText: activeConfigKey }); await activeRolloutRow.getByRole("button", { name: "暂停" }).click(); controlForm = page.getByRole("form", { name: "运营配置受控操作" }); await controlForm.waitFor(); const pauseTargetBound = (await controlForm.textContent())?.includes(activeRolloutPublicID) === true && (await controlForm.textContent())?.includes(activeConfigKey) === true && (await controlForm.textContent())?.includes("行版本 v7") === true; controlSubmit = controlForm.getByRole("button", { name: "确认暂停投放" }); const pauseEmptyDisabled = await controlSubmit.isDisabled(); await controlForm.getByLabel("暂停原因和观察依据").fill("新通话质量出现异常,暂停后观察供应商恢复状态"); const pauseNeedsAcknowledgement = await controlSubmit.isDisabled(); await controlForm.getByLabel(/我确认该投放将不再被后续新建通话选中/).check(); const pauseEnabledAfterAcknowledgement = await controlSubmit.isEnabled(); await controlForm.getByRole("button", { name: "取消" }).click(); const pauseCancelClosed = await controlForm.count() === 0; const pausedRolloutRow = page.locator(".ops-ledger").filter({ hasText: "配置投放控制" }).locator("li").filter({ hasText: pausedConfigKey }); await pausedRolloutRow.getByRole("button", { name: "恢复" }).click(); controlForm = page.getByRole("form", { name: "运营配置受控操作" }); await controlForm.waitFor(); const resumeTargetBound = (await controlForm.textContent())?.includes(pausedRolloutPublicID) === true && (await controlForm.textContent())?.includes(pausedConfigKey) === true && (await controlForm.textContent())?.includes("行版本 v8") === true; controlSubmit = controlForm.getByRole("button", { name: "确认恢复投放" }); const resumeEmptyDisabled = await controlSubmit.isDisabled(); await controlForm.getByLabel("恢复原因和确认依据").fill("供应商恢复验证通过,已准备观察和快速回退方案"); const resumeNeedsAcknowledgement = await controlSubmit.isDisabled(); await controlForm.getByLabel(/我确认目标配置仍可发布、同范围没有其他活动投放/).check(); const resumeEnabledAfterAcknowledgement = await controlSubmit.isEnabled(); await page.screenshot({ path: fileURLToPath(new URL("ops-config-controlled-actions.png", outputDirectory)), fullPage: true }); await controlForm.getByRole("button", { name: "取消" }).click(); const resumeCancelClosed = await controlForm.count() === 0; const overflow = await page.evaluate(() => [...document.querySelectorAll("body *")] .filter((element) => element instanceof HTMLElement && element.scrollWidth > element.clientWidth + 2 && getComputedStyle(element).overflowX === "visible") .slice(0, 20) .map((element) => `${element.tagName}.${element.className}: ${element.clientWidth}/${element.scrollWidth}`)); const result = { targetBound, emptyFieldsDisabled, missingResultDisabled, passedNeedsAcknowledgement, passedEnabledAfterAcknowledgement, failedExplicitlyEnabled, acknowledgementRemovedForFailure, cancelClosedForm, resourceTargetBound, resourceEmptyDisabled, resourceNeedsAcknowledgement, resourceEnabledAfterAcknowledgement, resourceCancelClosed, pauseTargetBound, pauseEmptyDisabled, pauseNeedsAcknowledgement, pauseEnabledAfterAcknowledgement, pauseCancelClosed, resumeTargetBound, resumeEmptyDisabled, resumeNeedsAcknowledgement, resumeEnabledAfterAcknowledgement, resumeCancelClosed, evaluationMutations, configControlMutations, overflow, consoleErrors, failedRequests }; console.log(JSON.stringify(result, null, 2)); if (!targetBound || !emptyFieldsDisabled || !missingResultDisabled || !passedNeedsAcknowledgement || !passedEnabledAfterAcknowledgement || !failedExplicitlyEnabled || !acknowledgementRemovedForFailure || !cancelClosedForm || !resourceTargetBound || !resourceEmptyDisabled || !resourceNeedsAcknowledgement || !resourceEnabledAfterAcknowledgement || !resourceCancelClosed || !pauseTargetBound || !pauseEmptyDisabled || !pauseNeedsAcknowledgement || !pauseEnabledAfterAcknowledgement || !pauseCancelClosed || !resumeTargetBound || !resumeEmptyDisabled || !resumeNeedsAcknowledgement || !resumeEnabledAfterAcknowledgement || !resumeCancelClosed || evaluationMutations.length || configControlMutations.length || overflow.length || consoleErrors.length || failedRequests.length) { throw new Error("Ops config evaluation visual smoke failed"); } } finally { await browser.close(); cleanup(); }