feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
+53
View File
@@ -7,6 +7,13 @@ import {
type EdgeTtsMiddlewareOptions,
type EdgeTtsSynthesizer
} from "./edge-tts-service";
import type { SpeechVoice } from "../src/shared/tts/voices";
const voices: SpeechVoice[] = [
{ id: "zh-CN-XiaoxiaoNeural", name: "Xiaoxiao", locale: "zh-CN", gender: "Female" },
{ id: "zh-CN-YunyangNeural", name: "Yunyang", locale: "zh-CN", gender: "Male" },
{ id: "zh-CN-XiaoyiNeural", name: "Xiaoyi", locale: "zh-CN", gender: "Female" }
];
const servers: ReturnType<typeof createServer>[] = [];
@@ -39,6 +46,52 @@ function postSpeech(baseUrl: string, body: unknown, headers = true) {
}
describe("Edge TTS adapter", () => {
it("falls back to Mandarin when a non-Mandarin default is configured", async () => {
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([1]));
const baseUrl = await startServer({ defaultVoice: "en-US-AriaNeural", synthesize });
expect((await postSpeech(baseUrl, { text: "测试" })).status).toBe(200);
expect(synthesize).toHaveBeenCalledWith("测试", "zh-CN-XiaoxiaoNeural");
});
it("returns all voices and the default, sharing the catalog with selection validation", async () => {
const listVoices = vi.fn(async () => voices);
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([1]));
const baseUrl = await startServer({ listVoices, synthesize, defaultVoice: voices[0].id });
const response = await fetch(`${baseUrl}/api/tts/edge/voices`);
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toBe("no-store");
expect(await response.json()).toEqual({ voices, defaultVoice: voices[0].id });
expect((await postSpeech(baseUrl, { text: "测试", voice: voices[1].id })).status).toBe(200);
expect(synthesize).toHaveBeenLastCalledWith("测试", voices[1].id);
expect((await postSpeech(baseUrl, { text: "test", voice: voices[2].id })).status).toBe(200);
expect(synthesize).toHaveBeenLastCalledWith("test", voices[2].id);
expect(listVoices).toHaveBeenCalledOnce();
});
it("rejects unknown and malformed voice choices before synthesis", async () => {
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([1]));
const baseUrl = await startServer({ listVoices: async () => voices, synthesize });
for (const voice of ["zh-CN-UnknownNeural", "en-US-AriaNeural", "zh-TW-HsiaoChenNeural", "zh-CN-liaoning-XiaobeiNeural", "<voice name='bad'>", null, 12, {}]) {
expect((await postSpeech(baseUrl, { text: "测试", voice })).status).toBe(400);
}
expect(synthesize).not.toHaveBeenCalled();
});
it("can use the default when the catalog is unavailable and retry a failed catalog load", async () => {
const listVoices = vi.fn().mockRejectedValueOnce(new Error("offline")).mockResolvedValue(voices);
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([1]));
const baseUrl = await startServer({ listVoices, synthesize });
expect((await fetch(`${baseUrl}/api/tts/edge/voices`)).status).toBe(503);
expect((await postSpeech(baseUrl, { text: "默认" })).status).toBe(200);
expect((await fetch(`${baseUrl}/api/tts/edge/voices`)).status).toBe(200);
expect(listVoices).toHaveBeenCalledTimes(2);
});
it("refuses a chosen voice when its availability cannot be verified", async () => {
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([1]));
const baseUrl = await startServer({ listVoices: async () => { throw new Error("offline"); }, synthesize });
expect((await postSpeech(baseUrl, { text: "测试", voice: voices[1].id })).status).toBe(503);
expect(synthesize).not.toHaveBeenCalled();
});
it("returns synthesized MP3 audio with the server-owned voice", async () => {
const synthesize = vi.fn<EdgeTtsSynthesizer>(async () => Uint8Array.from([73, 68, 51]));
const baseUrl = await startServer({ synthesize, defaultVoice: "zh-CN-XiaoxiaoNeural" });