vignette/apps/web/e2e/db-persistence.spec.ts

192 lines
6.6 KiB
TypeScript

import { expect, test, type APIResponse } from "@playwright/test";
import { useRealApi, withGlobalEngineConfigLock } from "./support";
interface HealthResponse {
db: boolean;
}
interface UserProfileResponse {
user_id: string;
email: string;
display_name: string;
role: string;
cohort_ids: string[];
affiliation: string;
}
interface UserPreferencesResponse {
theme: string;
voice_preset_id: string;
voice_rate: number;
notifications: {
session_done: boolean;
safety_signal: boolean;
learner_progress: boolean;
product_news: boolean;
};
}
interface AdminUsersResponse {
source: "database" | "server_session_registry";
durable: boolean;
users: UserProfileResponse[];
}
interface AdminEngineConfigResponse {
engine_mode: string;
engine_url: string;
model: string;
reasoning_effort: string | null;
updated_by: string | null;
updated_at: number | null;
}
async function expectResponseOk(response: APIResponse) {
expect(response.ok(), await response.text()).toBeTruthy();
}
function slugFor(testInfo: { project: { name: string }; workerIndex: number; retry: number }) {
const project = testInfo.project.name.includes("mobile") ? "mob" : "desk";
return `${project}.${testInfo.workerIndex}.${testInfo.retry}.${Date.now()}`;
}
test.describe("database-backed runtime state", () => {
test.beforeEach(async ({ page }) => {
await useRealApi(page);
});
test("persists user settings and admin AI configuration through DB-backed APIs @single-run", async ({
page,
}, testInfo) => {
const healthResponse = await page.request.get("/api/health");
await expectResponseOk(healthResponse);
const health = (await healthResponse.json()) as HealthResponse;
test.skip(!health.db, "PostgreSQL is not connected for this run");
const slug = slugFor(testInfo);
const learnerEmail = `db-persist.${slug}@hs.ac.kr`.toLowerCase();
const learnerName = `DB Persist Learner ${testInfo.project.name}`;
const nextName = `DB Persist Updated ${testInfo.project.name}`;
const affiliation = "Hanshin University";
const learnerLogin = await page.request.post("/api/auth/dev-login", {
data: {
email: learnerEmail,
role: "learner",
display_name: learnerName,
},
});
await expectResponseOk(learnerLogin);
const profilePatch = await page.request.patch("/api/users/me", {
data: {
display_name: nextName,
affiliation,
},
});
await expectResponseOk(profilePatch);
const profile = (await profilePatch.json()) as UserProfileResponse;
expect(profile).toMatchObject({
email: learnerEmail,
display_name: nextName,
affiliation,
});
const preferencesPatch = await page.request.patch("/api/users/me/preferences", {
data: {
theme: "dark",
voice_rate: 1.1,
notifications: {
session_done: true,
safety_signal: true,
learner_progress: true,
product_news: true,
},
},
});
await expectResponseOk(preferencesPatch);
const preferences = (await preferencesPatch.json()) as UserPreferencesResponse;
expect(preferences.theme).toBe("dark");
expect(preferences.voice_rate).toBeCloseTo(1.1);
expect(preferences.notifications.product_news).toBe(true);
const adminEmail = `db-persist-admin.${slug}@twentyoz.kr`.toLowerCase();
const adminLogin = await page.request.post("/api/auth/dev-login", {
data: {
email: adminEmail,
role: "admin",
display_name: `DB Persist Admin ${testInfo.project.name}`,
},
});
await expectResponseOk(adminLogin);
const usersResponse = await page.request.get("/api/admin/users");
await expectResponseOk(usersResponse);
const users = (await usersResponse.json()) as AdminUsersResponse;
expect(users.source).toBe("database");
expect(users.durable).toBe(true);
const learner = users.users.find((user) => user.email === learnerEmail);
expect(learner, `Expected ${learnerEmail} in DB-backed admin list`).toBeTruthy();
expect(learner).toMatchObject({
display_name: nextName,
affiliation,
});
await withGlobalEngineConfigLock(`db-persistence-${slug}`, async () => {
const engineResponse = await page.request.get("/api/admin/engine-config");
await expectResponseOk(engineResponse);
const currentEngine = (await engineResponse.json()) as AdminEngineConfigResponse;
const capabilityResponse = await page.request.get(
`/api/admin/engine-capabilities?engine_mode=${encodeURIComponent(currentEngine.engine_mode)}`,
);
await expectResponseOk(capabilityResponse);
const capability = (await capabilityResponse.json()) as {
models: Array<{ id: string; default_reasoning_effort?: string | null }>;
};
const candidate =
capability.models.find((model) => model.id !== currentEngine.model) ?? capability.models[0];
if (!candidate) throw new Error("engine capability returned no selectable model");
const nextModel = candidate.id;
const nextEffort = candidate.default_reasoning_effort ?? null;
try {
const enginePatch = await page.request.patch("/api/admin/engine-config", {
data: {
engine_mode: currentEngine.engine_mode,
engine_url: currentEngine.engine_url,
model: nextModel,
reasoning_effort: nextEffort,
},
});
await expectResponseOk(enginePatch);
const updatedEngine = (await enginePatch.json()) as AdminEngineConfigResponse;
expect(updatedEngine).toMatchObject({
engine_mode: currentEngine.engine_mode,
engine_url: currentEngine.engine_url,
model: nextModel,
reasoning_effort: nextEffort,
updated_by: adminEmail,
});
expect(updatedEngine.updated_at).toBeGreaterThan(0);
const persistedEngineResponse = await page.request.get("/api/admin/engine-config");
await expectResponseOk(persistedEngineResponse);
const persistedEngine = (await persistedEngineResponse.json()) as AdminEngineConfigResponse;
expect(persistedEngine).toMatchObject({
model: nextModel,
updated_by: adminEmail,
});
} finally {
const restoreResponse = await page.request.patch("/api/admin/engine-config", {
data: {
engine_mode: currentEngine.engine_mode,
engine_url: currentEngine.engine_url,
model: currentEngine.model,
reasoning_effort: currentEngine.reasoning_effort,
},
});
await expectResponseOk(restoreResponse);
}
});
});
});