Files
ai-contract-platform/frontend/scripts/slice12-pilot-launch-pack-closed-loop-e2e.mjs
T

256 lines
9.9 KiB
JavaScript

import { chromium, request } from "playwright";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const runId = `slice12-pilot-launch-pack-${Date.now()}`;
const webBase = process.env.SLICE12_WEB_URL || "http://localhost:3000";
const apiBase = process.env.SLICE12_API_URL || "http://127.0.0.1:8010/api/v1";
const healthUrl = process.env.SLICE12_HEALTH_URL || "http://127.0.0.1:8010/health";
const adminPassword = process.env.AI_CONTRACT_ADMIN_PASSWORD || "123456";
const artifactDir =
process.env.SLICE12_ARTIFACT_DIR ||
path.resolve(__dirname, "..", "..", "project-docs", "qa-artifacts", "slice12-pilot-launch-pack-closed-loop", stamp);
fs.mkdirSync(artifactDir, { recursive: true });
const results = [];
const launchScriptDocPath = path.resolve(
__dirname,
"..",
"..",
"project-docs",
"pilot-materials",
"AI合同起草与审查助手-销售客户演示脚本.md",
);
const state = {
api: null,
browser: null,
contexts: [],
admin: null,
};
function apiUrl(route) {
return `${apiBase.replace(/\/$/, "")}${route}`;
}
function screenshotPath(name) {
return path.join(artifactDir, `${name}.png`);
}
function assert(condition, message, details = undefined) {
if (!condition) {
const error = new Error(message);
error.details = details;
throw error;
}
}
async function step(id, title, fn) {
const startedAt = new Date().toISOString();
const started = Date.now();
try {
const details = await fn();
const item = { id, title, status: "PASS", duration_ms: Date.now() - started, started_at: startedAt, details: details || {} };
results.push(item);
console.log(`PASS ${id} ${title}`);
return item;
} catch (error) {
const item = {
id,
title,
status: "FAIL",
duration_ms: Date.now() - started,
started_at: startedAt,
error: error.message,
details: error.details || {},
};
results.push(item);
console.error(`FAIL ${id} ${title}: ${error.message}`);
return item;
}
}
async function apiRequest(method, route, options = {}) {
const headers = {
...(options.authToken ? { Authorization: `Bearer ${options.authToken}` } : {}),
...(options.headers || {}),
};
const response = await state.api[method](apiUrl(route), { ...options, headers });
const text = await response.text();
let body = null;
if (text) {
try {
body = JSON.parse(text);
} catch {
body = text;
}
}
return { response, body, text };
}
function expectOk(payload, label) {
assert(payload.response.status() >= 200 && payload.response.status() < 300, `${label} HTTP ${payload.response.status()}`, {
body: payload.body,
});
if (payload.body && typeof payload.body === "object" && "code" in payload.body) {
assert(payload.body.code === 0, `${label} API code ${payload.body.code}`, { body: payload.body });
}
}
function assertPilotLaunchPack(pack, label) {
assert(pack.version === "pilot-launch-pack-2026-05-12", `${label} version mismatch`, pack);
assert(pack.status === "ready", `${label} status should be ready`, pack);
const stepKeys = new Set((pack.script_steps || []).map((item) => item.key));
for (const key of ["kickoff_alignment", "business_contract_generation", "legal_auto_review", "review_chat_followup", "report_feedback_handoff"]) {
assert(stepKeys.has(key), `${label} missing script step ${key}`, pack.script_steps);
}
for (const item of pack.script_steps || []) {
assert(item.route, `${label} script step ${item.key} missing route`, item);
assert(item.talk_track, `${label} script step ${item.key} missing talk track`, item);
assert(Array.isArray(item.evidence) && item.evidence.length > 0, `${label} script step ${item.key} missing evidence`, item);
}
const gateKeys = new Set((pack.readiness_gates || []).map((item) => item.key));
for (const key of ["system_readiness", "pilot_scope_frozen", "pilot_materials_ready", "error_feedback_ready", "demo_assets_seedable"]) {
assert(gateKeys.has(key), `${label} missing readiness gate ${key}`, pack.readiness_gates);
}
assert((pack.roles || []).length >= 5, `${label} roles too small`, pack.roles);
assert((pack.blockers || []).length >= 4, `${label} blockers too small`, pack.blockers);
assert((pack.acceptance || []).length >= 4, `${label} acceptance too small`, pack.acceptance);
}
async function login(account, password) {
const payload = await apiRequest("post", "/auth/login", { data: { account, password } });
expectOk(payload, `login ${account}`);
return payload.body.data;
}
async function attachApiProxy(context) {
await context.route("**/api/v1/**", async (route) => {
const browserRequest = route.request();
const browserUrl = new URL(browserRequest.url());
const targetUrl = apiUrl(browserUrl.pathname.replace(/^\/api\/v1/, "") + browserUrl.search);
const headers = { ...browserRequest.headers() };
delete headers.host;
try {
const response = await state.api.fetch(targetUrl, {
method: browserRequest.method(),
headers,
data: browserRequest.postDataBuffer() ?? undefined,
});
await route.fulfill({ response });
} catch {
await route.abort("failed").catch(() => {});
}
});
}
async function createAuthenticatedContext(userSession) {
const context = await state.browser.newContext({ viewport: { width: 1440, height: 1400 } });
await context.addInitScript(
({ token, user }) => {
window.localStorage.setItem("access_token", token);
window.localStorage.setItem("auth-store", JSON.stringify({ state: { accessToken: token, currentUser: user }, version: 0 }));
},
{ token: userSession.access_token, user: userSession.user },
);
await attachApiProxy(context);
state.contexts.push(context);
return context;
}
async function run() {
state.api = await request.newContext();
state.browser = await chromium.launch({ headless: true });
await step("ENV-001", "frontend and backend health", async () => {
const [frontend, health] = await Promise.all([state.api.get(`${webBase}/`), state.api.get(healthUrl)]);
assert(frontend.status() === 200, `frontend / status ${frontend.status()}`);
assert(health.status() === 200, `backend /health status ${health.status()}`);
return { webBase, apiBase, healthUrl, artifactDir, runId };
});
await step("DOC-001", "pilot customer demo script document exists", async () => {
assert(fs.existsSync(launchScriptDocPath), "missing customer demo script doc", { launchScriptDocPath });
const stat = fs.statSync(launchScriptDocPath);
assert(stat.size > 600, "customer demo script doc too small", { size: stat.size });
return { launchScriptDocPath, size: stat.size };
});
await step("LAUNCH-API-001", "pilot launch pack API exposes script and gates", async () => {
const payload = await apiRequest("get", "/system/pilot-launch-pack");
expectOk(payload, "pilot launch pack");
assertPilotLaunchPack(payload.body.data, "pilot launch pack API");
return {
version: payload.body.data.version,
script_steps: payload.body.data.script_steps.length,
readiness_gates: payload.body.data.readiness_gates.length,
};
});
await step("READINESS-001", "readiness embeds pilot launch pack", async () => {
const payload = await apiRequest("get", "/system/readiness");
expectOk(payload, "readiness");
assertPilotLaunchPack(payload.body.data.pilot_launch_pack, "readiness pilot launch pack");
return {
readiness_status: payload.body.data.status,
launch_pack_version: payload.body.data.pilot_launch_pack.version,
};
});
await step("AUTH-001", "default admin seed and login", async () => {
const seed = await apiRequest("post", "/auth/seed-admin");
expectOk(seed, "seed default admin");
state.admin = await login("admin@example.com", adminPassword);
assert(state.admin.user.role === "admin", "admin role mismatch", state.admin.user);
return { user: state.admin.user.username, role: state.admin.user.role };
});
await step("BROWSER-001", "pilot readiness page exposes launch pack", async () => {
const context = await createAuthenticatedContext(state.admin);
const page = await context.newPage();
await page.goto(`${webBase}/pilot-readiness`, { waitUntil: "domcontentloaded" });
const panel = page.getByTestId("pilot-launch-pack");
await panel.waitFor({ state: "visible", timeout: 12000 });
const stepCount = await panel.getByTestId("pilot-launch-step").count();
assert(stepCount >= 5, "launch pack should expose five script steps", { stepCount });
await page.getByTestId("pilot-launch-gates").waitFor({ state: "visible", timeout: 12000 });
await page.getByTestId("pilot-launch-roles").waitFor({ state: "visible", timeout: 12000 });
await page.getByTestId("pilot-launch-blockers").waitFor({ state: "visible", timeout: 12000 });
await page.getByTestId("pilot-launch-acceptance").waitFor({ state: "visible", timeout: 12000 });
await page.screenshot({ path: screenshotPath("pilot-launch-pack"), fullPage: true });
return { screenshot: "pilot-launch-pack.png", step_count: stepCount };
});
}
try {
await run();
} finally {
for (const context of state.contexts) {
await context.close().catch(() => {});
}
if (state.browser) {
await state.browser.close().catch(() => {});
}
if (state.api) {
await state.api.dispose().catch(() => {});
}
const summary = {
run_id: runId,
artifact_dir: artifactDir,
passed: results.filter((item) => item.status === "PASS").length,
failed: results.filter((item) => item.status === "FAIL").length,
results,
};
fs.writeFileSync(path.join(artifactDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
console.log(`Wrote ${path.join(artifactDir, "summary.json")}`);
if (summary.failed > 0) {
process.exitCode = 1;
}
}