文件
hyb-oa/scripts/qa/test-invoice-first-http.cjs
T
wangsli 229867f5fd
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
fix: 财务本人报销由总经理独立审批(09-04-reimbursement-state-gap-finance-view)
2026-09-12 20:03:29 +08:00

739 行
57 KiB
JavaScript

const assert = require("node:assert/strict");
const { execFileSync } = require("node:child_process");
const { randomUUID } = require("node:crypto");
const { readFileSync, readdirSync, mkdtempSync, rmSync } = require("node:fs");
const { tmpdir } = require("node:os");
const { createRequire } = require("node:module");
const path = require("node:path");
const { setTimeout: delay } = require("node:timers/promises");
const root = path.resolve(__dirname, "../..");
const serverRequire = createRequire(path.join(root, "server/package.json"));
const docker = (args, input) => execFileSync("docker", args, { input, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 60000 }).trim();
async function main() {
assert.equal(process.env.RUN_ISOLATED_DB_TEST, "1", "Explicit disposable database opt-in required");
const managerIsCeo = process.env.RUN_MANAGER_IS_CEO_TEST === "1";
const ownerApprovalTest = process.env.RUN_OWNER_APPROVAL_TEST === "1";
assert.ok(!ownerApprovalTest || managerIsCeo, "Owner approval checks require the small-team fixture");
const password = randomUUID();
let container;
let prisma;
let pool;
let app;
let storageDir;
let browser;
let preview;
const passed = [];
const businessBlockers = [];
try {
container = docker(["run", "-d", "--rm", "--pull=never", "--tmpfs", "/var/lib/postgresql/data", "-p", "127.0.0.1::5432", "-e", `POSTGRES_PASSWORD=${password}`, "postgres:16-alpine"]);
let ready = false;
for (let attempt = 0; attempt < 30; attempt += 1) {
try { docker(["exec", container, "pg_isready", "-U", "postgres"]); ready = true; break; } catch { await delay(1000); }
}
assert.ok(ready, "Disposable PostgreSQL readiness");
const migrationDir = path.join(root, "server/prisma/migrations");
for (const directory of readdirSync(migrationDir).sort()) {
if (directory === "migration_lock.toml") continue;
docker(["exec", "-i", container, "psql", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-U", "postgres"], readFileSync(path.join(migrationDir, directory, "migration.sql")));
}
const address = docker(["port", container, "5432/tcp"]);
assert.match(address, /^127\.0\.0\.1:\d+$/);
process.env.DATABASE_URL = `postgresql://postgres:${password}@${address}/postgres`;
({ prisma, pool } = serverRequire("./src/prismaClient").createPrismaClient());
const department = await prisma.department.create({ data: { name: "synthetic-http" } });
const employee = await prisma.employee.create({ data: {
name: "synthetic", email: "[email protected]", employee_no: "synthetic-http",
department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-09"),
bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash",
} });
const expenseType = await prisma.expenseType.create({ data: { code: "synthetic_http", name: "synthetic", category: "office", sort_order: 1 } });
let manager = managerIsCeo ? null : await prisma.employee.create({ data: {
name: "synthetic-manager", email: "[email protected]", employee_no: "synthetic-manager",
department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-09"),
bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash",
} });
const approvers = manager ? { manager } : {};
for (const role of ["ceo", "finance"]) {
approvers[role] = await prisma.employee.create({ data: {
name: `synthetic-${role}`, email: `invoice-${role}@example.invalid`, employee_no: `synthetic-${role}`,
department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-09"),
bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash",
} });
}
for (const [role, actor] of Object.entries(approvers)) {
await prisma.employeeRole.create({ data: { employee_id: actor.id, role_code: role } });
}
if (managerIsCeo) {
manager = approvers.ceo;
approvers.manager = manager;
for (const actor of [approvers.ceo, approvers.finance]) {
await prisma.employeeRole.create({ data: { employee_id: actor.id, role_code: "employee" } });
}
await prisma.employee.update({ where: { id: approvers.finance.id }, data: { direct_manager_id: manager.id } });
assert.equal(await prisma.employeeRole.count({ where: { role_code: "manager" } }), 0);
assert.equal(await prisma.employee.count(), 3);
passed.push("small team: employee and finance report to the same CEO; no manager role or separate manager account");
}
await prisma.employee.update({ where: { id: employee.id }, data: { direct_manager_id: manager.id } });
await prisma.employeeRole.create({ data: { employee_id: employee.id, role_code: "employee" } });
const loginPassword = `Synthetic!${randomUUID()}`;
const passwordHash = await serverRequire("./src/utils/crypto").hashPassword(loginPassword);
await prisma.employee.updateMany({ data: { password_hash: passwordHash, must_change_password: false } });
await prisma.workflowDefinition.create({ data: { name: "synthetic-reimbursement", entity_type: "reimbursement", definition_json: { steps: [] }, version: 1 } });
const approvalRule = await prisma.approvalRule.create({ data: { name: "synthetic-three-step", min_amount: 0, level1_role: "manager", level2_role: "ceo", level3_role: "finance", priority: 1 } });
await prisma.featureFlag.create({ data: { flag_key: "reimbursement_invoice_guard_v1", override_mode: "force_on", rollout_type: "global", department_ids: [] } });
const makeForm = () => prisma.reimbursementForm.create({ data: { form_number: `SYN-${randomUUID()}`, owner_id: employee.id, submitter_id: employee.id, department_id: department.id, reimbursement_date: new Date("2026-09-09") } });
const makeInvoice = (overrides = {}) => prisma.invoicePool.create({ data: {
owner_employee_id: employee.id, uploaded_by_employee_id: employee.id, status: "available",
storage_path: "synthetic-unused.pdf", original_name: "synthetic.pdf", mime_type: "application/pdf",
file_size: 1, file_sha256: randomUUID(), invoice_number: randomUUID(),
invoice_date: new Date("2026-09-09"), total_amount: "100.00", ocr_attempts: 1, ocr_completed_at: new Date("2026-09-09"), ...overrides,
} });
app = serverRequire("fastify")({ logger: false });
app.decorate("prisma", prisma);
storageDir = mkdtempSync(path.join(tmpdir(), "invoice-http-"));
app.decorate("config", { storageDir, storageDriver: "local", maxUploadSizeBytes: 10 * 1024 * 1024, ocrProvider: process.env.RUN_JOB_QUEUE === "1" ? "mock" : "tencent", reimbursementInvoiceGuardEnabled: true, appTimezone: "Asia/Shanghai", jwtSecret: randomUUID(), accessTokenTtl: "15m", refreshTtlSeconds: 3600, refreshCookieName: "synthetic_refresh", cookieSecure: false, loginFailMaxAttempts: 5, loginFailWindowSeconds: 300, loginLockSeconds: 60 });
await app.register(serverRequire("@fastify/cookie"));
await app.register(serverRequire("./src/plugins/auth"));
await app.register(serverRequire("./src/routes/auth"), { prefix: "/api/auth" });
app.setErrorHandler((error, request, reply) => reply.status(error.statusCode ?? 500).send({ error: { code: error.code, details: error.details } }));
const envelope = (data) => ({ requestId: "synthetic-http", data });
app.get("/api/me", { preHandler: app.authenticate }, async (request) => {
const profile = await prisma.employee.findUniqueOrThrow({ where: { id: request.employee.id }, select: { employee_no: true, email: true } });
return envelope({ id: request.employee.id, employeeNo: profile.employee_no, name: "synthetic", email: profile.email, departmentId: department.id, directManagerId: manager.id, status: "active", roleCodes: request.employee.roleCodes, notificationPushEnabled: false });
});
app.get("/api/system/navigation", async () => envelope({ workspace: { id: "synthetic", profile: "small_team_internal", orgMode: "single_company", moduleVisibility: {} }, sections: { main: [], admin: [], backup: [] }, recommendedRolePresets: [] }));
app.get("/api/notifications", async () => envelope({ items: [], total: 0, unreadCount: 0, page: 1, pageSize: 20 }));
await app.register(serverRequire("./src/routes/expenseTypes"), { prefix: "/api" });
await app.register(serverRequire("@fastify/multipart"), { limits: { fileSize: 10 * 1024 * 1024 } });
await app.register(serverRequire("./src/routes/invoicePool"), { prefix: "/api" });
await app.register(serverRequire("./src/routes/reimbursements"), { prefix: "/api" });
await app.register(serverRequire("./src/routes/workflow"), { prefix: "/api" });
if (process.env.RUN_PAYMENT_EXPERIENCE_TEST === "1") {
await app.register(serverRequire("./src/routes/paymentBatches"), { prefix: "/api" });
}
if (process.env.RUN_HANDOFF_PROJECTION_TEST === "1") {
app.config.portalHomeEnabled = true;
await prisma.featureFlag.create({ data: { flag_key: "portal_home_v1", override_mode: "force_on", rollout_type: "global", department_ids: [] } });
await app.register(serverRequire("./src/routes/portal"), { prefix: "/api" });
}
if (process.env.RUN_JOB_QUEUE === "1") {
Object.assign(app.config, { jobDriver: "pgboss", pgBossSchema: "synthetic_jobs", ocrMaxAttempts: 3, ocrPollCron: "0 0 1 1 *", delegationRevokeCron: "0 0 1 1 *", invoiceArchiveCron: "0 0 1 1 *", integrationOutboxCron: "0 0 1 1 *" });
await serverRequire("./src/jobs/pgBossJobs").startPgBossJobs(app);
}
const origin = await app.listen({ host: "127.0.0.1", port: 0 });
const tokens = new Map();
for (const actor of new Map([employee, ...Object.values(approvers)].map((actor) => [actor.id, actor])).values()) {
const login = await fetch(`${origin}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", "x-device-fingerprint": `synthetic-${actor.id}` }, body: JSON.stringify({ identifier: actor.email, password: loginPassword }) });
assert.equal(login.status, 200, "password login");
tokens.set(actor.id, (await login.json()).data.accessToken);
}
assert.equal((await fetch(`${origin}/api/me`)).status, 401);
assert.equal((await fetch(`${origin}/api/me`, { headers: { authorization: "Bearer invalid-synthetic-token" } })).status, 401);
passed.push("real password login issues tokens; absent and invalid JWT rejected");
const post = async (url, body, actor, method = "POST") => {
const token = tokens.get(actor ?? employee.id) ?? app.issueAccessToken({ id: actor, token_version: 0 });
const response = await fetch(`${origin}/api${url}`, { method, headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(15000) });
return { status: response.status, body: await response.json() };
};
const handoffCheck = async (actor, kind, entityId, expectedCount) => {
if (process.env.RUN_HANDOFF_PROJECTION_TEST !== "1") return;
const auditCount = await prisma.auditLog.count();
const getHandoffs = async () => {
const response = await fetch(`${origin}/api/portal/home`, { headers: { authorization: `Bearer ${tokens.get(actor.id)}` }, signal: AbortSignal.timeout(15000) });
assert.equal(response.status, 200, "authorized portal handoff projection");
const payload = (await response.json()).data.businessHandoffs;
assert.equal(payload.total, payload.items.length);
assert.equal(new Set(payload.items.map((item) => item.key)).size, payload.total);
assert.equal(payload.items.filter((item) => item.kind === kind && item.key.endsWith(`:${entityId}`)).length, expectedCount);
return payload;
};
assert.deepEqual(await getHandoffs(), await getHandoffs());
assert.equal(await prisma.auditLog.count(), auditCount, "handoff reads do not create audit or workflow writes");
if (browser && preview && ["reimbursement_edit", "payment_review"].includes(kind)
&& [employee.id, approvers.ceo.id].includes(actor.id)) {
const clientRequire = createRequire(path.join(root, "client/package.json"));
const { expect } = clientRequire("@playwright/test");
const uiOrigin = `http://127.0.0.1:${preview.httpServer.address().port}`;
const context = await browser.newContext({ serviceWorkers: "block", viewport: { width: 390, height: 844 } });
try {
const fingerprint = `synthetic-handoff-${randomUUID()}`;
await context.addCookies([{ name: "hyb_oa_device_fingerprint", value: fingerprint, url: uiOrigin }]);
const login = await context.request.post(`${uiOrigin}/api/auth/login`, {
headers: { "x-device-fingerprint": fingerprint }, data: { identifier: actor.email, password: loginPassword },
});
assert.equal(login.status(), 200, "non-finance mobile real password login");
const page = await context.newPage();
page.setDefaultTimeout(20000);
const loaded = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/portal/home");
await page.goto(`${uiOrigin}/mobile`);
assert.equal((await loaded).status(), 200, "non-finance mobile cold entry actually loads handoffs");
const href = kind === "reimbursement_edit" ? `/mobile/reimbursements/${entityId}` : `/mobile/payment-batches?id=${entityId}`;
const row = page.getByTestId("mobile-business-handoffs").locator(`a[href="${href}"]`);
await expect(row).toHaveCount(expectedCount);
if (expectedCount) await expect(row).toBeVisible();
await expect(page.getByTestId("mobile-finance-summary")).toHaveCount(0);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2), true);
passed.push(`handoffs: non-finance mobile cold entry with real auth/HTTP ${kind} count=${expectedCount}, no finance card`);
if (kind === "reimbursement_edit" && expectedCount === 1 && actor.id === employee.id) {
await page.evaluate(() => { Reflect.set(window, "__realHandoffSwitchDocument", "same-document"); });
await page.locator('a[href="/mobile/me"]').last().click();
await page.getByRole("button", { name: "退出登录", exact: true }).click();
await expect(page).toHaveURL(/\/login/);
await page.getByTestId("login-identifier").fill(approvers.ceo.email);
await page.getByTestId("login-password").fill(loginPassword);
const secondLogin = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/auth/login");
const secondProfile = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/me");
await page.getByTestId("login-submit").click();
assert.equal((await secondLogin).status(), 200, "second actor UI password login");
const profileResponse = await secondProfile;
assert.equal(profileResponse.status(), 200, "second actor profile loaded");
assert.equal((await profileResponse.json()).data.id === approvers.ceo.id, true, "second actor identity matches");
await expect(page).toHaveURL(/\/mobile\/me$/);
await expect(page.getByTestId("mobile-me-profile-copy")).toContainText(approvers.ceo.employee_no);
let releaseResponse;
let secondActorResponseVerified = false;
const responseGate = new Promise((resolve) => { releaseResponse = resolve; });
await page.route("**/api/portal/home", async (route) => {
const response = await route.fetch();
assert.equal(response.status(), 200, "second actor real portal response");
const data = (await response.json()).data.businessHandoffs;
assert.equal(data.items.some((item) => item.href === `/reimbursements/${entityId}`), false);
secondActorResponseVerified = true;
await responseGate;
await route.fulfill({ response });
});
try {
await page.locator('a[href="/mobile"]').last().click();
await expect.poll(() => secondActorResponseVerified).toBe(true);
assert.equal(await page.evaluate(() => Reflect.get(window, "__realHandoffSwitchDocument")), "same-document");
await expect(row).toHaveCount(0);
const refreshed = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/portal/home");
releaseResponse();
await refreshed;
await expect(row).toHaveCount(0);
passed.push("handoffs: same-document employee-to-CEO mobile switch with real password login and isolated HTTP, no former actor handoff while response is held");
} finally {
releaseResponse();
await page.unrouteAll({ behavior: "wait" });
}
}
} finally {
await context.close();
}
}
};
const bind = (invoice, form, actor) => post(`/invoice-pool/${invoice.id}/bind`, { reimbursementId: form.id, createItem: true }, actor);
const objects = [
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R >>",
"<< /Length 0 >>\nstream\n\nendstream",
];
let pdf = "%PDF-1.4\n";
const offsets = [0];
for (const [index, content] of objects.entries()) {
offsets.push(Buffer.byteLength(pdf));
pdf += `${index + 1} 0 obj\n${content}\nendobj\n`;
}
const xrefOffset = Buffer.byteLength(pdf);
pdf += `xref\n0 5\n0000000000 65535 f \n${offsets.slice(1).map((offset) => `${String(offset).padStart(10, "0")} 00000 n \n`).join("")}trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
const upload = async () => {
const body = new FormData();
body.append("file", new Blob([pdf], { type: "application/pdf" }), "synthetic-blank.pdf");
body.append("ocr_type", "vat");
const response = await fetch(`${origin}/api/invoice-pool/upload`, { method: "POST", headers: { authorization: `Bearer ${tokens.get(employee.id)}` }, body, signal: AbortSignal.timeout(15000) });
assert.equal(response.status, 200);
return (await response.json()).data;
};
const uploaded = await upload();
assert.equal(uploaded.status, "pending_ocr");
const uploadedRecord = await prisma.invoicePool.findUnique({ where: { id: uploaded.invoicePoolId } });
assert.equal(uploadedRecord.linked_reimbursement_id, null);
assert.deepEqual(readFileSync(path.join(storageDir, uploadedRecord.storage_path)), Buffer.from(pdf));
assert.equal((await upload()).invoicePoolId, uploaded.invoicePoolId);
assert.equal(await prisma.invoicePool.count(), 1);
passed.push("multipart PDF upload persists unbound pending OCR; duplicate upload reuses invoice without extra record");
if (process.env.RUN_BROWSER_TEST === "1") {
const clientRequire = createRequire(path.join(root, "client/package.json"));
const { pathToFileURL } = require("node:url");
const vite = await import(pathToFileURL(clientRequire.resolve("vite")).href);
preview = await (vite.default ?? vite).preview({ configFile: false, root: path.join(root, "client"), preview: { host: "127.0.0.1", port: 0, proxy: { "/api": origin } } });
const uiOrigin = `http://127.0.0.1:${preview.httpServer.address().port}`;
app.config.appOrigin = uiOrigin;
const { chromium, expect } = clientRequire("@playwright/test");
browser = await chromium.launch({ headless: true });
const runBrowserFlow = async (mobile) => {
const context = await browser.newContext({ serviceWorkers: "block", viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 960 } });
const fingerprint = `synthetic-browser-${mobile ? "mobile" : "desktop"}`;
await context.addCookies([{ name: "hyb_oa_device_fingerprint", value: fingerprint, url: uiOrigin }]);
const browserLogin = await context.request.post(`${uiOrigin}/api/auth/login`, { headers: { "x-device-fingerprint": fingerprint }, data: { identifier: employee.email, password: loginPassword } });
assert.equal(browserLogin.status(), 200);
const page = await context.newPage();
page.setDefaultTimeout(20000);
const browserForm = await makeForm();
await page.goto(`${uiOrigin}${mobile ? "/mobile" : ""}/reimbursements/${browserForm.id}`);
await page.getByTestId("reimbursement-invoice-add").click();
const browserPdf = pdf.replace("300 200", mobile ? "302 200" : "301 200");
await page.getByTestId("reimbursement-invoice-upload-file").setInputFiles({ name: "synthetic-blank.pdf", mimeType: "application/pdf", buffer: Buffer.from(browserPdf) });
const [uploadResponse] = await Promise.all([
page.waitForResponse((response) => response.request().method() === "POST" && response.url().endsWith("/api/invoice-pool/upload")),
page.getByTestId("reimbursement-invoice-upload-submit").click(),
]);
assert.equal(uploadResponse.status(), 200);
const browserInvoiceId = (await uploadResponse.json()).data.invoicePoolId;
if (process.env.RUN_JOB_QUEUE === "1") {
await expect.poll(async () => (await prisma.invoicePool.findUnique({ where: { id: browserInvoiceId } })).status, { timeout: 20000 }).toBe("available");
await expect.poll(async () => Number((await pool.query("SELECT count(*) FROM synthetic_jobs.job WHERE name = 'ocr.invoice' AND data->>'invoiceId' = $1 AND state = 'completed'", [browserInvoiceId])).rows[0].count), { timeout: 20000 }).toBe(1);
} else {
const workerConfig = { ...app.config, ocrProvider: "mock", ocrMaxAttempts: 3 };
const workerResult = await serverRequire("./src/jobs/ocrJob").runOcrForInvoiceId(prisma, workerConfig, app.log, browserInvoiceId, "synthetic-browser-worker");
assert.equal(workerResult.available, 1);
}
await expect(page.getByRole("dialog").getByRole("checkbox").first()).toBeChecked({ timeout: 20000 });
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: browserForm.id } }), 0);
await page.getByRole("dialog").getByRole("button", { name: mobile ? "添加到报销单 1" : "添加到报销单", exact: true }).click();
if (mobile) await page.getByRole("dialog").getByRole("button", { name: "返回报销单" }).click();
await expect(page.getByText("费用分类:1 条待补充")).toBeVisible();
await page.getByRole("checkbox", { name: /明细 1/ }).check();
await page.getByLabel("批量费用类型").selectOption(expenseType.id);
await page.getByRole("button", { name: "设置所选 1 条分类" }).click();
await expect(page.getByText("费用分类:1 条待补充")).toHaveCount(0);
await page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: "编辑", exact: true }).first().click();
await (mobile ? page.getByLabel("说明", { exact: true }) : page.getByTestId("reimbursement-item-description")).fill("synthetic browser purpose");
const [savedItemResponse] = await Promise.all([
page.waitForResponse((response) => response.request().method() === "PATCH" && response.url().includes(`/reimbursements/${browserForm.id}/items/`)),
(mobile ? page.getByRole("button", { name: "保存", exact: true }) : page.getByTestId("reimbursement-item-save")).click(),
]);
assert.equal(savedItemResponse.status(), 200);
assert.equal((await prisma.reimbursementItem.findFirst({ where: { reimbursement_id: browserForm.id } })).description, "synthetic browser purpose");
await page.reload();
await expect(page.getByTestId("reimbursement-approval-evidence").getByText("synthetic browser purpose").first()).toBeVisible();
await page.getByTestId("reimbursement-submit").click();
await expect.poll(async () => (await prisma.reimbursementForm.findUnique({ where: { id: browserForm.id } })).status, { timeout: 15000 }).toBe("reviewing");
const browserWorkflow = await prisma.workflowInstance.findFirst({ where: { reimbursement_id: browserForm.id } });
assert.equal(await prisma.workflowTask.count({ where: { instance_id: browserWorkflow.id, assignee_id: manager.id, status: "pending" } }), 1);
for (const role of ["manager", "ceo", "finance"]) {
const approvalTask = await prisma.workflowTask.findFirst({ where: { instance_id: browserWorkflow.id, status: "pending" } });
assert.ok(approvalTask);
assert.equal((await post(`/workflow/tasks/${approvalTask.id}/approve`, {}, approvers[role].id)).status, 200);
}
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: browserForm.id } })).status, "approved");
await page.reload();
await expect(page.getByTestId("reimbursement-approval-evidence")).toBeVisible();
await expect(page.getByTestId("reimbursement-invoice-add")).toHaveCount(0);
await context.close();
passed.push(`${mobile ? "mobile" : "desktop"} browser upload -> real OCR worker with mock provider -> select -> classify/edit -> submit -> three HTTP approvals -> read-only page`);
};
await runBrowserFlow(false);
await runBrowserFlow(true);
}
const form = await makeForm();
const invoice = await makeInvoice();
const repeated = await Promise.all(Array.from({ length: 4 }, () => bind(invoice, form)));
assert.deepEqual(repeated.map((result) => result.status), [200, 200, 200, 200]);
let items = await prisma.reimbursementItem.findMany({ where: { reimbursement_id: form.id } });
assert.equal(items.length, 1);
assert.equal(items[0].expense_type_id, null);
assert.equal(items[0].amount.toFixed(2), "100.00");
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).total_amount.toFixed(2), "100.00");
passed.push("same-form concurrent retry creates one persisted draft item");
const missingClassification = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(missingClassification.status, 400);
assert.equal(missingClassification.body.error.details.reasonCode, "reimbursement.items_expense_type_required");
const classification = await post(`/reimbursements/${form.id}/items/classify`, { itemIds: [items[0].id], expenseTypeId: expenseType.id });
assert.equal(classification.status, 200);
const missingDescription = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(missingDescription.body.error.details.reasonCode, "reimbursement.items_description_required");
assert.equal((await post(`/reimbursements/${form.id}/items/${items[0].id}`, { description: "synthetic edited purpose", amount: "90.00" }, null, "PATCH")).status, 200);
assert.equal((await bind(invoice, form)).status, 200);
const edited = await prisma.reimbursementItem.findUnique({ where: { id: items[0].id } });
assert.equal(edited.expense_type_id, expenseType.id);
assert.equal(edited.description, "synthetic edited purpose");
assert.equal(edited.amount.toFixed(2), "90.00");
passed.push("batch classification persists and retry preserves edits");
const submit = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(submit.status, 200, `submit: ${submit.status} ${submit.body.error?.details?.reasonCode ?? submit.body.error?.code ?? ""}`);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "reviewing");
const workflow = await prisma.workflowInstance.findFirst({ where: { reimbursement_id: form.id } });
assert.ok(workflow);
assert.equal(await prisma.workflowTask.count({ where: { instance_id: workflow.id, assignee_id: manager.id } }), 1);
assert.equal((await post(`/reimbursements/${form.id}/submit`, {})).status, 409);
assert.equal(await prisma.workflowInstance.count({ where: { reimbursement_id: form.id } }), 1);
passed.push("empty draft to classified edited submission assigns manager once");
const firstTask = await prisma.workflowTask.findFirst({ where: { instance_id: workflow.id, status: "pending" } });
assert.equal((await post(`/workflow/tasks/${firstTask.id}/approve`, {})).status, 403);
assert.equal((await post(`/workflow/tasks/${firstTask.id}/return`, { comment: "synthetic correction required" }, manager.id)).status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "draft");
await handoffCheck(employee, "reimbursement_edit", form.id, 1);
await handoffCheck(approvers.finance, "reimbursement_edit", form.id, 0);
assert.equal((await prisma.invoicePool.findUnique({ where: { id: invoice.id } })).linked_reimbursement_id, form.id);
assert.equal((await post(`/reimbursements/${form.id}/items/${items[0].id}`, { description: "synthetic corrected purpose" }, null, "PATCH")).status, 200);
const returnedResubmit = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(returnedResubmit.status, 200);
await handoffCheck(employee, "reimbursement_edit", form.id, 0);
const rejectTask = await prisma.workflowTask.findFirst({ where: { instance_id: returnedResubmit.body.data.workflowInstanceId, status: "pending" } });
assert.equal((await post(`/workflow/tasks/${rejectTask.id}/reject`, { comment: "synthetic rejected correction" }, manager.id)).status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "rejected");
assert.equal((await post(`/reimbursements/${form.id}/items/${items[0].id}`, { description: "synthetic rejected item corrected" }, null, "PATCH")).status, 200);
const resubmit = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(resubmit.status, 200);
const newWorkflowId = resubmit.body.data.workflowInstanceId;
assert.notEqual(newWorkflowId, workflow.id);
const approvalActors = [];
for (const role of ["manager", "ceo", "finance"]) {
const task = await prisma.workflowTask.findFirst({ where: { instance_id: newWorkflowId, status: "pending" } });
assert.ok(task, `pending task for ${role}`);
if (role === "manager") assert.equal(task.assignee_id, manager.id);
else { assert.equal(task.assignee_id, null); assert.equal(task.assignee_role, role); }
if (role === "finance" && browser && preview) {
await prisma.workflowHistory.updateMany({ where: { instance_id: newWorkflowId, action: "approve" }, data: { comment: "synthetic historical comment ".repeat(12) } });
const clientRequire = createRequire(path.join(root, "client/package.json"));
const { expect } = clientRequire("@playwright/test");
const uiOrigin = `http://127.0.0.1:${preview.httpServer.address().port}`;
for (const mobile of [false, true]) {
const context = await browser.newContext({ serviceWorkers: "block", viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 960 } });
try {
const fingerprint = `synthetic-approval-layout-${mobile}`;
await context.addCookies([{ name: "hyb_oa_device_fingerprint", value: fingerprint, url: uiOrigin }]);
const login = await context.request.post(`${uiOrigin}/api/auth/login`, { headers: { "x-device-fingerprint": fingerprint }, data: { identifier: approvers.finance.email, password: loginPassword } });
assert.equal(login.status(), 200);
const page = await context.newPage();
await page.goto(`${uiOrigin}${mobile ? "/mobile/approvals" : "/workflow/tasks"}/${task.id}`);
const progress = page.getByTestId("reimbursement-approval-processing-progress");
await expect(progress).toHaveCount(1);
await expect(page.getByTestId("reimbursement-workflow-history")).toContainText("synthetic historical comment");
if (!mobile) {
const actions = page.getByTestId("workflow-task-action-panel");
const actionBounds = await actions.boundingBox();
const progressBounds = await progress.boundingBox();
assert.ok(progressBounds.y > actionBounds.y + actionBounds.height);
assert.ok(Math.abs(progressBounds.x - actionBounds.x) < 35);
assert.notEqual(await actions.locator("..").evaluate((element) => getComputedStyle(element).position), "sticky");
} else {
await expect(page.getByTestId("mobile-approvals-sticky-actions")).toBeVisible();
}
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2), true);
} finally { await context.close(); }
}
passed.push("approval: real finance desktop/mobile evidence, current progress and synthetic long-history fixture; non-overlapping aside and safe-area action");
}
const approval = await post(`/workflow/tasks/${task.id}/approve`, {}, approvers[role].id);
assert.equal(approval.status, 200, `${role} approval: ${approval.status} ${approval.body.error?.code ?? ""}`);
approvalActors.push(approvers[role].id);
assert.equal((await post(`/workflow/tasks/${task.id}/approve`, {}, approvers[role].id)).status, 409);
}
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "approved");
assert.equal(await prisma.workflowTask.count({ where: { instance_id: newWorkflowId, status: "pending" } }), 0);
assert.equal((await prisma.workflowInstance.findUnique({ where: { id: newWorkflowId } })).current_status, "approved");
passed.push("owner cannot self-approve; return/edit/resubmit and manager/ceo/finance approval complete without payment");
if (managerIsCeo) {
assert.deepEqual(approvalActors, [approvers.ceo.id, approvers.ceo.id, approvers.finance.id]);
passed.push("small team: same CEO completes separate manager and CEO steps; no automatic person-level deduplication");
}
if (process.env.RUN_PAYMENT_EXPERIENCE_TEST === "1") {
await require("./test-payment-experience.cjs")({ app, prisma, employee, approvers, form, post, origin, tokens, serverRequire, passed, browser, preview, loginPassword, handoffCheck });
}
const competingForms = await Promise.all([makeForm(), makeForm()]);
const sharedInvoice = await makeInvoice();
const competition = await Promise.all(competingForms.map((candidate) => bind(sharedInvoice, candidate)));
assert.deepEqual(competition.map((result) => result.status).sort(), [200, 409]);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: { in: competingForms.map((candidate) => candidate.id) } } }), 1);
passed.push("cross-form competition has one winner and no orphan item");
const multipleForm = await makeForm();
const equalInvoices = await Promise.all([makeInvoice(), makeInvoice()]);
assert.deepEqual((await Promise.all(equalInvoices.map((candidate) => bind(candidate, multipleForm)))).map((result) => result.status), [200, 200]);
const multipleItems = await prisma.reimbursementItem.findMany({ where: { reimbursement_id: multipleForm.id } });
assert.equal(multipleItems.length, 2);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: multipleForm.id } })).total_amount.toFixed(2), "200.00");
const invalidBatch = await post(`/reimbursements/${multipleForm.id}/items/classify`, { itemIds: [multipleItems[0].id, items[0].id], expenseTypeId: expenseType.id });
assert.equal(invalidBatch.status, 409);
assert.equal((await prisma.reimbursementItem.findUnique({ where: { id: multipleItems[0].id } })).expense_type_id, null);
const additionalInvoice = await makeInvoice();
assert.equal((await post(`/invoice-pool/${additionalInvoice.id}/bind`, { reimbursementId: multipleForm.id, linkedItemId: multipleItems[0].id })).status, 200);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: multipleForm.id } }), 2);
assert.equal((await prisma.reimbursementItem.findUnique({ where: { id: multipleItems[0].id } })).amount.toFixed(2), "100.00");
passed.push("equal-amount invoices stay separate; existing-item attachment preserves amount; invalid classification is atomic");
const emptyForm = await makeForm();
const incompleteInvoice = await makeInvoice({ total_amount: null });
assert.equal((await bind(incompleteInvoice, emptyForm)).status, 409);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: emptyForm.id } }), 0);
assert.equal((await prisma.invoicePool.findUnique({ where: { id: incompleteInvoice.id } })).status, "available");
assert.equal((await bind(await makeInvoice(), emptyForm, randomUUID())).status, 403);
await prisma.reimbursementForm.update({ where: { id: emptyForm.id }, data: { status: "approved" } });
assert.equal((await bind(await makeInvoice(), emptyForm)).status, 409);
passed.push("missing OCR amount, nonparticipant and immutable state reject without items");
const rollbackForm = await makeForm();
const rollbackInvoice = await makeInvoice();
await prisma.$executeRawUnsafe("CREATE FUNCTION synthetic_reject_bind() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.status = 'bound' THEN RAISE EXCEPTION 'synthetic injected failure'; END IF; RETURN NEW; END $$");
await prisma.$executeRawUnsafe("CREATE TRIGGER synthetic_reject_bind BEFORE UPDATE ON invoice_pool FOR EACH ROW EXECUTE FUNCTION synthetic_reject_bind()");
assert.equal((await bind(rollbackInvoice, rollbackForm)).status, 500);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: rollbackForm.id } }), 0);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: rollbackForm.id } })).total_amount.toFixed(2), "0.00");
assert.equal((await prisma.invoicePool.findUnique({ where: { id: rollbackInvoice.id } })).status, "available");
await prisma.$executeRawUnsafe("DROP TRIGGER synthetic_reject_bind ON invoice_pool");
assert.equal((await bind(rollbackInvoice, rollbackForm)).status, 200);
passed.push("database failure rolls back item, occupancy and total; retry succeeds");
const blocker = await pool.connect();
const waitForLockedRequests = async (count) => {
for (let attempt = 0; attempt < 100; attempt += 1) {
const result = await pool.query("SELECT count(*)::int AS count FROM pg_stat_activity WHERE datname = current_database() AND wait_event_type = 'Lock' AND query LIKE '%reimbursement_forms%'");
if (result.rows[0].count >= count) return;
await delay(20);
}
assert.fail("synthetic lock barrier timed out");
};
let retryRequest;
let unbindRequest;
try {
await blocker.query("BEGIN");
await blocker.query("SELECT id FROM reimbursement_forms WHERE id = $1 FOR UPDATE", [rollbackForm.id]);
retryRequest = bind(rollbackInvoice, rollbackForm);
await waitForLockedRequests(1);
unbindRequest = post(`/invoice-pool/${rollbackInvoice.id}/unbind`, {});
await waitForLockedRequests(2);
} finally {
await blocker.query("ROLLBACK");
blocker.release();
}
const simultaneous = await Promise.all([retryRequest, unbindRequest]);
assert.deepEqual(simultaneous.map((result) => result.status), [200, 200], "retry and unbind must not deadlock");
passed.push("create retry and legacy unbind share a consistent lock order");
if (process.env.RUN_RETAINED_DELETE_TEST === "1") {
await require("./test-reimbursement-retained-delete.cjs")({
prisma, pool, app, origin, tokens, employee, manager, expenseType,
makeForm, makeInvoice, bind, post, uploadedRecord, storageDir, passed,
root, browser, preview, loginPassword,
});
}
if (ownerApprovalTest) {
await prisma.approvalRule.update({ where: { id: approvalRule.id }, data: { owner_self_fallback_role: "finance" } });
const prepareOwnerForm = async (owner) => {
const ownedForm = await makeForm();
const ownedInvoice = await makeInvoice();
assert.equal((await bind(ownedInvoice, ownedForm)).status, 200);
const item = await prisma.reimbursementItem.findFirst({ where: { reimbursement_id: ownedForm.id } });
await prisma.reimbursementItem.update({ where: { id: item.id }, data: { expense_type_id: expenseType.id, description: "synthetic owner approval" } });
await prisma.invoicePool.update({ where: { id: ownedInvoice.id }, data: { owner_employee_id: owner.id, uploaded_by_employee_id: owner.id } });
await prisma.reimbursementForm.update({ where: { id: ownedForm.id }, data: { owner_id: owner.id, submitter_id: owner.id } });
return ownedForm;
};
const ceoForm = await prepareOwnerForm(approvers.ceo);
const ceoSubmitted = await post(`/reimbursements/${ceoForm.id}/submit`, {}, approvers.ceo.id);
assert.equal(ceoSubmitted.status, 200, "CEO owner submits with explicit finance fallback");
const instanceId = ceoSubmitted.body.data.workflowInstanceId;
const tasks = await prisma.workflowTask.findMany({ where: { instance_id: instanceId } });
assert.equal(tasks.length, 1, "CEO own chain collapses to one finance step");
assert.equal(tasks[0].assignee_role, "finance");
assert.equal((await post(`/workflow/tasks/${tasks[0].id}/approve`, {}, approvers.ceo.id)).status, 403);
assert.equal((await post(`/workflow/tasks/${tasks[0].id}/approve`, {}, approvers.finance.id)).status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: ceoForm.id } })).status, "approved");
passed.push("CEO owner with finance fallback: exactly one finance task, owner rejected, finance approves");
const financeForm = await prepareOwnerForm(approvers.finance);
const financeSubmitted = await post(`/reimbursements/${financeForm.id}/submit`, {}, approvers.finance.id);
assert.equal(financeSubmitted.status, 200, "finance owner submits for one CEO approval");
const originalFinanceInstanceId = financeSubmitted.body.data.workflowInstanceId;
const returnedTask = await prisma.workflowTask.findFirst({ where: { instance_id: originalFinanceInstanceId, status: "pending" } });
assert.equal(returnedTask.assignee_role, "ceo");
assert.equal((await post(`/workflow/tasks/${returnedTask.id}/return`, { comment: "synthetic finance correction" }, approvers.ceo.id)).status, 200);
const financeResubmitted = await post(`/reimbursements/${financeForm.id}/submit`, {}, approvers.finance.id);
assert.equal(financeResubmitted.status, 200);
const financeInstanceId = financeResubmitted.body.data.workflowInstanceId;
assert.notEqual(financeInstanceId, originalFinanceInstanceId);
assert.equal(await prisma.workflowTask.count({ where: { instance_id: originalFinanceInstanceId } }), 1);
const selfTask = await prisma.workflowTask.findFirst({ where: { instance_id: financeInstanceId, status: "pending" } });
assert.ok(selfTask);
assert.equal(selfTask.assignee_role, "ceo");
assert.equal(await prisma.workflowTask.count({ where: { instance_id: financeInstanceId } }), 1);
const selfApproval = await post(`/workflow/tasks/${selfTask.id}/approve`, {}, approvers.finance.id);
assert.equal(selfApproval.status, 403);
assert.equal((await post(`/workflow/tasks/${selfTask.id}/approve`, {}, approvers.ceo.id)).status, 200);
assert.equal((await post(`/workflow/tasks/${selfTask.id}/approve`, {}, approvers.ceo.id)).status, 409);
assert.equal(await prisma.workflowTask.count({ where: { instance_id: financeInstanceId, status: "pending" } }), 0);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: financeForm.id } })).status, "approved");
passed.push("finance owner: return/resubmit preserves history; one CEO approval completes reimbursement, owner denied, replay denied, no finance task");
if (process.env.RUN_PAYMENT_EXPERIENCE_TEST === "1") {
await require("./test-payment-experience.cjs")({
app, prisma, employee, approvers, form: financeForm, payee: approvers.finance,
fixtureCode: "SYN-PAY-FINANCE", post, origin, tokens, serverRequire, passed,
browser, preview, loginPassword, handoffCheck,
});
passed.push("finance owner payment: finance creates own batch, cannot review, CEO separately reviews, synthetic export/receipt only");
}
for (const unavailable of ["inactive", "archived", "submitter", "missing_role"]) {
const blockedForm = await prepareOwnerForm(approvers.finance);
try {
if (unavailable === "inactive") await prisma.employee.update({ where: { id: approvers.ceo.id }, data: { status: "archived" } });
if (unavailable === "archived") await prisma.employee.update({ where: { id: approvers.ceo.id }, data: { archived_at: new Date() } });
if (unavailable === "submitter") await prisma.reimbursementForm.update({ where: { id: blockedForm.id }, data: { submitter_id: approvers.ceo.id } });
if (unavailable === "missing_role") await prisma.employeeRole.deleteMany({ where: { employee_id: approvers.ceo.id, role_code: "ceo" } });
for (const budgetEnabled of [false, true]) {
await prisma.featureFlag.upsert({
where: { flag_key: "budget_precheck_v1" },
create: { flag_key: "budget_precheck_v1", override_mode: budgetEnabled ? "force_on" : "force_off", rollout_type: "global", department_ids: [] },
update: { override_mode: budgetEnabled ? "force_on" : "force_off" },
});
const precheck = await post(`/reimbursements/${blockedForm.id}/precheck`, {}, approvers.finance.id);
assert.equal(precheck.status, 200);
assert.equal(precheck.body.data.decision, "block");
assert.ok(precheck.body.data.blockingReasons.some((reason) => reason.code === "finance_owner_ceo_unavailable"));
assert.equal((await post(`/reimbursements/${blockedForm.id}/submit`, {}, approvers.finance.id)).status, 409);
assert.equal(await prisma.workflowInstance.count({ where: { reimbursement_id: blockedForm.id } }), 0);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: blockedForm.id } })).status, "draft");
}
} finally {
await prisma.employee.update({ where: { id: approvers.ceo.id }, data: { status: "active", archived_at: null } });
if (unavailable === "missing_role") await prisma.employeeRole.create({ data: { employee_id: approvers.ceo.id, role_code: "ceo" } });
await prisma.featureFlag.update({ where: { flag_key: "budget_precheck_v1" }, data: { override_mode: "force_off" } });
}
}
passed.push("finance owner: inactive/archived/submitter/missing-role CEO blocks precheck and submission without pending workflow");
for (const additionalRole of ["second_finance", "owner_ceo"]) {
const actorId = additionalRole === "second_finance" ? employee.id : approvers.finance.id;
const roleCode = additionalRole === "second_finance" ? "finance" : "ceo";
await prisma.employeeRole.create({ data: { employee_id: actorId, role_code: roleCode } });
try {
const extraForm = await prepareOwnerForm(approvers.finance);
const submitted = await post(`/reimbursements/${extraForm.id}/submit`, {}, approvers.finance.id);
assert.equal(submitted.status, 200);
const extraTasks = await prisma.workflowTask.findMany({ where: { instance_id: submitted.body.data.workflowInstanceId } });
assert.equal(extraTasks.length, 1);
assert.equal(extraTasks[0].assignee_role, "ceo");
if (additionalRole === "owner_ceo") {
const denied = await post(`/workflow/tasks/${extraTasks[0].id}/approve`, {}, approvers.finance.id);
assert.equal(denied.status, 409);
assert.equal(denied.body.error.details.workflow, "self_approval_not_allowed");
} else {
assert.equal((await post(`/workflow/tasks/${extraTasks[0].id}/approve`, {}, employee.id)).status, 403);
}
assert.equal((await post(`/workflow/tasks/${extraTasks[0].id}/approve`, {}, approvers.ceo.id)).status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: extraForm.id } })).status, "approved");
} finally {
await prisma.employeeRole.deleteMany({ where: { employee_id: actorId, role_code: roleCode } });
}
}
passed.push("finance owner: second finance does not change CEO-only rule; finance+CEO applicant still denied self-approval");
if (browser && preview) {
const { expect } = createRequire(path.join(root, "client/package.json"))("@playwright/test");
const uiOrigin = `http://127.0.0.1:${preview.httpServer.address().port}`;
for (const mobile of [false, true]) {
const browserForm = await prepareOwnerForm(approvers.finance);
for (const actor of [approvers.finance, approvers.ceo]) {
const context = await browser.newContext({ serviceWorkers: "block", viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 960 } });
try {
const fingerprint = `synthetic-owner-${mobile}-${actor.id}`;
await context.addCookies([{ name: "hyb_oa_device_fingerprint", value: fingerprint, url: uiOrigin }]);
const login = await context.request.post(`${uiOrigin}/api/auth/login`, {
headers: { "x-device-fingerprint": fingerprint },
data: { identifier: actor.email, password: loginPassword },
});
assert.equal(login.status(), 200);
const page = await context.newPage();
if (actor.id === approvers.finance.id) {
await page.goto(`${uiOrigin}${mobile ? "/mobile" : ""}/reimbursements/${browserForm.id}`);
const response = page.waitForResponse((result) => result.url().endsWith(`/reimbursements/${browserForm.id}/submit`) && result.request().method() === "POST");
const precheckResponse = page.waitForResponse((result) => result.url().endsWith(`/reimbursements/${browserForm.id}/precheck`) && result.request().method() === "POST");
await page.getByTestId("reimbursement-submit").click();
const precheck = await (await precheckResponse).json();
assert.notEqual(precheck.data.decision, "block");
if (precheck.data.decision !== "pass") {
const confirm = page.getByRole("button", { name: "确认提交", exact: true });
await expect(confirm).toBeVisible();
await confirm.click();
}
assert.equal((await response).status(), 200);
} else {
const workflow = await prisma.workflowInstance.findFirst({ where: { reimbursement_id: browserForm.id, current_status: "pending" } });
const task = await prisma.workflowTask.findFirst({ where: { instance_id: workflow.id, status: "pending" } });
assert.equal(task.assignee_role, "ceo");
await page.goto(`${uiOrigin}${mobile ? "/mobile/approvals" : "/workflow/tasks"}/${task.id}`);
await page.getByTestId(mobile ? "mobile-approvals-approve" : "workflow-task-approve").click();
await expect(page.getByTestId("workflow-action-confirm-dialog")).toBeVisible();
const response = page.waitForResponse((result) => result.url().endsWith(`/workflow/tasks/${task.id}/approve`) && result.request().method() === "POST");
await page.getByTestId("workflow-action-confirm-submit").click();
assert.equal((await response).status(), 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: browserForm.id } })).status, "approved");
assert.equal(await prisma.workflowTask.count({ where: { instance_id: workflow.id } }), 1);
}
} finally { await context.close(); }
}
}
passed.push("finance owner: real desktop/mobile finance submits after precheck, CEO approves once via UI and no further task is created");
}
const transferForm = await prepareOwnerForm(approvers.finance);
const transferSubmitted = await post(`/reimbursements/${transferForm.id}/submit`, {}, approvers.finance.id);
assert.equal(transferSubmitted.status, 200);
const transferTask = await prisma.workflowTask.findFirst({ where: { instance_id: transferSubmitted.body.data.workflowInstanceId, status: "pending" } });
const assertNoPendingActions = async (actor) => {
const response = await fetch(`${origin}/api/reimbursements?scope=all&pageSize=100`, {
headers: { authorization: `Bearer ${tokens.get(actor.id)}` }, signal: AbortSignal.timeout(15000),
});
assert.equal(response.status, 200);
const source = (await response.json()).data.items.find((item) => item.id === transferForm.id);
assert.ok(source);
assert.equal(source.pendingWorkflow, null, "non-CEO has no reimbursement list approval prompt");
};
await prisma.employeeRole.create({ data: { employee_id: employee.id, role_code: "finance" } });
const transferBody = { toApproverId: employee.id, reason: "synthetic transfer safety validation" };
const deniedTransfer = await post(`/workflow/tasks/${transferTask.id}/transfer`, transferBody, approvers.ceo.id);
assert.equal(deniedTransfer.status, 403);
assert.equal(deniedTransfer.body.error.details.reasonCode, "reimbursement.finance_owner_ceo_required");
assert.equal((await prisma.workflowTask.findUnique({ where: { id: transferTask.id } })).assignee_role, "ceo");
await prisma.employeeRole.create({ data: { employee_id: employee.id, role_code: "ceo" } });
assert.equal((await post(`/workflow/tasks/${transferTask.id}/transfer`, transferBody, approvers.ceo.id)).status, 200);
await prisma.employeeRole.deleteMany({ where: { employee_id: employee.id, role_code: "ceo" } });
await assertNoPendingActions(employee);
assert.equal((await post(`/workflow/tasks/${transferTask.id}/approve`, {}, employee.id)).status, 403);
const delegatedGrant = await prisma.delegationGrant.create({ data: {
principal_employee_id: employee.id, delegate_employee_id: approvers.ceo.id, scope: "all",
valid_from: new Date(Date.now() - 60000),
} });
assert.equal((await post(`/workflow/tasks/${transferTask.id}/approve`, {}, approvers.ceo.id)).status, 403);
await prisma.employeeRole.create({ data: { employee_id: employee.id, role_code: "ceo" } });
await prisma.employeeRole.create({ data: { employee_id: approvers.ceo.id, role_code: "finance" } });
await prisma.employeeRole.deleteMany({ where: { employee_id: approvers.ceo.id, role_code: "ceo" } });
await assertNoPendingActions(approvers.ceo);
assert.equal((await post(`/workflow/tasks/${transferTask.id}/approve`, {}, approvers.ceo.id)).status, 403);
await prisma.employeeRole.create({ data: { employee_id: approvers.ceo.id, role_code: "ceo" } });
await prisma.employeeRole.deleteMany({ where: { employee_id: approvers.ceo.id, role_code: "finance" } });
assert.equal((await post(`/workflow/tasks/${transferTask.id}/approve`, {}, approvers.ceo.id)).status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: transferForm.id } })).status, "approved");
await prisma.delegationGrant.delete({ where: { id: delegatedGrant.id } });
await prisma.employeeRole.deleteMany({ where: { employee_id: employee.id, role_code: { in: ["finance", "ceo"] } } });
passed.push("finance owner: transfer only to CEO, stale assignee or principal and non-CEO delegate rejected; eligible CEO delegation approves");
}
await prisma.employee.update({ where: { id: employee.id }, data: { token_version: { increment: 1 } } });
assert.equal((await fetch(`${origin}/api/me`, { headers: { authorization: `Bearer ${tokens.get(employee.id)}` } })).status, 401);
passed.push("revoked token version rejected by real auth plugin");
console.log(JSON.stringify({ status: businessBlockers.length ? "BLOCKED_OWNER_APPROVAL" : "PASS_REAL_DB_HTTP", passed, businessBlockers, authentication: "REAL_PASSWORD_LOGIN_JWT_SYNTHETIC_ACCOUNTS", liveOcr: "NOT_TESTED", ocrWorker: process.env.RUN_JOB_QUEUE === "1" ? "REAL_PGBOSS_MOCK_PROVIDER" : process.env.RUN_BROWSER_TEST === "1" ? "REAL_WORKER_MOCK_PROVIDER_MANUAL_DISPATCH" : "NOT_TESTED", approvalFlow: "RETURN_RESUBMIT_THREE_STEP_APPROVAL", payment: process.env.RUN_HANDOFF_PROJECTION_TEST === "1" ? "SYNTHETIC_RECEIPT_RECORDED_PAID_NO_BANK_CONNECTION" : process.env.RUN_PAYMENT_EXPERIENCE_TEST === "1" ? "CREATE_REVIEW_DOWNLOAD_STILL_UNPAID" : "NOT_TESTED" }, null, 2));
if (businessBlockers.length) process.exitCode = 1;
} finally {
if (browser) await browser.close().catch(() => { process.exitCode = 1; console.error("Browser cleanup failed"); });
if (preview) await new Promise((resolve, reject) => preview.httpServer.close((error) => error ? reject(error) : resolve())).catch(() => { process.exitCode = 1; console.error("Preview cleanup failed"); });
try { if (app) await app.close(); }
finally {
try { if (prisma) await prisma.$disconnect(); }
finally {
try { if (pool) await pool.end(); }
finally {
try { if (container) { docker(["rm", "-f", container]); console.log("Disposable database removed"); } }
finally { if (storageDir) { rmSync(storageDir, { recursive: true, force: true }); console.log("Synthetic upload storage removed"); } }
}
}
}
}
}
main().catch((error) => { console.error("Isolated HTTP verification failed:", error.code ?? error.name, error.code === "ERR_ASSERTION" ? error.message : "", error.stack?.match(/test-(?:invoice-first-http|reimbursement-retained-delete|payment-experience)\.cjs:\d+:\d+/)?.[0] ?? ""); process.exitCode = 1; });