현재 작업 전체 반영
This commit is contained in:
parent
5560638e54
commit
c0dddab594
85 changed files with 11322 additions and 539 deletions
|
|
@ -39,6 +39,35 @@ interface AdminUsersResponse {
|
|||
users: AdminManagedUser[];
|
||||
}
|
||||
|
||||
interface AdminUsageBreakdown {
|
||||
provider: string;
|
||||
model: string;
|
||||
turns: number;
|
||||
tokens_in: number;
|
||||
tokens_out: number;
|
||||
cost_usd: number;
|
||||
}
|
||||
|
||||
interface AdminUsageBudget {
|
||||
limit_usd: number;
|
||||
used_ratio: number;
|
||||
remaining_usd: number | null;
|
||||
status: "disabled" | "ok" | "warn" | "exceeded";
|
||||
}
|
||||
|
||||
interface AdminUsageResponse {
|
||||
source: "database" | "server_session_registry";
|
||||
durable: boolean;
|
||||
window_days: number;
|
||||
total_turns: number;
|
||||
metered_turns: number;
|
||||
tokens_in: number;
|
||||
tokens_out: number;
|
||||
cost_usd: number;
|
||||
budget: AdminUsageBudget;
|
||||
by_provider: AdminUsageBreakdown[];
|
||||
}
|
||||
|
||||
async function expectResponseOk(response: APIResponse | Response) {
|
||||
if (!response.ok()) {
|
||||
expect(response.ok(), await response.text()).toBeTruthy();
|
||||
|
|
@ -66,6 +95,11 @@ function isAdminUsersResponse(response: Response) {
|
|||
return response.request().method() === "GET" && url.pathname.endsWith("/admin/users");
|
||||
}
|
||||
|
||||
function isAdminUsageResponse(response: Response) {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "GET" && url.pathname.endsWith("/admin/usage");
|
||||
}
|
||||
|
||||
function isAdminUserCreate(response: Response) {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname.endsWith("/admin/users");
|
||||
|
|
@ -106,17 +140,33 @@ function engineModeLabel(value: string) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function countLabel(value: number) {
|
||||
if (!Number.isFinite(value)) return "0";
|
||||
return Math.round(value).toLocaleString("ko-KR");
|
||||
}
|
||||
|
||||
function costLabel(value: number) {
|
||||
if (!Number.isFinite(value) || value <= 0) return "$0";
|
||||
return `$${value.toFixed(value < 0.01 ? 6 : 4)}`;
|
||||
}
|
||||
|
||||
async function openAdminAndReadHealth(page: Page) {
|
||||
const healthResponsePromise = page.waitForResponse(isAdminHealthResponse);
|
||||
const usageResponsePromise = page.waitForResponse(isAdminUsageResponse);
|
||||
|
||||
await page.goto("/admin");
|
||||
|
||||
const healthResponse = await healthResponsePromise;
|
||||
const [healthResponse, usageResponse] = await Promise.all([
|
||||
healthResponsePromise,
|
||||
usageResponsePromise,
|
||||
]);
|
||||
await expectResponseOk(healthResponse);
|
||||
await expectResponseOk(usageResponse);
|
||||
|
||||
const health = (await healthResponse.json()) as AdminHealthResponse;
|
||||
const usage = (await usageResponse.json()) as AdminUsageResponse;
|
||||
expect(health.services.length).toBeGreaterThan(0);
|
||||
return health;
|
||||
return { health, usage };
|
||||
}
|
||||
|
||||
async function openAdminAndReadUsers(page: Page) {
|
||||
|
|
@ -225,7 +275,7 @@ test.describe("admin route", () => {
|
|||
await signInAsAdmin(page);
|
||||
|
||||
await withGlobalEngineConfigLock("admin-health-dashboard", async () => {
|
||||
const health = await openAdminAndReadHealth(page);
|
||||
const { health, usage } = await openAdminAndReadHealth(page);
|
||||
const serviceCards = page.locator(".ad-service:not(.ad-service--skeleton)");
|
||||
const counts = {
|
||||
ok: health.services.filter((service) => service.status === "ok").length,
|
||||
|
|
@ -242,6 +292,34 @@ test.describe("admin route", () => {
|
|||
String(counts.degraded),
|
||||
String(counts.down),
|
||||
]);
|
||||
await expect(page.getByRole("heading", { name: "AI 비용 관측" })).toBeVisible();
|
||||
await expect(page.locator(".ad-usage-kpi b")).toHaveText([
|
||||
costLabel(usage.cost_usd),
|
||||
countLabel(usage.tokens_in),
|
||||
countLabel(usage.tokens_out),
|
||||
usage.total_turns > 0
|
||||
? `${Math.round((usage.metered_turns / usage.total_turns) * 100)}%`
|
||||
: "0%",
|
||||
]);
|
||||
await expect(page.locator(".ad-usage-budget")).toContainText(
|
||||
usage.budget.status === "disabled"
|
||||
? "예산 경고 비활성"
|
||||
: usage.budget.status === "exceeded"
|
||||
? "예산 초과"
|
||||
: usage.budget.status === "warn"
|
||||
? "예산 주의"
|
||||
: "예산 정상",
|
||||
);
|
||||
if (usage.by_provider.length > 0) {
|
||||
await expect(page.locator(".ad-usage-row")).toHaveCount(usage.by_provider.length);
|
||||
await expect(page.locator(".ad-usage-row").first()).toContainText(
|
||||
usage.by_provider[0].provider,
|
||||
);
|
||||
} else {
|
||||
await expect(page.locator(".ad-usage-breakdown")).toContainText(
|
||||
"최근 윈도우에 계량된 AI 턴이 없습니다.",
|
||||
);
|
||||
}
|
||||
await expect(serviceCards).toHaveCount(health.services.length);
|
||||
|
||||
for (const service of health.services) {
|
||||
|
|
|
|||
|
|
@ -69,10 +69,8 @@ test.describe("auth domain policy", () => {
|
|||
const googleButtons = page.locator(".lg-obtn");
|
||||
await expect(googleButtons).toHaveCount(2);
|
||||
await expect(page.locator(".lg-policy b")).toContainText(config.allowed_email_domains);
|
||||
const currentHost = new URL(page.url()).hostname;
|
||||
const redirectHost = new URL(config.redirect_uri).hostname;
|
||||
const localOAuthUnavailable =
|
||||
isLocalHostname(currentHost) &&
|
||||
const devOAuthUnavailable =
|
||||
config.dev_login_enabled &&
|
||||
!isLocalHostname(redirectHost);
|
||||
if (config.dev_login_enabled) {
|
||||
|
|
@ -81,7 +79,7 @@ test.describe("auth domain policy", () => {
|
|||
await expect(page.locator(".lg-dev")).toHaveCount(0);
|
||||
}
|
||||
|
||||
if (config.google_oauth_configured && !localOAuthUnavailable) {
|
||||
if (config.google_oauth_configured && !devOAuthUnavailable) {
|
||||
await expect(googleButtons.first()).toBeEnabled();
|
||||
await expect(page.locator(".lg-config")).toHaveCount(0);
|
||||
} else {
|
||||
|
|
@ -91,8 +89,11 @@ test.describe("auth domain policy", () => {
|
|||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(page.locator("body")).not.toContainText("Google OAuth is not configured");
|
||||
|
||||
if (localOAuthUnavailable) {
|
||||
if (devOAuthUnavailable) {
|
||||
await expect(page.locator(".lg-config")).toContainText("로컬 테스트 계정으로 로그인");
|
||||
await page.goto("/api/auth/login?provider=google&next=%2Flearn");
|
||||
await expect(page).toHaveURL(/\/login\?oauth=local_oauth_unavailable$/);
|
||||
await expect(page.locator(".lg-error")).toContainText("로컬 개발 주소에서는 Google OAuth");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +104,14 @@ test.describe("auth domain policy", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("shows the concrete OAuth failure reason on the login screen", async ({ page }) => {
|
||||
await page.goto("/login?oauth=provider_error");
|
||||
const error = page.locator(".lg-error");
|
||||
|
||||
await expect(error).toContainText("Google이 인증 코드를 발급하지 못했습니다");
|
||||
await expect(error).toContainText("오류 코드: provider_error");
|
||||
});
|
||||
|
||||
test("logs in locally with the server dev session and redirects to learner home", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue