텍스트 응답 음성 재생 연결

This commit is contained in:
Yun Chan 2026-07-13 16:09:34 +09:00
parent 64e06a1185
commit d80e33da5e
9 changed files with 524 additions and 16 deletions

View file

@ -139,6 +139,25 @@ async function startFakeEngine(): Promise<TestServer> {
);
return;
}
if (req.method === "POST" && req.url === "/v1/stream") {
res.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
});
res.write(`event: token\ndata: ${JSON.stringify({ text: "괜찮아요. " })}\n\n`);
res.write(`event: token\ndata: ${JSON.stringify({ text: "천천히 말해볼게요." })}\n\n`);
res.end(
`event: done\ndata: ${JSON.stringify({
provider: "e2e",
model: "fake-client",
tokens_in: 1,
tokens_out: 1,
cost_usd: 0,
turns: 1,
})}\n\n`,
);
return;
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not found" }));
});
@ -836,7 +855,7 @@ test.describe("voice cascade success path", () => {
test("drives one voice turn through the Session mic UI with synthetic browser audio @single-run", async ({
page,
}, testInfo) => {
test.setTimeout(90_000);
test.setTimeout(150_000);
const diagnostics: string[] = [];
page.on("pageerror", (error) => diagnostics.push(`pageerror: ${error.message}`));
@ -848,7 +867,10 @@ test.describe("voice cascade success path", () => {
});
page.on("response", (response) => {
const url = response.url();
if (response.status() >= 400 && (url.includes("/sessions") || url.includes("/voice/ws"))) {
if (
response.status() >= 400 &&
(url.includes("/sessions") || url.includes("/voice/ws") || url.includes("/voice/speech"))
) {
diagnostics.push(`response: ${response.status()} ${url}`);
}
});
@ -964,6 +986,39 @@ test.describe("voice cascade success path", () => {
].join("\n\n"),
).toBeVisible({ timeout: 20_000 });
const textInput = page.getByLabel("학습자 발화 입력");
const sendButton = page.getByRole("button", { name: "보내기", exact: true });
await textInput.fill("텍스트로 말해도 내담자 음성을 들려주세요.");
await sendButton.click();
await expect
.poll(
() => openai.requests().filter((request) => request === "POST /v1/audio/speech").length,
{ timeout: 30_000 },
)
.toBeGreaterThan(0);
await expect
.poll(async () => (await readVoiceUiProbe(page)).audioBufferStarts, { timeout: 30_000 })
.toBeGreaterThan(0);
await expect(page.locator(".sx-utt").filter({ hasText: "괜찮아요. 천천히 말해볼게요." })).toBeVisible();
const freshVoiceSession = await page.evaluate(async ({ apiBase, personaCode }) => {
const response = await fetch(`${apiBase}/sessions`, {
method: "POST",
credentials: "include",
headers: { "content-type": "application/json" },
body: JSON.stringify({ persona_code: personaCode, theory_mode: "humanistic" }),
});
return {
ok: response.ok,
status: response.status,
body: await response.json() as { session_id?: string },
};
}, { apiBase: api.baseURL, personaCode: SEEDED_VOICE_PERSONA_CODE });
expect(freshVoiceSession, api.logs()).toMatchObject({ ok: true });
expect(typeof freshVoiceSession.body.session_id).toBe("string");
await page.goto(`${web.baseURL}/learn/session/${freshVoiceSession.body.session_id}`);
await expect(page.locator(".sx-page.sx-page--active")).toBeVisible({ timeout: 20_000 });
const mic = page.locator(".sx-mic");
await expect(mic).toBeEnabled();
await mic.click();