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"); if (process.env.RUN_ERROR_GUIDANCE_TEST === "1") { assert.equal(process.env.RUN_BROWSER_TEST, "1", "Error guidance requires browser verification"); assert.equal(process.env.RUN_JOB_QUEUE, "1", "Error guidance requires mock OCR provider"); } 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: "invoice-http@example.invalid", 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: "invoice-manager@example.invalid", 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 }); if (process.env.RUN_ERROR_GUIDANCE_TEST === "1") { assert.equal(process.env.RUN_JOB_QUEUE, "1", "Error guidance requires mock OCR provider"); const invoice = await makeInvoice({ status: "ocr_failed", ocr_type: "vat", invoice_code: null, seller_name: null }); const context = await browser.newContext({ serviceWorkers: "block" }); try { const fingerprint = `synthetic-guidance-${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: employee.email, password: loginPassword } }); assert.equal(login.status(), 200); const page = await context.newPage(); await page.goto(`${uiOrigin}/invoice-pool?id=${invoice.id}`); await page.getByTestId("invoice-edit-open").click(); await page.getByTestId("invoice-edit-set-available").check(); const failed = page.waitForResponse((response) => response.request().method() === "PATCH" && response.url().endsWith(`/invoice-pool/${invoice.id}`)); await page.getByTestId("invoice-edit-save").click(); const failure = await failed; assert.equal(failure.status(), 409); assert.equal((await failure.json()).error.details.reasonCode, "invoice_pool.ocr_fields_missing"); await expect(page.locator("main").getByText("票据仍缺少关键字段:发票代码、销售方,补齐后才能标记为可用。", { exact: true })).toBeVisible(); assert.equal((await prisma.invoicePool.findUniqueOrThrow({ where: { id: invoice.id } })).status, "ocr_failed"); await page.getByTestId("invoice-edit-code").fill("044003204111"); await page.getByTestId("invoice-edit-seller-name").fill("synthetic seller"); await page.getByTestId("invoice-edit-save").click(); await expect(page.getByTestId("invoice-detail-panel")).toContainText("状态:可用"); assert.equal((await prisma.invoicePool.findUniqueOrThrow({ where: { id: invoice.id } })).status, "available"); console.log(JSON.stringify({ status: "PASS_ERROR_GUIDANCE_REAL_AUTH_DB_BROWSER", missingFieldsShown: true, failureRetainsState: true, correctedSavePersists: true, liveOcr: "NOT_CALLED", fixture: "SEEDED_OCR_FAILED_SYNTHETIC_INVOICE" })); } finally { await context.close(); } return; } 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; });