import assert from "node:assert/strict"; import test from "node:test"; import { readFileSync } from "node:fs"; const actionRoute = readFileSync( new URL("../app/api/action/route.ts", import.meta.url), "utf8", ); const bootstrapRoute = readFileSync( new URL("../app/api/bootstrap/route.ts", import.meta.url), "utf8", ); const taskReportExportRoute = readFileSync( new URL("../app/api/task-report-export/route.ts", import.meta.url), "utf8", ); const recoveryExportRoute = readFileSync( new URL("../app/api/recovery-export/route.ts", import.meta.url), "utf8", ); const screenshotTaskExportRoute = readFileSync( new URL("../app/api/screenshot-task-export/route.ts", import.meta.url), "utf8", ); const taskResultImageRoute = readFileSync( new URL("../app/api/task-result-image/route.ts", import.meta.url), "utf8", ); const usersRoute = readFileSync( new URL("../app/api/users/route.ts", import.meta.url), "utf8", ); const mvpDb = readFileSync( new URL("../lib/mvp-db.ts", import.meta.url), "utf8", ); function section(source, action) { const start = source.indexOf(`"${action}"`); assert.ok(start >= 0, `action ${action} 不存在于路由源码`); const next = source.indexOf("} else if (body.action ===", start); return source.slice(start, next === -1 ? undefined : next); } test("manager-only actions gate on owner scope", () => { const managerOnly = [ "inspect_feishu", "create_task", "create_screenshot_task", "claim", "save_collection_schedule", "run_due_collections", "retry_failed_collections", "backfill_account_profiles", "set_public_account_ids", "bind_wecom_external_id", "send_test_wecom", "trigger_wecom_overdue_escalation", "wecom_list_external_contacts", "wecom_list_internal_contacts", "wecom_sync_group_chats", "project_list", "project_detail", "project_save", "project_delete", "project_ensure_main_chat", "task_set_owner", "task_update", "partner_save", "partner_group_owners", "project_push_task", "refresh_account_fields", "account_field_refresh_status", ]; for (const action of managerOnly) { const body = section(actionRoute, action); assert.match( body, /managerGate\(\)/, `${action} 必须 managerGate() 拒绝任务负责人`, ); } }); test("collect actions stay manager-only", () => { const body = section(actionRoute, "collect_now"); assert.match(body, /managerGate\(\)/); }); test("task-scoped actions verify ownership for task owners", () => { for (const action of [ "wecom_push_task_to_groups", "task_group_links", "task_group_push", "task_customer_chats", "task_bind_customer_chat", ]) { const body = section(actionRoute, action); assert.match( body, /taskOwnerGate\(/, `${action} 必须校验任务归属`, ); } const dueSoon = section(actionRoute, "trigger_wecom_due_soon"); assert.match(dueSoon, /taskOwnerGate\(/); assert.match(dueSoon, /runDueSoonWecomNotifications\(\s*db,\s*wecomConfig,\s*undefined,\s*undefined,\s*taskId \|\| undefined,/); const pushes = section(actionRoute, "wecom_task_group_pushes"); assert.match(pushes, /taskOwnerGate\(/); const manual = section(actionRoute, "manual_metrics"); assert.match(manual, /distributionOwnerGate\(/); }); test("task_report_list scopes reports by owner and drops the broken gate", () => { const body = section(actionRoute, "task_report_list"); assert.match(body, /getTaskReportData\(db, ownerScopeUserid\(scope\)\)/); assert.ok( !/isManagerRequest/.test(body), "task_report_list 不再是管理员专属(负责人按 scope 过滤)", ); }); test("no unawaited isManagerRequest gates remain", () => { assert.ok( !actionRoute.includes("if (!isManagerRequest(request))"), "isManagerRequest 返回 Promise,缺 await 的门形同虚设", ); }); test("action fallthrough dashboard is owner-scoped", () => { assert.match( actionRoute, /getDashboardData\(ownerScopeUserid\(scope\)\)/, ); }); test("customer chat actions require a task owner before binding", () => { for (const action of ["task_customer_chats", "task_bind_customer_chat"]) { const body = section(actionRoute, action); assert.match( body, /请先设置任务负责人/, `${action} 必须在无负责人时明确报错`, ); assert.match(body, /Response\.json\(/); assert.ok( !/getDashboardData/.test(body), `${action} 显式返回,不能落兜底 dashboard`, ); } const bind = section(actionRoute, "task_bind_customer_chat"); assert.match(bind, /bindTaskCustomerChat\(/); assert.match(bind, /unbindTaskCustomerChat\(/); }); test("partner claim detail exposes join qr only for claimed tasks", () => { const partnerRoute = readFileSync( new URL("../app/api/partner/route.ts", import.meta.url), "utf8", ); assert.match(partnerRoute, /join_qr_code FROM tasks/); assert.match(partnerRoute, /joinQrCode,/); const portalPage = readFileSync( new URL("../koc-portal/app/page.tsx", import.meta.url), "utf8", ); assert.match(portalPage, /joinQrCode\?: string/); assert.match(portalPage, /join-qr-card/); assert.match(portalPage, /isClaimOwner && payload\.claim\?\.joinQrCode/); }); test("bootstrap scopes dashboard data by owner scope", () => { assert.match(bootstrapRoute, /getOwnerScope\(request\)/); assert.match(bootstrapRoute, /getDashboardData\(ownerScopeUserid\(scope\)\)/); }); test("getDashboardData filters tasks and distributions by owner", () => { const dashboard = mvpDb.slice( mvpDb.indexOf("export async function getDashboardData"), mvpDb.indexOf("export type TaskReportRow"), ); const occurrences = dashboard.match(/WHERE t\.owner_userid = \?/g) ?? []; assert.equal( occurrences.length, 2, "tasks 与 distributions 两条查询都要按 owner 过滤", ); }); test("export routes enforce owner scope", () => { assert.match(taskReportExportRoute, /getOwnerScope\(request\)/); assert.match(taskReportExportRoute, /ownerScopeUserid\(await getOwnerScope\(request\)\)/); assert.match(recoveryExportRoute, /isTaskOwner\(/); assert.match(screenshotTaskExportRoute, /isTaskOwner\(/); assert.match(taskResultImageRoute, /isDistributionOwner\(/); }); test("users route supports wecom binding lifecycle", () => { assert.match(usersRoute, /action === "set_wecom"/); assert.match(usersRoute, /INSERT INTO users[\s\S]*?wecom_userid/); assert.match(usersRoute, /SELECT id, username, role, wecom_userid, created_at/); });