589 lines
24 KiB
JavaScript
589 lines
24 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 = `phase1-auto-${Date.now()}`;
|
|
const webBase = process.env.PHASE1_WEB_URL || "http://127.0.0.1:3004";
|
|
const apiBase = process.env.PHASE1_API_URL || "http://127.0.0.1:8000/api/v1";
|
|
const healthUrl = process.env.PHASE1_HEALTH_URL || "http://127.0.0.1:8000/health";
|
|
const adminPassword = process.env.AI_CONTRACT_ADMIN_PASSWORD || "123456";
|
|
const artifactDir =
|
|
process.env.PHASE1_ARTIFACT_DIR ||
|
|
path.resolve(__dirname, "..", "..", "..", "project-docs", "qa-artifacts", "phase1-closed-loop", stamp);
|
|
|
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
|
|
const results = [];
|
|
const state = {
|
|
admin: null,
|
|
member: null,
|
|
browser: null,
|
|
contexts: [],
|
|
api: null,
|
|
cleanup: [],
|
|
};
|
|
|
|
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 });
|
|
}
|
|
}
|
|
|
|
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 createAuthenticatedContext(userSession) {
|
|
const context = await state.browser.newContext({
|
|
viewport: { width: 1440, height: 1100 },
|
|
acceptDownloads: true,
|
|
});
|
|
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 });
|
|
state.contexts.push(context);
|
|
return context;
|
|
}
|
|
|
|
function buildVariableValues(template) {
|
|
const values = {};
|
|
for (const variable of template.variables || []) {
|
|
values[variable.name] = `${runId}-${variable.name}`;
|
|
}
|
|
return values;
|
|
}
|
|
|
|
async function downloadAndSave(route, fileName, token) {
|
|
const response = await state.api.get(apiUrl(route), {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
assert(response.status() >= 200 && response.status() < 300, `download failed ${route}: ${response.status()}`);
|
|
const body = await response.body();
|
|
const target = path.join(artifactDir, fileName);
|
|
fs.writeFileSync(target, body);
|
|
assert(body.length > 0, `downloaded file is empty: ${fileName}`);
|
|
return { file: target, bytes: body.length };
|
|
}
|
|
|
|
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 = await state.api.get(`${webBase}/login`);
|
|
const health = await state.api.get(healthUrl);
|
|
assert(frontend.status() === 200, `frontend /login status ${frontend.status()}`);
|
|
assert(health.status() === 200, `backend /health status ${health.status()}`);
|
|
return { webBase, apiBase, healthUrl, artifactDir };
|
|
});
|
|
|
|
await step("AUTH-001", "admin login and current user", async () => {
|
|
state.admin = await login("admin@example.com", adminPassword);
|
|
const me = await apiRequest("get", "/auth/me", { authToken: state.admin.access_token });
|
|
expectOk(me, "auth me");
|
|
assert(me.body.data.role === "admin", "admin role mismatch", me.body.data);
|
|
return { user: me.body.data.username, role: me.body.data.role };
|
|
});
|
|
|
|
await step("AUTH-002", "member registration and permission boundary", async () => {
|
|
const username = `${runId}-member@example.com`;
|
|
const password = "123456";
|
|
const register = await apiRequest("post", "/auth/register", {
|
|
data: { username, password, display_name: "Phase1 Auto Member" },
|
|
});
|
|
expectOk(register, "register member");
|
|
state.member = await login(username, password);
|
|
const forbidden = await apiRequest("get", "/users?page=1&page_size=1", { authToken: state.member.access_token });
|
|
assert(forbidden.response.status() === 403 || forbidden.body?.code === 403, "member should not access users API", {
|
|
status: forbidden.response.status(),
|
|
body: forbidden.body,
|
|
});
|
|
return { username, forbidden_status: forbidden.response.status() };
|
|
});
|
|
|
|
await step("AUTH-003", "unauthenticated protected page redirects", async () => {
|
|
const page = await state.browser.newPage({ viewport: { width: 1365, height: 900 } });
|
|
await page.goto(`${webBase}/contracts`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(1200);
|
|
await page.screenshot({ path: screenshotPath("unauth-contracts-redirect"), fullPage: true });
|
|
assert(page.url().includes("/login"), "unauthenticated /contracts did not redirect to /login", { url: page.url() });
|
|
const finalUrl = page.url();
|
|
await page.close();
|
|
return { url: finalUrl, screenshot: "unauth-contracts-redirect.png" };
|
|
});
|
|
|
|
await step("REVIEW-001", "empty review input is rejected", async () => {
|
|
const payload = await apiRequest("post", "/reviews", {
|
|
authToken: state.admin.access_token,
|
|
multipart: { text: " ", review_as: "party_a", contract_type: "procurement" },
|
|
});
|
|
assert(payload.body?.code === 40001, "empty review should return business validation error", payload.body);
|
|
return { code: payload.body.code, message: payload.body.message };
|
|
});
|
|
|
|
await step("REVIEW-002", "UTF-8 text review completes and report downloads", async () => {
|
|
const text = [
|
|
"\u9636\u6bb51 UTF-8 \u81ea\u52a8\u5316\u5408\u540c\u5ba1\u67e5\u6837\u672c\u3002",
|
|
"\u5408\u540c\u7ea6\u5b9a\uff1a\u4efb\u4f55\u4e00\u65b9\u8fdd\u7ea6\uff0c\u5e94\u6309\u5408\u540c\u603b\u91d1\u989d\u768450%\u652f\u4ed8\u8fdd\u7ea6\u91d1\u3002",
|
|
"\u540c\u65f6\u5b58\u5728\u81ea\u52a8\u7eed\u7ea6\u3001\u514d\u8d23\u548c\u4ed8\u6b3e\u6761\u4ef6\u53e6\u884c\u534f\u5546\u7684\u8868\u8ff0\u3002",
|
|
].join("\n");
|
|
const create = await apiRequest("post", "/reviews", {
|
|
authToken: state.admin.access_token,
|
|
multipart: { text, review_as: "party_a", contract_type: "procurement" },
|
|
});
|
|
expectOk(create, "create text review");
|
|
const reviewId = create.body.data.id;
|
|
const result = await apiRequest("get", `/reviews/${reviewId}/result`, { authToken: state.admin.access_token });
|
|
expectOk(result, "review result");
|
|
await downloadAndSave(`/reviews/${reviewId}/report/download`, `${runId}-review-report.pdf`, state.admin.access_token);
|
|
return {
|
|
review_id: reviewId,
|
|
risk_summary: result.body.data.risk_summary,
|
|
risk_items: (result.body.data.risk_items || []).map((item) => ({ title: item.title, level: item.level })),
|
|
};
|
|
});
|
|
|
|
await step("REVIEW-003", "file review completes", async () => {
|
|
const content = Buffer.from(`${runId}\nThis procurement contract includes payment and confidentiality clauses.`, "utf8");
|
|
const create = await apiRequest("post", "/reviews", {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
file: { name: `${runId}-review.txt`, mimeType: "text/plain", buffer: content },
|
|
review_as: "party_b",
|
|
contract_type: "procurement",
|
|
},
|
|
});
|
|
expectOk(create, "create file review");
|
|
const detail = await apiRequest("get", `/reviews/${create.body.data.id}`, { authToken: state.admin.access_token });
|
|
expectOk(detail, "file review detail");
|
|
assert(detail.body.data.source_type === "file", "file review source_type mismatch", detail.body.data);
|
|
return { review_id: create.body.data.id, source_type: detail.body.data.source_type };
|
|
});
|
|
|
|
await step("CONTRACT-001", "contract preview generate detail download asset", async () => {
|
|
const templates = await apiRequest("get", "/contracts/templates", { authToken: state.admin.access_token });
|
|
expectOk(templates, "contract templates");
|
|
const template = templates.body.data[0];
|
|
assert(template, "no contract template available");
|
|
const variableValues = buildVariableValues(template);
|
|
const preview = await apiRequest("post", "/contracts/preview", {
|
|
authToken: state.admin.access_token,
|
|
multipart: { template_id: template.id, variable_values_json: JSON.stringify(variableValues) },
|
|
});
|
|
expectOk(preview, "contract preview");
|
|
const generated = await apiRequest("post", "/contracts/generate", {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
template_id: template.id,
|
|
variable_values_json: JSON.stringify(variableValues),
|
|
export_format: "txt",
|
|
},
|
|
});
|
|
expectOk(generated, "contract generate");
|
|
const contract = generated.body.data;
|
|
const detail = await apiRequest("get", `/contracts/${contract.id}`, { authToken: state.admin.access_token });
|
|
expectOk(detail, "contract detail");
|
|
await downloadAndSave(`/contracts/${contract.id}/download`, contract.file_name, state.admin.access_token);
|
|
assert(contract.file_id, "generated contract did not create file asset", contract);
|
|
const file = await apiRequest("get", `/files/${contract.file_id}`, { authToken: state.admin.access_token });
|
|
expectOk(file, "generated contract file asset");
|
|
return { contract_id: contract.id, file_id: contract.file_id, file_name: contract.file_name };
|
|
});
|
|
|
|
await step("FILE-001", "file upload detail download delete", async () => {
|
|
const upload = await apiRequest("post", "/files", {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
file: { name: `${runId}-asset.txt`, mimeType: "text/plain", buffer: Buffer.from(`${runId} file asset`, "utf8") },
|
|
file_type: "document",
|
|
bucket: "default",
|
|
},
|
|
});
|
|
expectOk(upload, "upload file");
|
|
const fileId = upload.body.data.id;
|
|
const detail = await apiRequest("get", `/files/${fileId}`, { authToken: state.admin.access_token });
|
|
expectOk(detail, "file detail");
|
|
await downloadAndSave(`/files/${fileId}/download`, `${runId}-asset-downloaded.txt`, state.admin.access_token);
|
|
const deleted = await apiRequest("delete", `/files/${fileId}`, { authToken: state.admin.access_token });
|
|
expectOk(deleted, "delete file");
|
|
return { file_id: fileId, deleted: deleted.body.data.success };
|
|
});
|
|
|
|
await step("KB-001", "knowledge base success and failed document lifecycle", async () => {
|
|
const create = await apiRequest("post", "/knowledge-bases", {
|
|
authToken: state.admin.access_token,
|
|
multipart: { name: `${runId} KB`, description: "Phase 1 automated KB" },
|
|
});
|
|
expectOk(create, "create knowledge base");
|
|
const kbId = create.body.data.id;
|
|
const good = await apiRequest("post", `/knowledge-bases/${kbId}/documents`, {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
file: {
|
|
name: `${runId}-good.txt`,
|
|
mimeType: "text/plain",
|
|
buffer: Buffer.from("Contract review knowledge: payment, confidentiality, liability.", "utf8"),
|
|
},
|
|
},
|
|
});
|
|
expectOk(good, "upload good kb document");
|
|
const empty = await apiRequest("post", `/knowledge-bases/${kbId}/documents`, {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
file: { name: `${runId}-empty.txt`, mimeType: "text/plain", buffer: Buffer.from("") },
|
|
},
|
|
});
|
|
expectOk(empty, "upload empty kb document");
|
|
assert(good.body.data.status === "indexed", "good KB document should be indexed", good.body.data);
|
|
assert(empty.body.data.status === "failed", "empty KB document should fail", empty.body.data);
|
|
assert(Boolean(empty.body.data.next_action), "failed KB document should include next_action", empty.body.data);
|
|
return {
|
|
kb_id: kbId,
|
|
good_status: good.body.data.status,
|
|
failed_status: empty.body.data.status,
|
|
failed_next_action: empty.body.data.next_action,
|
|
};
|
|
});
|
|
|
|
await step("ADMIN-001", "review rule CRUD", async () => {
|
|
const create = await apiRequest("post", "/review-rules", {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
name: `${runId} rule`,
|
|
keyword: runId,
|
|
contract_type: "all",
|
|
risk_level: "medium",
|
|
description: "Phase 1 automated rule",
|
|
suggestion: "Review this phase1-auto marker.",
|
|
legal_basis: "Phase 1 automation",
|
|
is_active: "true",
|
|
},
|
|
});
|
|
expectOk(create, "create review rule");
|
|
const ruleId = create.body.data.id;
|
|
const update = await apiRequest("put", `/review-rules/${ruleId}`, {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
name: `${runId} rule updated`,
|
|
keyword: runId,
|
|
contract_type: "all",
|
|
risk_level: "high",
|
|
description: "Updated phase 1 automated rule",
|
|
suggestion: "Updated suggestion.",
|
|
legal_basis: "Updated basis",
|
|
is_active: "true",
|
|
},
|
|
});
|
|
expectOk(update, "update review rule");
|
|
const status = await apiRequest("patch", `/review-rules/${ruleId}/status`, {
|
|
authToken: state.admin.access_token,
|
|
multipart: { is_active: "false" },
|
|
});
|
|
expectOk(status, "disable review rule");
|
|
const deleted = await apiRequest("delete", `/review-rules/${ruleId}`, { authToken: state.admin.access_token });
|
|
expectOk(deleted, "delete review rule");
|
|
return { rule_id: ruleId };
|
|
});
|
|
|
|
await step("ADMIN-002", "template CRUD", async () => {
|
|
const variables = [{ name: "party", label: "Party" }];
|
|
const create = await apiRequest("post", "/templates", {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
name: `${runId} template`,
|
|
contract_type: "phase1",
|
|
description: "Phase 1 automated template",
|
|
content: "Party: {{party}}",
|
|
variables_json: JSON.stringify(variables),
|
|
enabled: "true",
|
|
},
|
|
});
|
|
expectOk(create, "create template");
|
|
const templateId = create.body.data.id;
|
|
const update = await apiRequest("put", `/templates/${templateId}`, {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
name: `${runId} template updated`,
|
|
contract_type: "phase1",
|
|
description: "Updated phase 1 automated template",
|
|
content: "Updated Party: {{party}}",
|
|
variables_json: JSON.stringify(variables),
|
|
enabled: "false",
|
|
},
|
|
});
|
|
expectOk(update, "update template");
|
|
const deleted = await apiRequest("delete", `/templates/${templateId}`, { authToken: state.admin.access_token });
|
|
assert(deleted.response.status() === 204, `delete template HTTP ${deleted.response.status()}`);
|
|
return { template_id: templateId };
|
|
});
|
|
|
|
await step("ADMIN-003", "user CRUD and tenant settings restore", async () => {
|
|
const created = await apiRequest("post", "/users", {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
username: `${runId}-managed@example.com`,
|
|
display_name: "Phase1 Managed User",
|
|
password: "123456",
|
|
role: "member",
|
|
status: "active",
|
|
},
|
|
});
|
|
expectOk(created, "create managed user");
|
|
const userId = created.body.data.id;
|
|
const updated = await apiRequest("put", `/users/${userId}`, {
|
|
authToken: state.admin.access_token,
|
|
multipart: {
|
|
username: `${runId}-managed@example.com`,
|
|
display_name: "Phase1 Managed User Disabled",
|
|
password: "",
|
|
role: "member",
|
|
status: "disabled",
|
|
},
|
|
});
|
|
expectOk(updated, "disable managed user");
|
|
const currentSettings = await apiRequest("get", "/tenant-settings/me", { authToken: state.admin.access_token });
|
|
expectOk(currentSettings, "tenant settings get");
|
|
const original = currentSettings.body.data;
|
|
const changed = await apiRequest("put", "/tenant-settings/me", {
|
|
authToken: state.admin.access_token,
|
|
multipart: { name: `${original.name} ${runId}`, code: original.code, status: original.status },
|
|
});
|
|
expectOk(changed, "tenant settings update");
|
|
const restored = await apiRequest("put", "/tenant-settings/me", {
|
|
authToken: state.admin.access_token,
|
|
multipart: { name: original.name, code: original.code, status: original.status },
|
|
});
|
|
expectOk(restored, "tenant settings restore");
|
|
return { user_id: userId, tenant_id: original.id };
|
|
});
|
|
|
|
await step("AUDIT-001", "audit log list and action filter", async () => {
|
|
const list = await apiRequest("get", "/audit-logs?page=1&page_size=20", { authToken: state.admin.access_token });
|
|
expectOk(list, "audit log list");
|
|
assert(Array.isArray(list.body.data.items), "audit log items missing", list.body.data);
|
|
const filtered = await apiRequest("get", "/audit-logs?page=1&page_size=20&action=login", { authToken: state.admin.access_token });
|
|
expectOk(filtered, "audit log filter");
|
|
return { total: list.body.data.total, filtered_total: filtered.body.data.total };
|
|
});
|
|
|
|
await step("BROWSER-001", "admin pages load without hard errors", async () => {
|
|
const context = await createAuthenticatedContext(state.admin);
|
|
const page = await context.newPage();
|
|
const pages = ["/", "/review", "/reviews", "/chat", "/contracts", "/knowledge-bases", "/files", "/rules", "/templates", "/tenant-settings", "/users", "/audit-logs"];
|
|
const failures = [];
|
|
for (const route of pages) {
|
|
const errors = [];
|
|
page.removeAllListeners("console");
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error" && !message.text().includes("[antd:")) {
|
|
errors.push(message.text());
|
|
}
|
|
});
|
|
await page.goto(`${webBase}${route}`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(800);
|
|
await page.screenshot({ path: screenshotPath(`page-${route === "/" ? "home" : route.slice(1).replace(/\//g, "-")}`), fullPage: true });
|
|
const body = await page.locator("body").innerText();
|
|
if (!body.trim() || errors.length) {
|
|
failures.push({ route, errors, body: body.slice(0, 300) });
|
|
}
|
|
}
|
|
assert(failures.length === 0, "one or more admin pages failed browser smoke", failures);
|
|
return { pages: pages.length };
|
|
});
|
|
|
|
await step("BROWSER-002", "member admin page shows permission message", async () => {
|
|
const context = await createAuthenticatedContext(state.member);
|
|
const page = await context.newPage();
|
|
await page.goto(`${webBase}/users`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(1000);
|
|
await page.screenshot({ path: screenshotPath("member-users-forbidden"), fullPage: true });
|
|
const body = await page.locator("body").innerText();
|
|
assert(body.includes("\u4ec5\u7ba1\u7406\u5458"), "member /users page did not show admin-only message", body.slice(0, 500));
|
|
return { screenshot: "member-users-forbidden.png" };
|
|
});
|
|
|
|
await step("BROWSER-003", "failed KB document disables chat entry in current frontend", async () => {
|
|
const context = await createAuthenticatedContext(state.admin);
|
|
const page = await context.newPage();
|
|
await page.goto(`${webBase}/knowledge-bases`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: screenshotPath("kb-failed-disabled-current"), fullPage: true });
|
|
const body = await page.locator("body").innerText();
|
|
assert(body.includes("\u4e0b\u4e00\u6b65"), "failed KB document next_action not visible", body.slice(-1200));
|
|
const disabledLinks = await page.locator("a.disabled-link[aria-disabled='true']").count();
|
|
assert(disabledLinks >= 2, "failed KB chat actions should be disabled", { disabledLinks });
|
|
return { disabled_links: disabledLinks, screenshot: "kb-failed-disabled-current.png" };
|
|
});
|
|
|
|
await step("BROWSER-004", "Chat context auto prompt starts a new conversation", async () => {
|
|
const context = await createAuthenticatedContext(state.admin);
|
|
const page = await context.newPage();
|
|
const payload = {
|
|
reviewId: null,
|
|
prompt: `${runId}: summarize this knowledge document only.`,
|
|
metadata: {
|
|
reviewTitle: `${runId} KB context probe`,
|
|
reviewStatus: "failed",
|
|
reviewOpinion: "This context must not reuse an older conversation.",
|
|
focusLabel: `${runId}-empty.txt`,
|
|
actionLabel: "Summarize KB document",
|
|
focusType: "knowledge_document",
|
|
},
|
|
};
|
|
const chatRequests = [];
|
|
page.on("request", (req) => {
|
|
if (req.url().includes("/api/v1/chat/stream")) {
|
|
try {
|
|
chatRequests.push(JSON.parse(req.postData() || "{}"));
|
|
} catch {
|
|
chatRequests.push({});
|
|
}
|
|
}
|
|
});
|
|
await page.goto(`${webBase}/chat?review=${encodeURIComponent(JSON.stringify(payload))}`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForRequest((req) => req.url().includes("/api/v1/chat/stream"), { timeout: 20000 });
|
|
await page.waitForTimeout(2500);
|
|
await page.screenshot({ path: screenshotPath("chat-context-new-conversation"), fullPage: true });
|
|
const sent = chatRequests[0] || {};
|
|
assert(!sent.conversation_id, "context auto prompt should not reuse an existing conversation", sent);
|
|
assert(sent.review_context?.metadata?.reviewTitle === payload.metadata.reviewTitle, "review context title mismatch", sent);
|
|
return { sent_conversation_id: sent.conversation_id || null, screenshot: "chat-context-new-conversation.png" };
|
|
});
|
|
}
|
|
|
|
async function finalize() {
|
|
for (const context of state.contexts) {
|
|
await context.close().catch(() => undefined);
|
|
}
|
|
if (state.browser) {
|
|
await state.browser.close().catch(() => undefined);
|
|
}
|
|
if (state.api) {
|
|
await state.api.dispose().catch(() => undefined);
|
|
}
|
|
|
|
const summary = {
|
|
run_id: runId,
|
|
web_base: webBase,
|
|
api_base: apiBase,
|
|
health_url: healthUrl,
|
|
artifact_dir: artifactDir,
|
|
total: results.length,
|
|
passed: results.filter((item) => item.status === "PASS").length,
|
|
failed: results.filter((item) => item.status === "FAIL").length,
|
|
results,
|
|
};
|
|
fs.writeFileSync(path.join(artifactDir, "phase1-closed-loop-summary.json"), JSON.stringify(summary, null, 2), "utf8");
|
|
const markdown = [
|
|
`# Phase 1 Closed Loop E2E`,
|
|
``,
|
|
`- Run ID: \`${runId}\``,
|
|
`- Web: \`${webBase}\``,
|
|
`- API: \`${apiBase}\``,
|
|
`- Passed: ${summary.passed}/${summary.total}`,
|
|
``,
|
|
`| ID | Status | Title | Duration |`,
|
|
`|---|---|---|---:|`,
|
|
...results.map((item) => `| ${item.id} | ${item.status} | ${item.title.replace(/\|/g, "/")} | ${item.duration_ms}ms |`),
|
|
``,
|
|
].join("\n");
|
|
fs.writeFileSync(path.join(artifactDir, "phase1-closed-loop-summary.md"), markdown, "utf8");
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
if (summary.failed > 0) {
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
run()
|
|
.catch((error) => {
|
|
results.push({
|
|
id: "RUNNER",
|
|
title: "unhandled runner error",
|
|
status: "FAIL",
|
|
duration_ms: 0,
|
|
started_at: new Date().toISOString(),
|
|
error: error.message,
|
|
});
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(finalize);
|