172 lines
15 KiB
JavaScript
172 lines
15 KiB
JavaScript
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();
|
||
|
|
}
|