Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/development/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,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.
45 changes: 35 additions & 10 deletions tests/browser/voice_library.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
const readTimeout = 15000;
async function call(path, body, origin = api) {
const response = await fetch(origin + path, {
Expand All @@ -34,25 +45,26 @@ async function reopenAudio(page, assetPath, savedLabel) {
let receive, timer;
const received = new Promise((resolve) => { receive = resolve; });
const observe = (response) => {
if (new URL(response.url()).pathname !== assetPath) return;
if (new URL(response.url()).pathname !== assetPath ||
response.request().method() !== "GET") return;
statuses.push(response.status());
if (response.ok()) receive(response);
receive(response);
};
page.on("response", observe);
try {
// Observe early responses, but give navigation its own deadline. A cold
// document has not started the project's media read yet.
await page.reload({ timeout: 30000 });
await page.getByText(savedLabel, { exact: true }).waitFor();
await page.reload({ waitUntil: "domcontentloaded", timeout: reopenTimeout });
await waitForSaved(page, savedLabel);
console.log(JSON.stringify({
event: "voice-reopen-ready", assetPath, elapsedMs: Date.now() - started,
}));
return await Promise.race([
received,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(
`No successful reopened audio response; statuses=${JSON.stringify(statuses)}`,
)), readTimeout);
`No reopened audio response; statuses=${JSON.stringify(statuses)}`,
)), reopenTimeout);
}),
]);
} finally {
Expand Down Expand Up @@ -265,8 +277,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 })
Expand Down Expand Up @@ -497,8 +512,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
Expand Down Expand Up @@ -827,7 +842,17 @@ 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 reopenAudio(page, assetPath, L.saved);
assert.ok(
reopenedAudio.ok(),
`Reopened audio GET ${assetPath}: HTTP ${reopenedAudio.status()}`,
);
assert.match(reopenedAudio.headers()["content-type"], /^audio\//);
await page.locator(".react-flow__node").first().click();
assert.ok(
Expand Down
Loading