From acdfd3c3f3d93f712ce3e2236843c35c25ce6d3c Mon Sep 17 00:00:00 2001 From: Tiance Wang <11400530+wangtiance@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:54:52 +0800 Subject: [PATCH] [test] ci: bound voice-library cold reopen waits Use explicit 45-second cold-load and reopen waits while retaining the 15-second interaction timeout. Surface failed media HTTP responses directly and provide a deterministic slow-response probe. Refs #250. --- docs/development/validation.md | 4 ++++ tests/browser/voice_library.mjs | 40 +++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/development/validation.md b/docs/development/validation.md index 3d201a5e..3c9b51da 100644 --- a/docs/development/validation.md +++ b/docs/development/validation.md @@ -232,4 +232,8 @@ OPENDIRECTOR_API_URL=http://127.0.0.1:8030 npm run dev -- --port 5190 CREATION_API_URL=http://127.0.0.1:8030 CREATION_WEB_URL=http://127.0.0.1:5190 node tests/browser/voice_library.mjs ``` +For a slow-response regression, set `VOICE_REOPEN_DELAY_MS=17000` for the browser command. It delays the reopened asset request beyond the ordinary 15-second interaction timeout. Cold navigation, saved-state and reopened-media waits have explicit 45-second limits; HTTP errors still fail immediately with their status. No generation is retried. + +慢响应回归:在浏览器命令的环境变量中设置 `VOICE_REOPEN_DELAY_MS=17000`,让重开后的资源请求超过普通交互的 15 秒等待。冷启动导航、保存状态和重开媒体分别有明确的 45 秒上限;HTTP 错误仍立即报告状态并失败,不重试生成。 + The bilingual browser workflow covers opening/outside click/Escape, keyboard tabs, four-column and narrow layouts, real previews and unavailable previews, metadata filters, scoped favorites, loading/error/retry, invalid and valid local audio, creation, selection, Drama3 transcript and Nano reference requirements, supported deletion and failure feedback, model/service switching, project reopening and persisted job input. `moss_speech.mjs` is retained as an alias for this integrated workflow. The pagination regression includes a 15,001-voice catalog. With a configured MOSS account, open the library, go to the next page, and return to the first. Verify the page counts and that returning to the first page restores its entries using read-only requests. Python tests verify provider fields, pagination, credentials, account boundaries, actual audio/container/duration validation, and equivalent Agent operations. Actual provider synthesis still requires a configured service, account permission and a recording authorized for use. diff --git a/tests/browser/voice_library.mjs b/tests/browser/voice_library.mjs index bcf79925..cb216666 100644 --- a/tests/browser/voice_library.mjs +++ b/tests/browser/voice_library.mjs @@ -11,6 +11,17 @@ const headers = { "X-OpenDirector-Request": "1", "Content-Type": "application/json", }; +// Reopening includes cold module loading, project recovery and asset fetching. +// Keep ordinary interaction waits short, but budget this whole path explicitly. +const reopenTimeout = 45000; +// Optional deterministic slow-CI probe for the reopened asset request. +const reopenDelay = Number(process.env.VOICE_REOPEN_DELAY_MS ?? 0); +assert.ok( + Number.isFinite(reopenDelay) && reopenDelay >= 0 && reopenDelay < reopenTimeout, +); +async function waitForSaved(page, label) { + await page.getByText(label, { exact: true }).waitFor({ timeout: reopenTimeout }); +} async function call(path, body, origin = api) { const response = await fetch(origin + path, { headers, @@ -210,8 +221,11 @@ try { ); }; try { - await page.goto(`${base}/ai-tool/ai-canvas/${id}?creation=unified`); - await page.getByText(L.saved, { exact: true }).waitFor(); + await page.goto(`${base}/ai-tool/ai-canvas/${id}?creation=unified`, { + waitUntil: "domcontentloaded", + timeout: reopenTimeout, + }); + await waitForSaved(page, L.saved); await page .getByRole("toolbar", { name: L.tools }) .getByRole("button", { name: L.add, exact: true }) @@ -442,8 +456,8 @@ try { .voiceId, "sample", ); - await page.reload(); - await page.getByText(L.saved, { exact: true }).waitFor(); + await page.reload({ waitUntil: "domcontentloaded", timeout: reopenTimeout }); + await waitForSaved(page, L.saved); await page.locator(".react-flow__node").first().click(); assert.match( await page @@ -772,15 +786,27 @@ try { }); const assetPath = `/api/creation/projects/${id}/assets/${asset.id}/content`; + if (reopenDelay) { + await page.route((url) => url.pathname === assetPath, async (route) => { + await new Promise((resolve) => setTimeout(resolve, reopenDelay)); + await route.continue(); + }); + } const [reopenedAudio] = await Promise.all([ page.waitForResponse( (response) => - new URL(response.url()).pathname === assetPath && response.ok(), + new URL(response.url()).pathname === assetPath && + response.request().method() === "GET", + { timeout: reopenTimeout }, ), - page.reload(), + page.reload({ waitUntil: "domcontentloaded", timeout: reopenTimeout }), ]); + assert.ok( + reopenedAudio.ok(), + `Reopened audio GET ${assetPath}: HTTP ${reopenedAudio.status()}`, + ); assert.match(reopenedAudio.headers()["content-type"], /^audio\//); - await page.getByText(L.saved, { exact: true }).waitFor(); + await waitForSaved(page, L.saved); await page.locator(".react-flow__node").first().click(); assert.ok( (