const path = require("path"); const fs = require("fs"); const { execFileSync } = require("child_process"); const { randomUUID } = require("crypto"); const dependencyRoot = process.env.PLAYWRIGHT_REQUIRE_FROM; if (!dependencyRoot) throw new Error("PLAYWRIGHT_REQUIRE_FROM is required"); const { chromium } = require(path.join(dependencyRoot, "playwright")); const chromePath = process.env.CHROME_PATH || "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"; const outputDir = path.resolve(process.env.UI_SCREENSHOT_DIR || "test/e2e/screenshots/operations"); const marker = `sse_${randomUUID().replaceAll("-", "").slice(0, 16)}`; fs.mkdirSync(outputDir, { recursive: true }); function psql(sql) { return execFileSync("docker", ["compose", "exec", "-T", "postgres", "psql", "-U", "voice", "-d", "voice_platform", "-v", "ON_ERROR_STOP=1", "-q", "-c", sql], { cwd: path.resolve(__dirname, ".."), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); } function insertEvent() { psql(`INSERT INTO web_event(tenant_id,household_id,subject_id,event_type,resource_type,resource_public_id,status) VALUES('10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000201','10000000-0000-0000-0000-000000000401', 'device.presence.changed','device',gen_random_uuid(),'${marker}')`); } (async () => { const browser = await chromium.launch({ executablePath: chromePath, headless: true }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); const failures = []; const routeCounts = new Map(); let countResponses = false; page.on("pageerror", (error) => failures.push(`pageerror: ${error.message}`)); page.on("console", (message) => { if (message.type() === "error") failures.push(`console: ${message.text()}`); }); page.on("response", (response) => { if (!countResponses) return; const url = new URL(response.url()); if (!url.pathname.startsWith("/api/v1/") || url.pathname === "/api/v1/events") return; routeCounts.set(url.pathname, (routeCounts.get(url.pathname) || 0) + 1); if (response.status() === 429) failures.push(`rate limited: ${url.pathname}`); else if (response.status() >= 400) failures.push(`http ${response.status()}: ${url.pathname}`); }); await page.goto("http://localhost:3100", { waitUntil: "networkidle" }); const login = page.getByRole("button", { name: "登录研发环境" }); if (await login.isVisible()) { await login.click(); await page.locator("#username").fill("owner"); await page.locator("#password").fill("LocalOnly123!"); await page.locator("#kc-login").click(); } await page.waitForURL("http://localhost:3100/**", { timeout: 15000 }); await page.getByRole("heading", { name: "家庭业务工作台" }).waitFor({ state: "visible", timeout: 15000 }); await page.getByText("状态实时更新", { exact: true }).waitFor({ state: "visible", timeout: 15000 }); // Let the login/navigation burst leave the local user token bucket before // measuring only the refreshes caused by a sustained event stream. await page.waitForTimeout(6000); countResponses = true; for (let index = 0; index < 8; index += 1) { insertEvent(); await page.waitForTimeout(650); } await page.waitForTimeout(2500); countResponses = false; const factRefreshes = [...routeCounts.entries()].filter(([route]) => route.endsWith("/facts")).reduce((sum, [, count]) => sum + count, 0); const totalBusinessResponses = [...routeCounts.values()].reduce((sum, count) => sum + count, 0); if (factRefreshes < 2) failures.push(`event refresh did not run enough times to verify coalescing: ${factRefreshes}`); // The first event may refresh immediately; subsequent refreshes are capped // at one per two seconds. The measured window includes eight Docker/psql // inserts and therefore permits at most five batches. if (factRefreshes > 5) failures.push(`event refresh was not coalesced: facts loaded ${factRefreshes} times`); if (totalBusinessResponses > 90) failures.push(`event stream amplified into ${totalBusinessResponses} business requests`); const eventStatus = page.locator(".event-stream-status"); const title = await eventStatus.getAttribute("title"); if (!title || title === "等待状态事件") failures.push("the browser did not observe the injected events"); await page.screenshot({ path: path.join(outputDir, "sse-refresh-coalescing.png"), fullPage: true }); if (failures.length) throw new Error(failures.join("\n")); process.stdout.write(`SSE refresh coalescing passed: 8 events -> ${factRefreshes} refresh batches, ${totalBusinessResponses} business responses.\n`); } finally { try { psql(`DELETE FROM web_event WHERE status='${marker}'`); } catch (error) { process.stderr.write(`cleanup failed: ${error.message}\n`); } await browser.close(); } })().catch((error) => { process.stderr.write(`${error.stack || error}\n`); process.exitCode = 1; });