154 lines
8.6 KiB
TypeScript
154 lines
8.6 KiB
TypeScript
import { mkdirSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
import { createServer, type ViteDevServer } from "vite";
|
|
|
|
let vite: ViteDevServer;
|
|
let webUrl: string;
|
|
|
|
test.beforeAll(async () => {
|
|
vite = await createServer({ configFile: resolve("apps/web/vite.config.ts"), root: resolve("apps/web"), server: { host: "127.0.0.1", port: 0 } });
|
|
await vite.listen();
|
|
const address = vite.httpServer?.address();
|
|
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
|
|
webUrl = `http://127.0.0.1:${address.port}`;
|
|
});
|
|
|
|
test.afterAll(async () => vite.close());
|
|
|
|
const adminSession = {
|
|
acknowledged_private_content_notice_version: null,
|
|
admin: { role: "super_admin", status: "active", user_id: "00000000-0000-4000-8000-000000000901" },
|
|
audience: "admin", authenticated: true,
|
|
csrf_token: "csrf-admin-model-fixture-000000000000000000000000000000000",
|
|
current_private_content_notice_version: null, expires_at: "2026-09-02T12:00:00.000Z", notice_acknowledged: false,
|
|
};
|
|
|
|
function configuration(version = 1) {
|
|
const ids = ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview", "gpt-image-2"];
|
|
return {
|
|
config_set_version: version,
|
|
configured_default_model_id: version === 1 ? ids[0] : ids[1],
|
|
recommended_model_id: null,
|
|
models: ids.map((modelId, index) => ({
|
|
config_version: version === 1 ? 1 : index < 2 ? 2 : 1,
|
|
contract_evidence_ref: null,
|
|
contract_validation_status: "unverified",
|
|
credit_cost: 1,
|
|
display_name: ["Gemini 3.1 Flash Image Preview", "Gemini 3 Pro Image Preview", "GPT Image 2"][index],
|
|
enabled: version === 1 || index !== 0,
|
|
error_mapping_profile: { timeout: "upstream_timeout" },
|
|
gateway_account_ref: "mock-gateway",
|
|
is_default: version === 1 ? index === 0 : index === 1,
|
|
model_id: modelId,
|
|
prompt_max_length: 1_000,
|
|
recommendation_priority: index + 1,
|
|
reference_limits: { max_file_bytes: 1_024, max_files: 2, max_total_bytes: 2_048 },
|
|
route_profile: { endpoint: "https://mock.invalid/v1/images" },
|
|
runtime_availability: { available_for_new_jobs: false, checked_at: "2026-08-02T12:00:00.000Z", reason: "contract_unverified" },
|
|
safety_source: "provider",
|
|
supported_ratios: ["3:4", "1:1", "4:3", "9:16"],
|
|
})),
|
|
};
|
|
}
|
|
|
|
function runtimeConfiguration() {
|
|
const result = configuration();
|
|
result.recommended_model_id = "gemini-3-pro-image-preview";
|
|
result.models[0].contract_validation_status = "verified";
|
|
result.models[1].contract_validation_status = "verified";
|
|
result.models[2].contract_validation_status = "verified";
|
|
result.models[0].runtime_availability = { available_for_new_jobs: false, checked_at: "2026-08-02T15:00:00.000Z", reason: "gateway_balance_insufficient" };
|
|
result.models[1].runtime_availability = { available_for_new_jobs: true, checked_at: "2026-08-02T15:00:00.000Z", reason: "available" };
|
|
result.models[2].runtime_availability = { available_for_new_jobs: false, checked_at: "2026-08-02T15:00:00.000Z", reason: "worker_degraded" };
|
|
return result;
|
|
}
|
|
|
|
async function routeBase(page: Page) {
|
|
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill({ body: JSON.stringify(adminSession), contentType: "application/json", status: 200 }));
|
|
}
|
|
|
|
test("TDD-WP3-MDL-002-replace-default edits and submits one complete model set", async ({ page }) => {
|
|
await routeBase(page);
|
|
await page.route("**/api/v1/models", (route) => route.fulfill({ body: JSON.stringify(configuration()), contentType: "application/json", status: 200 }));
|
|
let payload: { expected_config_set_version: number; models: Array<Record<string, unknown>> } | undefined;
|
|
await page.route("**/api/v1/admin/models/configuration", (route) => {
|
|
payload = route.request().postDataJSON() as typeof payload;
|
|
return route.fulfill({ body: JSON.stringify(configuration(2)), contentType: "application/json", status: 200 });
|
|
});
|
|
|
|
await page.goto(`${webUrl}/admin/models`);
|
|
await expect(page.getByRole("heading", { name: "模型配置" })).toBeVisible();
|
|
await expect(page.getByRole("columnheader", { name: "默认", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("columnheader", { name: "当前推荐" })).toBeVisible();
|
|
await expect(page.getByRole("columnheader", { name: "运行时可用" })).toBeVisible();
|
|
await expect(page.getByText("当前推荐").locator("..").getByText("无", { exact: true })).toBeVisible();
|
|
|
|
const proPriority = page.getByLabel("Gemini 3 Pro Image Preview 推荐优先级");
|
|
await proPriority.fill("1");
|
|
await expect(page.getByText("推荐优先级不能重复。")).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "保存完整配置集合" })).toBeDisabled();
|
|
await proPriority.fill("2");
|
|
await page.getByLabel("设为默认 Gemini 3 Pro Image Preview").check();
|
|
await page.getByLabel("启用 Gemini 3.1 Flash Image Preview").uncheck();
|
|
await page.getByRole("button", { name: "保存完整配置集合" }).click();
|
|
|
|
await expect(page.getByText("模型配置已原子保存并记录审计。")).toBeVisible();
|
|
expect(payload?.expected_config_set_version).toBe(1);
|
|
expect(payload?.models).toHaveLength(3);
|
|
expect(payload?.models[0]).not.toHaveProperty("runtime_availability");
|
|
expect(payload?.models.filter((model) => model.enabled && model.is_default)).toHaveLength(1);
|
|
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_MODELS;
|
|
if (evidenceRoot) {
|
|
const directory = resolve(evidenceRoot, "TDD-WP3-MDL-002-replace-default", "screenshots");
|
|
mkdirSync(directory, { recursive: true });
|
|
await page.screenshot({ fullPage: true, path: resolve(directory, "model-set-saved.png") });
|
|
}
|
|
});
|
|
|
|
test("TDD-WP3-MDL-002-cas-conflict blocks saving until the complete set is refreshed", async ({ page }) => {
|
|
await routeBase(page);
|
|
await page.route("**/api/v1/models", (route) => route.fulfill({ body: JSON.stringify(configuration()), contentType: "application/json", status: 200 }));
|
|
await page.route("**/api/v1/admin/models/configuration", (route) => route.fulfill({
|
|
body: JSON.stringify({ error: { code: "MODEL_CONFIG_VERSION_CONFLICT", correlation_id: "00000000-0000-4000-8000-000000000902", details: { latest_version: 2 }, message_key: "MODEL_CONFIG_VERSION_CONFLICT" } }),
|
|
contentType: "application/json", status: 412,
|
|
}));
|
|
|
|
await page.goto(`${webUrl}/admin/models`);
|
|
await page.getByRole("button", { name: "保存完整配置集合" }).click();
|
|
await expect(page.getByRole("alert")).toContainText("请刷新最新配置后重新编辑");
|
|
await expect(page.getByRole("button", { name: "保存完整配置集合" })).toBeDisabled();
|
|
await expect(page.getByRole("button", { name: "刷新最新配置" })).toBeVisible();
|
|
});
|
|
|
|
test("TDD-WP3-MDL-003 keeps configured default, runtime and recommendation independent", async ({ page }) => {
|
|
await routeBase(page);
|
|
let recovered = false;
|
|
await page.route("**/api/v1/models", (route) => route.fulfill({ body: JSON.stringify(recovered ? {
|
|
...runtimeConfiguration(),
|
|
recommended_model_id: "gemini-3.1-flash-image-preview",
|
|
models: runtimeConfiguration().models.map((model) => ({ ...model, runtime_availability: { available_for_new_jobs: true, checked_at: "2026-08-02T15:01:00.000Z", reason: "available" } })),
|
|
} : runtimeConfiguration()), contentType: "application/json", status: 200 }));
|
|
await page.goto(`${webUrl}/admin/models`);
|
|
await expect(page.getByText("配置默认").locator("..").getByText("gemini-3.1-flash-image-preview", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("当前推荐").locator("..").getByText("gemini-3-pro-image-preview", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("不可用 · gateway_balance_insufficient")).toBeVisible();
|
|
await expect(page.getByText("可用于新任务")).toBeVisible();
|
|
await expect(page.locator("tbody tr").nth(1).getByText("是", { exact: true })).toBeVisible();
|
|
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_RUNTIME;
|
|
if (evidenceRoot) {
|
|
const directory = resolve(evidenceRoot, "TDD-WP3-MDL-003-runtime-recommendation", "screenshots");
|
|
mkdirSync(directory, { recursive: true });
|
|
await page.screenshot({ fullPage: true, path: resolve(directory, "model-states.png") });
|
|
}
|
|
recovered = true;
|
|
await page.reload();
|
|
await expect(page.locator(".admin-models-summary").getByText("gemini-3.1-flash-image-preview", { exact: true }).last()).toBeVisible();
|
|
if (evidenceRoot) {
|
|
const directory = resolve(evidenceRoot, "TDD-WP3-BAL-001-confirmed-recovery", "screenshots");
|
|
mkdirSync(directory, { recursive: true });
|
|
await page.screenshot({ fullPage: true, path: resolve(directory, "recovery.png") });
|
|
}
|
|
});
|