SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리
페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침
버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)
검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
186 lines
5.9 KiB
TypeScript
186 lines
5.9 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
const sessionId = "mvp-session-001";
|
|
const learnerText = "요즘 많이 힘들었겠어요. 어떤 마음이 가장 크게 남아 있나요?";
|
|
const clientReply = "괜찮아요. 천천히 말해볼게요.";
|
|
|
|
async function routeMvpApi(page: Page) {
|
|
await page.route("**/api/auth/me", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
user_id: "00000000-0000-0000-0000-00000000e2e1",
|
|
email: "mvp.learner@hs.ac.kr",
|
|
display_name: "MVP Learner",
|
|
role: "learner",
|
|
cohort_ids: [],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/personas", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([
|
|
{
|
|
code: "P1",
|
|
display_name: "민서(청소년 우울)",
|
|
difficulty: "hard",
|
|
theory_target: ["humanistic"],
|
|
demographics: { age_band: "10대" },
|
|
presenting_summary: "자퇴와 무기력감을 둘러싼 상담 연습",
|
|
voice_preset: "soft-young-fem",
|
|
source: "database",
|
|
degraded: false,
|
|
},
|
|
]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/sessions", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 201,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
session_id: sessionId,
|
|
case_id: "mvp-case-001",
|
|
session_no: 1,
|
|
stage: "라포",
|
|
effective_openness: 0.21,
|
|
recall_summary: null,
|
|
degraded: false,
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(`**/api/sessions/${sessionId}/stream`, async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: [
|
|
"event: token",
|
|
`data: ${clientReply}`,
|
|
"",
|
|
"event: done",
|
|
`data: ${JSON.stringify({
|
|
session_id: sessionId,
|
|
stage: "탐색",
|
|
effective_openness: 0.42,
|
|
turn_seq: 1,
|
|
safety_flagged: false,
|
|
})}`,
|
|
"",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
await page.route(`**/api/sessions/${sessionId}/end`, async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
session_id: sessionId,
|
|
session_no: 1,
|
|
digest_pending: true,
|
|
end_state: { stage: "탐색", turn_seq: 1, effective_openness: 0.42 },
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
session_id: sessionId,
|
|
client: {
|
|
name: "민서",
|
|
initial: "민",
|
|
persona: "P1 · hard",
|
|
},
|
|
date: "2026-06-26",
|
|
durationLabel: "1분 02초",
|
|
durationSeconds: 62,
|
|
reachedPhase: "탐색",
|
|
sessionSignal: "종료됨",
|
|
supervisorState: "평가 완료",
|
|
supervisorName: "AI",
|
|
summary: "평가 AI가 저장된 축어록을 분석했습니다.",
|
|
phases: [{ key: "explore", label: "탐색", weight: 1 }],
|
|
phaseAxis: ["0:00", "1:02"],
|
|
valenceAxis: ["0:00", "1:02"],
|
|
clientValence: [],
|
|
counselorBaseline: [],
|
|
turns: [
|
|
{
|
|
id: "t1",
|
|
ts: "0:01",
|
|
speaker: "learner",
|
|
who: "학습자",
|
|
text: learnerText,
|
|
techniques: [],
|
|
note: null,
|
|
},
|
|
{
|
|
id: "t2",
|
|
ts: "0:04",
|
|
speaker: "client",
|
|
who: "민서",
|
|
text: clientReply,
|
|
techniques: [],
|
|
note: null,
|
|
},
|
|
],
|
|
rubric: [],
|
|
goodMoments: [{ title: "반영", body: "학습자가 정서를 먼저 반영했습니다." }],
|
|
growthPoints: [{ title: "탐색 확장", body: "다음 턴에서 구체 상황을 더 묻습니다." }],
|
|
nextLine: "그 말을 꺼내는 것도 쉽지 않았을 것 같아요.",
|
|
clientFeedback: clientReply,
|
|
audioUrl: null,
|
|
pdfExportUrl: null,
|
|
degraded: false,
|
|
reviewReady: true,
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
test.describe("P1 MVP core loop", () => {
|
|
test("runs login, P1 text stream, session end, and review feedback @single-run", async ({
|
|
page,
|
|
}) => {
|
|
const diagnostics: string[] = [];
|
|
page.on("pageerror", (error) => diagnostics.push(`pageerror: ${error.message}`));
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") diagnostics.push(`console: ${message.text()}`);
|
|
});
|
|
await routeMvpApi(page);
|
|
|
|
await page.goto("/learn/session/P1");
|
|
await expect(
|
|
page.getByRole("button", { name: "회기 시작" }),
|
|
diagnostics.join("\n") || (await page.locator("#root").innerText().catch(() => "")),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "회기 시작" }).click();
|
|
|
|
await expect(page.locator(".sx-page.sx-page--active")).toBeVisible();
|
|
await page.getByLabel("학습자 발화 입력").fill(learnerText);
|
|
await page.getByRole("button", { name: "보내기" }).click();
|
|
|
|
await expect(page.locator(".sx-utt").filter({ hasText: learnerText })).toBeVisible();
|
|
await expect(page.locator(".sx-utt").filter({ hasText: clientReply })).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: /밀어서 회기 종료/ }).press("Enter");
|
|
|
|
await expect(page).toHaveURL(new RegExp(`/learn/session/${sessionId}/review$`));
|
|
await expect(page.getByText("내담자가 남긴 것")).toBeVisible();
|
|
await expect(page.locator(".sr-feedback").getByText(clientReply)).toBeVisible();
|
|
await expect(page.getByText("평가 AI가 저장된 축어록을 분석했습니다.")).toBeVisible();
|
|
});
|
|
});
|