import { test, expect } from "@playwright/test"; import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { createHash } from "node:crypto"; import { remoteAudio, remoteAudioCapabilities, } from "@/lib/video-canvas/smart-edit/remote-audio"; import { ProviderTaskUncertainError } from "@/lib/video-canvas/video-task-lifecycle"; test.describe.configure({ mode: "serial" }); const originalFetch = global.fetch, oldUrl = process.env.SMART_EDIT_AUDIO_URL, oldToken = process.env.SMART_EDIT_AUDIO_TOKEN; test.beforeEach(() => { process.env.SMART_EDIT_AUDIO_URL = "http://127.0.0.1:19342"; process.env.SMART_EDIT_AUDIO_TOKEN = "test-".repeat(8); }); test.afterEach(() => { global.fetch = originalFetch; if (oldUrl === undefined) delete process.env.SMART_EDIT_AUDIO_URL; else process.env.SMART_EDIT_AUDIO_URL = oldUrl; if (oldToken === undefined) delete process.env.SMART_EDIT_AUDIO_TOKEN; else process.env.SMART_EDIT_AUDIO_TOKEN = oldToken; }); test("audio uploads once, resumes same identity, and isolates tenant/task/content", async () => { const dir = await mkdtemp(path.join(tmpdir(), "audio-identity-")), file = path.join(dir, "input.wav"); await writeFile(file, "fixture bytes"); const ids: string[] = [], jobs = new Set(); let uploads = 0; let expected = "fixture bytes"; global.fetch = (async (_url: any, init: any) => { if (init.method === "POST") { const input = JSON.parse(init.body); ids.push(input.id); return Response.json({ id: input.id, status: jobs.has(input.id) ? "succeeded" : "awaiting-input", stage: "ready", ...(jobs.has(input.id) ? { result: { durationSeconds: 1, subtitles: [{ start: 0, end: 800000, text: "测试" }], }, } : {}), }); } const id = String(_url).split("/").at(-2)!; let data = ""; for await (const c of init.body) data += c; expect(data).toBe(expected); uploads++; jobs.add(id); return Response.json({ id, status: "succeeded", stage: "done", result: { durationSeconds: 1, subtitles: [{ start: 0, end: 800000, text: "测试" }], }, }); }) as any; try { await remoteAudio("transcribe", file, "owner", "task", dir); await remoteAudio("transcribe", file, "owner", "task", dir); expect(uploads).toBe(1); expect(ids[0]).toBe(ids[1]); await remoteAudio("transcribe", file, "other", "task", dir); await remoteAudio("transcribe", file, "owner", "other-task", dir); expect(new Set(ids).size).toBe(3); expected = "updated fixture"; await writeFile(file, expected); await remoteAudio("transcribe", file, "owner", "task", dir); expect(new Set(ids).size).toBe(4); expect(uploads).toBe(4); } finally { await rm(dir, { recursive: true, force: true }); } }); test("unavailable remote audio remains recoverable and does not advertise local fallback", async () => { const dir = await mkdtemp(path.join(tmpdir(), "audio-offline-")), file = path.join(dir, "input.wav"); await writeFile(file, "audio"); global.fetch = (async () => { throw Error("offline"); }) as any; try { expect(await remoteAudioCapabilities()).toEqual({ asr: false, separation: false, }); await expect( remoteAudio("transcribe", file, "u", "t", dir) ).rejects.toBeInstanceOf(ProviderTaskUncertainError); } finally { await rm(dir, { recursive: true, force: true }); } }); test("corrupt separated download never replaces saved stem", async () => { const dir = await mkdtemp(path.join(tmpdir(), "audio-integrity-")), file = path.join(dir, "input.wav"); await writeFile(file, "audio"); await writeFile(path.join(dir, "vocals.wav"), "previous"); const bytes = Buffer.from("expected"), sha256 = createHash("sha256").update(bytes).digest("hex"); global.fetch = (async (_url: any, init: any) => init.method === "POST" ? Response.json({ id: JSON.parse(init.body).id, status: "succeeded", stage: "done", result: { durationSeconds: 1, stems: { vocals: { bytes: bytes.length, sha256 }, background: { bytes: bytes.length, sha256 }, }, }, }) : new Response("corrupt!")) as any; try { await expect( remoteAudio("separate", file, "u", "t", dir) ).rejects.toBeInstanceOf(ProviderTaskUncertainError); expect(await readFile(path.join(dir, "vocals.wav"), "utf8")).toBe( "previous" ); } finally { await rm(dir, { recursive: true, force: true }); } });