Files
story/test/e2e/visual-ops-config-evaluation.mjs
T

172 lines
15 KiB
JavaScript
Raw Normal View History

2026-08-26 06:41:07 +08:00
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();
}