import { expect, test, type APIResponse, type Page } from "@playwright/test"; import { completeOnboarding } from "./support"; interface HealthResponse { db?: boolean; } interface SourcePackSyncItem { source_id: string; doc_id: number | null; chunks_indexed: number; skipped_unchanged: boolean; embedded: boolean; degraded: boolean; } interface SourcePackSyncResponse { sources_upserted: number; chunks_indexed: number; skipped_unchanged: number; embedded: boolean; degraded: boolean; items: SourcePackSyncItem[]; } interface EvalGroundingResponse { chunks: Array<{ source_id?: string | null; }>; policy: string; } async function expectResponseOk(response: APIResponse) { if (!response.ok()) { expect(response.ok(), await response.text()).toBeTruthy(); } } async function signInForSourcePackSync(page: Page, role: "admin" | "learner") { const response = await page.request.post("/api/auth/dev-login", { data: { email: role === "admin" ? "source-pack-admin@twentyoz.kr" : "source-pack-learner@hs.ac.kr", role, display_name: role === "admin" ? "Source Pack Admin" : "Source Pack Learner", }, }); await expectResponseOk(response); await completeOnboarding(page, { legal_name: role === "admin" ? "Source Pack Admin" : "Source Pack Learner", affiliation: "한신대학교", department: role === "admin" ? "운영" : "상담심리학과", grade_level: role === "admin" ? "관리자" : "3학년", phone: role === "admin" ? "010-3333-3333" : "010-4444-4444", contact_address: "경기도 오산시 한신대학교", }); } test.describe("live coach source packs", () => { test("syncs licensed source packs into DB-backed evaluator RAG with admin-only access @single-run", async ({ page, }) => { test.setTimeout(180_000); const healthResponse = await page.request.get("/api/health"); await expectResponseOk(healthResponse); const health = (await healthResponse.json()) as HealthResponse; test.skip(!health.db, "source pack sync requires DB"); const anonymousSync = await page.request.post("/api/kb/live-coach/source-packs/sync"); expect(anonymousSync.status()).toBe(401); await signInForSourcePackSync(page, "learner"); const learnerSync = await page.request.post("/api/kb/live-coach/source-packs/sync"); expect(learnerSync.status()).toBe(403); await signInForSourcePackSync(page, "admin"); const syncResponse = await page.request.post("/api/kb/live-coach/source-packs/sync"); expect(syncResponse.status(), await syncResponse.text()).toBe(202); const sync = (await syncResponse.json()) as SourcePackSyncResponse; const sourceIds = new Set(sync.items.map((item) => item.source_id)); expect(sync.sources_upserted).toBeGreaterThanOrEqual(4); expect(sync.items.length).toBeGreaterThanOrEqual(4); expect(sourceIds).toContain("workbook_0615_case_conceptualization"); expect(sourceIds).toContain("dsm5tr_case_formulation"); expect(sourceIds).toContain("official_counseling_guideline_seed"); expect(sourceIds).toContain("official_suicide_risk_guidelines"); expect(sync.chunks_indexed).toBeGreaterThanOrEqual(0); expect(sync.skipped_unchanged).toBeGreaterThanOrEqual(0); expect(typeof sync.embedded).toBe("boolean"); expect(typeof sync.degraded).toBe("boolean"); for (const item of sync.items) { expect(typeof item.source_id).toBe("string"); expect(item.source_id.length).toBeGreaterThan(0); expect(item.doc_id === null || typeof item.doc_id === "number").toBe(true); expect(item.chunks_indexed).toBeGreaterThanOrEqual(0); expect(typeof item.skipped_unchanged).toBe("boolean"); expect(typeof item.embedded).toBe("boolean"); expect(typeof item.degraded).toBe("boolean"); expect(item.skipped_unchanged || item.chunks_indexed > 0).toBe(true); } const groundingResponse = await page.request.post("/api/kb/eval-grounding", { data: { query: "안전계획 보호요인 위기전화", k: 3, rerank: false, source_id: ["official_suicide_risk_guidelines"], }, }); expect(groundingResponse.status(), await groundingResponse.text()).toBe(200); const grounding = (await groundingResponse.json()) as EvalGroundingResponse; expect(grounding.policy).toMatch(/^evaluator:/); expect(grounding.chunks.length).toBeGreaterThan(0); expect(grounding.chunks.every((chunk) => chunk.source_id === "official_suicide_risk_guidelines")).toBe(true); }); });