세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터

- 누적 작업트리 커밋: 회기 평가 복구·durable 저장, 라이브 코치 이력/근거, 교수자 학생분석, 음성 비언어 메타, PII 마스킹, 운영 티켓/헬스 등
- 문서: 완료 기록 docs/archive/ 냉동 보관, docs/ 단일 인덱스(docs/README.md)+통합 TODO(docs/TODO.md)로 정리
- 리팩터(행위 보존): Stage enum SSOT(taxonomy 소유·state_machine re-export), store recent/masked_turns 중복 제거, speaker_ko_label 단일 헬퍼, _list_sessions N+1 제거(state/turns 배치 + 턴평가 하이드레이션 배치)
- 검증: 백엔드 pytest 352 passed, _list_sessions E2E chromium-single-run 2 passed
This commit is contained in:
Yun Chan 2026-07-02 02:50:36 +09:00
parent 7c41c3ce79
commit 778e8526d4
108 changed files with 6457 additions and 455 deletions

View file

@ -242,6 +242,80 @@ test.describe("session review", () => {
expect(reviewAttempts).toBeGreaterThanOrEqual(2);
});
test("keeps polling long-running session evaluation until the ready review arrives", async ({
page,
}) => {
await routeSupervisorCapableLearner(page);
await routePrepostMeasures(page);
const sessionId = "review-delayed-ready";
let reviewAttempts = 0;
const readyAtAttempt = 10;
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
reviewAttempts += 1;
const ready = {
...filledReviewResponse(sessionId),
summary: "평가 AI가 늦게 완료된 deep-loop 결과를 반영했습니다.",
};
const pending = {
...ready,
supervisorState: "평가 대기",
summary: "평가 AI가 긴 회기 축어록을 분석 중입니다.",
rubric: [],
goodMoments: [],
growthPoints: [],
degraded: true,
reviewReady: false,
};
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(reviewAttempts >= readyAtAttempt ? ready : pending),
});
});
await page.goto(`/learn/session/${sessionId}/review`);
await expect(page.getByText("평가 AI가 늦게 완료된 deep-loop 결과를 반영했습니다.")).toBeVisible({
timeout: 20_000,
});
await expect(page.getByText("평가 완료")).toBeVisible();
expect(reviewAttempts).toBeGreaterThanOrEqual(readyAtAttempt);
});
test("does not offer manual AI retry while session evaluation is still pending", async ({
page,
}) => {
await routeSupervisorCapableLearner(page);
await routePrepostMeasures(page);
const sessionId = "review-pending-no-manual-retry";
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
const pending = {
...filledReviewResponse(sessionId),
supervisorState: "평가 대기",
summary: "평가 AI가 저장된 축어록을 분석 중입니다.",
rubric: [],
goodMoments: [],
growthPoints: [],
degraded: true,
reviewReady: false,
};
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(pending),
});
});
await page.goto(`/teach/session/${sessionId}/review`);
await expect(page.getByText("평가 대기", { exact: true })).toBeVisible();
await expect(page.getByText("평가 AI가 저장된 축어록을 분석 중입니다.")).toBeVisible();
await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "검토 완료" })).toBeDisabled();
});
test("lets supervisor retry a failed AI session evaluation", async ({ page }) => {
await routeSupervisorCapableLearner(page);
await routePrepostMeasures(page);
@ -303,11 +377,13 @@ test.describe("session review", () => {
await page.goto(`/teach/session/${sessionId}/review`);
await expect(page.getByText("평가 실패")).toBeVisible();
await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toBeVisible();
await expect(page.getByRole("button", { name: "검토 완료" })).toBeDisabled();
await page.getByRole("button", { name: "AI 평가 재시도" }).click();
await expect(page.getByText("평가 완료")).toBeVisible();
await expect(page.getByText("평가 실패")).toHaveCount(0);
await expect(page.getByRole("button", { name: "검토 완료" })).toBeEnabled();
expect(reevaluateRequests).toBe(1);
});
@ -483,5 +559,13 @@ test.describe("session review", () => {
await expect(page.getByText("2/3 쌍")).toBeVisible();
await expect(page.locator(".sr-prepost__actions")).toContainText("저장된 값 기준");
const prepostCard = page.locator(".sr-card--prepost");
await prepostInputs.nth(0).fill("");
await expect(prepostInputs.nth(0)).toHaveAttribute("aria-invalid", "true");
await expect(prepostCard.locator(".sr-prepost__actions")).toContainText("저장되지 않은 입력 있음");
await expect(prepostCard.locator(".sr-prepost__actions")).not.toContainText("저장된 값 기준");
await expect(prepostCard.getByRole("alert")).toContainText("기존 값을 비우려면 새 점수를 입력해 주세요.");
await expect(prepostCard.getByRole("button", { name: "점수 저장" })).toBeDisabled();
});
});