회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s

This commit is contained in:
Yun Chan 2026-09-08 23:28:06 +09:00
parent a479db7a5a
commit a0311c5957
100 changed files with 4884 additions and 11210 deletions

View file

@ -1,4 +1,3 @@
import path from "node:path";
import { expect, test, type Page } from "@playwright/test";
import { parseVoicePracticeContext } from "../src/lib/voicePracticeContext";
import { expectNoHorizontalOverflow } from "./support";
@ -424,6 +423,7 @@ async function routeMvpApi(page: Page, options: RouteMvpOptions = {}) {
growthPoints: [{ title: "탐색 확장", body: "다음 턴에서 구체 상황을 더 묻습니다." }],
nextLine: "그 말을 꺼내는 것도 쉽지 않았을 것 같아요.",
clientFeedback: clientReply,
learnerFeedbackEnabled: true,
audioUrl: null,
pdfExportUrl: null,
degraded: false,
@ -773,7 +773,6 @@ async function installVoiceLifecycleFixture(page: Page) {
releaseEmptyFinal() {
const socket = sockets[2];
if (!socket || socket.readyState !== FixtureWebSocket.OPEN) return;
socket.emitJson({ type: "state", state: "thinking" });
socket.emitJson({ type: "transcript", text: "", final: true });
socket.emitJson({ type: "state", state: "idle" });
},
@ -877,11 +876,12 @@ async function installVoiceLifecycleFixture(page: Page) {
}
if (payload.type !== "audio_end") return;
window.setTimeout(() => {
if (this.connectionNumber === 1) {
this.emitJson({ type: "transcript", text: firstInterim, final: false });
} else if (this.connectionNumber === 2) {
this.emitJson({ type: "transcript", text: secondInterim, final: false });
}
if (this.connectionNumber > 2) return;
this.emitJson({
type: "transcript",
text: this.connectionNumber === 1 ? firstInterim : secondInterim,
final: false,
});
this.emitJson({
type: "eot",
ready: false,
@ -1116,7 +1116,7 @@ test.describe("P1 MVP core loop", () => {
await expect(
page.getByRole("heading", { name: "원본 회기 정보를 확인할 수 없습니다." }),
).toBeVisible();
await expect(page.getByRole("button", { name: "새 회기 시작" })).toBeDisabled();
await expect(page.getByRole("button", { name: "완전히 새로 시작" })).toBeDisabled();
await expectNoHorizontalOverflow(page);
await page.goto(`/learn/session/P1?${malformedQuery}`);
@ -1153,11 +1153,7 @@ test.describe("P1 MVP core loop", () => {
await expect(page.getByRole("heading", { name: "첫 발화 전에 내 기준을 잠급니다" })).toBeVisible();
await expect(page.getByLabel("학습자 발화 입력")).toBeDisabled();
await page.screenshot({
path: path.resolve(
process.cwd(),
"../../docs/ops/evidence",
`g1-alliance-pre-${testInfo.project.name}-2026-08-07.png`,
),
path: testInfo.outputPath("g1-alliance-pre.png"),
fullPage: true,
});
await selectAllianceScore(page, "목표", "3 보통이다");
@ -1176,11 +1172,7 @@ test.describe("P1 MVP core loop", () => {
await expect(page.getByLabel("학습자 발화 입력")).toBeEnabled();
await page.getByRole("button", { name: "30초 점검" }).click();
await page.screenshot({
path: path.resolve(
process.cwd(),
"../../docs/ops/evidence",
`g1-alliance-mid-${testInfo.project.name}-2026-08-07.png`,
),
path: testInfo.outputPath("g1-alliance-mid.png"),
fullPage: true,
});
await selectAllianceScore(page, "목표", "4 대체로 그렇다");
@ -1210,7 +1202,8 @@ test.describe("P1 MVP core loop", () => {
page.getByRole("button", { name: "회기 시작" }),
diagnostics.join("\n") || (await page.locator("#root").innerText().catch(() => "")),
).toBeVisible();
await page.getByRole("button", { name: /CBT/ }).click();
await page.locator("details.sx-prestart__settings > summary").click();
await page.getByRole("button", { name: /^CBT/ }).click();
await page.getByRole("button", { name: "회기 시작" }).click();
await expect(page.locator(".sx-page.sx-page--active")).toBeVisible();
@ -1253,7 +1246,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
await page.getByLabel("학습자 발화 입력").fill(learnerText);
await page.getByRole("button", { name: "보내기" }).click();
@ -1292,7 +1285,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
await page.getByLabel("학습자 발화 입력").fill(learnerText);
await page.getByRole("button", { name: "보내기" }).click();
@ -1317,7 +1310,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
await page.getByLabel("학습자 발화 입력").fill(learnerText);
await page.getByRole("button", { name: "보내기" }).click();
@ -1341,7 +1334,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
await page.getByLabel("학습자 발화 입력").fill(learnerText);
await page.getByRole("button", { name: "보내기" }).click();
@ -1367,7 +1360,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
await page.getByLabel("학습자 발화 입력").fill(learnerText);
await page.getByRole("button", { name: "보내기" }).click();
@ -1505,6 +1498,14 @@ test.describe("P1 MVP core loop", () => {
await page.getByRole("button", { name: "음성 다시 연결" }).click();
await page.getByRole("button", { name: "발화 보내기" }).click();
await expect.poll(async () =>
page.evaluate(
() =>
window.__voiceLifecycleFixture?.sent.filter((payload) =>
payload.includes('"audio_end"'),
).length ?? 0,
),
).toBe(3);
await page.evaluate(() => window.__voiceLifecycleFixture?.releaseEmptyFinal());
await expect(transcriptLog.locator(".sx-utt.is-learner")).toHaveCount(2);
await expect(page.locator(".sx-mic-block__h")).toContainText("음성을 인식하지 못했습니다");
@ -1519,7 +1520,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
const micButton = page.getByRole("button", { name: "마이크 켜기" });
await expect(micButton).toBeEnabled();
await micButton.click();
@ -1548,7 +1549,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
await page.getByLabel("학습자 발화 입력").fill(learnerText);
await page.getByRole("button", { name: "보내기" }).click();
@ -1583,7 +1584,7 @@ test.describe("P1 MVP core loop", () => {
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await page.getByRole("button", { name: "코칭" }).click();
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
await page.getByLabel("학습자 발화 입력").fill("저 지금 자살하고 싶어요. 도와주세요.");
await page.getByRole("button", { name: "보내기" }).click();