feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -56,6 +56,7 @@ interface AdminEngineConfigResponse {
|
|||
engine_mode: string;
|
||||
engine_url: string;
|
||||
model: string;
|
||||
reasoning_effort: string | null;
|
||||
source: "database" | "runtime_cache" | "runtime_default";
|
||||
durable: boolean;
|
||||
updated_by: string | null;
|
||||
|
|
@ -100,6 +101,7 @@ function engineConfigFixture(
|
|||
engine_mode: "openai",
|
||||
engine_url: "http://127.0.0.1:9099",
|
||||
model: "gateway-default",
|
||||
reasoning_effort: "medium",
|
||||
source: "database",
|
||||
durable: true,
|
||||
updated_by: "admin@twentyoz.kr",
|
||||
|
|
@ -109,7 +111,7 @@ function engineConfigFixture(
|
|||
}
|
||||
|
||||
interface AdminAiMockState {
|
||||
counts: { usage: number; health: number; engine: number };
|
||||
counts: { usage: number; health: number; engine: number; capabilities: number };
|
||||
authUser: {
|
||||
account_status?: "pending" | "approved" | "suspended";
|
||||
onboarding_completed_at?: number | null;
|
||||
|
|
@ -124,7 +126,7 @@ interface AdminAiMockState {
|
|||
|
||||
function createMockState(overrides: Partial<AdminAiMockState> = {}): AdminAiMockState {
|
||||
return {
|
||||
counts: { usage: 0, health: 0, engine: 0 },
|
||||
counts: { usage: 0, health: 0, engine: 0, capabilities: 0 },
|
||||
authUser: {},
|
||||
usageForWindow: (days) => usageFixture(days),
|
||||
engineConfig: engineConfigFixture(),
|
||||
|
|
@ -198,6 +200,31 @@ async function mockAdminAiSession(page: Page, state: AdminAiMockState) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (method === "GET" && path.endsWith("/admin/engine-capabilities")) {
|
||||
state.counts.capabilities += 1;
|
||||
const provider = url.searchParams.get("engine_mode") ?? state.engineConfig.engine_mode;
|
||||
await fulfillJson({
|
||||
provider,
|
||||
available: true,
|
||||
source: "live_cli",
|
||||
models: [
|
||||
{
|
||||
id: "gateway-default",
|
||||
label: "게이트웨이 기본 모델",
|
||||
description: "테스트 기본 모델",
|
||||
reasoning_efforts: ["medium"],
|
||||
default_reasoning_effort: "medium",
|
||||
is_default: true,
|
||||
},
|
||||
],
|
||||
default_model: "gateway-default",
|
||||
default_reasoning_effort: "medium",
|
||||
detail: "테스트 모델 목록",
|
||||
fetched_at: 1_785_142_800,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "GET" && path.endsWith("/admin/engine-config")) {
|
||||
state.counts.engine += 1;
|
||||
if (state.engineConfigStatus) {
|
||||
|
|
@ -235,11 +262,11 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
).toBeVisible();
|
||||
await expect(page.locator('[data-testid="admin-ai-page"]')).toHaveCount(0);
|
||||
// 승인 대기 계정은 관리자 데이터 API를 호출하지 않아야 한다.
|
||||
expect(state.counts).toEqual({ usage: 0, health: 0, engine: 0 });
|
||||
expect(state.counts).toEqual({ usage: 0, health: 0, engine: 0, capabilities: 0 });
|
||||
});
|
||||
|
||||
// checklist: admin-ai-aria-busy, admin-ai-refresh-button
|
||||
test("marks the page aria-busy while loading and reloads all three data sets on refresh", async ({
|
||||
test("marks the page aria-busy while loading and reloads all operations data on refresh", async ({
|
||||
page,
|
||||
}) => {
|
||||
let gate: ReturnType<typeof deferred> | null = deferred();
|
||||
|
|
@ -251,7 +278,7 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
await page.goto("/admin/ai");
|
||||
|
||||
const container = page.locator('[data-testid="admin-ai-page"]');
|
||||
const refreshButton = page.getByRole("button", { name: "새로고침" });
|
||||
const refreshButton = page.getByRole("button", { name: "새로고침", exact: true });
|
||||
|
||||
// usage 응답이 보류된 동안 aria-busy=true, 새로고침 버튼 비활성.
|
||||
await expect(container).toHaveAttribute("aria-busy", "true");
|
||||
|
|
@ -278,7 +305,12 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
await expect(refreshButton).toBeEnabled();
|
||||
await expect
|
||||
.poll(() => state.counts)
|
||||
.toEqual({ usage: base.usage + 1, health: base.health + 1, engine: base.engine + 1 });
|
||||
.toEqual({
|
||||
usage: base.usage + 1,
|
||||
health: base.health + 1,
|
||||
engine: base.engine + 1,
|
||||
capabilities: base.capabilities + 1,
|
||||
});
|
||||
});
|
||||
|
||||
// checklist: admin-ai-error-alert, admin-ai-engine-loading-empty
|
||||
|
|
@ -302,7 +334,7 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
state.engineConfigStatus = undefined;
|
||||
let gate: ReturnType<typeof deferred> | null = deferred();
|
||||
state.usageGate = () => gate?.promise;
|
||||
await page.getByRole("button", { name: "새로고침" }).click();
|
||||
await page.getByRole("button", { name: "새로고침", exact: true }).click();
|
||||
await expect(alert).toHaveCount(0);
|
||||
|
||||
gate.resolve();
|
||||
|
|
@ -331,7 +363,7 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
|
||||
await page.goto("/admin/ai");
|
||||
const ledger = page.locator(".aic-ledger");
|
||||
await expect(ledger).toContainText("$3.3300");
|
||||
await expect(ledger).toContainText("$3.33");
|
||||
// React dev StrictMode가 mount 효과를 이중 실행해 usage가 1~2회일 수 있어 기준값을 캡처한다.
|
||||
const base = { ...state.counts };
|
||||
expect(base.health).toBe(1);
|
||||
|
|
@ -348,19 +380,20 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
);
|
||||
await page.getByRole("button", { name: "7일" }).click();
|
||||
await expect(page.getByRole("button", { name: "7일" })).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(ledger).toContainText("$1.1100");
|
||||
await expect(ledger).toContainText("$1.11");
|
||||
await expect(ledger).toContainText("7일 DB 집계");
|
||||
|
||||
// 늦게 도착한 90일 응답은 active 플래그로 무시되어야 한다.
|
||||
slowWindow.resolve();
|
||||
await staleResponse;
|
||||
await expect(ledger).toContainText("$1.1100");
|
||||
await expect(ledger).not.toContainText("$9.9900");
|
||||
await expect(ledger).toContainText("$1.11");
|
||||
await expect(ledger).not.toContainText("$9.99");
|
||||
|
||||
// 기간 변경은 usage만 재조회한다(health·engine-config는 초기 1회 그대로).
|
||||
await expect.poll(() => state.counts.usage).toBe(base.usage + 2);
|
||||
expect(state.counts.health).toBe(1);
|
||||
expect(state.counts.engine).toBe(1);
|
||||
expect(state.counts.capabilities).toBe(1);
|
||||
});
|
||||
|
||||
// checklist: admin-ai-coverage-track, admin-ai-daily-chart, admin-ai-cache-panel
|
||||
|
|
@ -423,7 +456,7 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
await expect(chart.locator(".aic-chart__day")).toHaveCount(3);
|
||||
await expect(chart.locator(".aic-chart__day").first()).toHaveAttribute(
|
||||
"title",
|
||||
"2026-07-13 $1.4200",
|
||||
"2026-07-13 $1.42",
|
||||
);
|
||||
await expect(chart.locator(".aic-chart__day").first()).toContainText("8턴");
|
||||
|
||||
|
|
@ -492,7 +525,7 @@ test.describe("full sweep: admin ai operations", () => {
|
|||
updated_by: null,
|
||||
updated_at: null,
|
||||
});
|
||||
await page.getByRole("button", { name: "새로고침" }).click();
|
||||
await page.getByRole("button", { name: "새로고침", exact: true }).click();
|
||||
await expect(rowValue("저장 원천")).toHaveText("런타임 적용");
|
||||
await expect(rowValue("현재 소스")).toHaveText("runtime_default");
|
||||
await expect(rowValue("최근 변경")).toHaveText("기록 없음");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue