import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; const readAuditLogsView = () => readFileSync(resolve(__dirname, "./AuditLogs.vue"), "utf8"); describe("audit logs access", () => { it("keeps the audit summary header compact", () => { const source = readAuditLogsView(); expect(source).toContain("padding: 10px 16px;"); expect(source).toContain("padding: 9px 12px;"); expect(source).toContain("width: 32px;"); expect(source).toContain("height: 32px;"); expect(source).toContain(".stat-icon svg { width: 18px; height: 18px; }"); expect(source).toContain(".stat-value { font-size: 20px;"); expect(source).toContain(".stat-label { font-size: 11px;"); }); it("loads audit logs from the selected project context", () => { const source = readAuditLogsView(); expect(source).toContain("const selectedStudy = ref(null)"); expect(source).toContain("const selectedStudyId = computed({"); expect(source).toContain("get: () => selectedStudy.value?.id || \"\""); expect(source).toContain("set: (studyId: string) => {"); expect(source).toContain("await fetchAuditLogs(selectedStudyId.value, params)"); expect(source).toContain("await fetchApiEndpointPermissions(selectedStudyId.value)"); expect(source).toContain("await createAuditEvent(selectedStudyId.value,"); expect(source).not.toContain("await fetchAuditLogs(study.currentStudy.id, params)"); }); it("limits non-admin project choices to PM-owned projects", () => { const source = readAuditLogsView(); expect(source).toContain('const availableItems = isAdmin.value ? items : items.filter((item) => item.role_in_study === "PM");'); expect(source).toContain("studies.value = availableItems;"); expect(source).toContain("availableItems.find((item) => item.id === queryProjectId)"); expect(source).toContain("availableItems.find((item) => item.id === study.currentStudy?.id)"); expect(source).toContain("availableItems[0]"); }); it("allows only admins or authorized PMs to use audit export", () => { const source = readAuditLogsView(); expect(source).toContain("const canAccessAuditLogs = computed"); expect(source).toContain("isSystemAdmin"); expect(source).toContain("getProjectRole"); expect(source).toContain("study.currentStudyRole"); expect(source).toContain('projectRole.value === "PM"'); expect(source).toContain("isApiPermissionAllowed"); expect(source).toContain('permissionMatrix.value?.[projectRole.value]?.["audit_logs:read"]'); expect(source).toContain('permissionMatrix.value?.[projectRole.value]?.["audit_logs:export"]'); expect(source).toContain("if (!canProjectExport.value) return;"); expect(source).toContain('action: "AUDIT_EXPORT_PROJECT"'); expect(source).not.toContain('command="system"'); expect(source).not.toMatch(/auth\.user\?\.role/); expect(source).not.toContain('!== "ADMIN"'); }); it("exports all matching audit log pages instead of a fixed first page", () => { const source = readAuditLogsView(); expect(source).toContain("const fetchAuditLogPages = async"); expect(source).toContain("while (true)"); expect(source).toContain("if (items.length < batchLimit) break;"); expect(source).not.toContain("limit: 2000"); }); it("surfaces request source, IP and location filters for audit troubleshooting", () => { const source = readAuditLogsView(); expect(source).toContain("filters.clientType"); expect(source).toContain("filters.ipKeyword"); expect(source).toContain("TEXT.modules.adminAuditLogs.filterSource"); expect(source).toContain("TEXT.modules.adminAuditLogs.filterIpLocation"); expect(source).toContain("TEXT.modules.adminAuditLogs.columns.source"); expect(source).toContain("formatClientIpLine(scope.row)"); expect(source).toContain("client_ip: ipKeyword && isIpSearchToken(ipKeyword) ? ipKeyword : undefined"); }); it("includes request source context in audit exports", () => { const columns = readFileSync(resolve(__dirname, "../../audit/export/auditExportColumns.ts"), "utf8"); const formatter = readFileSync(resolve(__dirname, "../../audit/export/auditExportFormatter.ts"), "utf8"); expect(columns).toContain('key: "ipLocation"'); expect(columns).toContain('key: "clientSource"'); expect(columns).toContain('key: "userAgent"'); expect(formatter).toContain("ip: event.clientIp || \"\""); expect(formatter).toContain("clientSource: event.clientSourceLabel || \"\""); expect(formatter).toContain("userAgent: event.userAgent || \"\""); }); it("keeps the audit table compact by removing the duplicate content column", () => { const source = readAuditLogsView(); expect(source).not.toContain('prop="actionText"'); expect(source).not.toContain("TEXT.modules.adminAuditLogs.columns.action"); expect(source).toContain("TEXT.modules.adminAuditLogs.columns.target"); expect(source).toContain("TEXT.modules.adminAuditLogs.columns.diff"); }); it("shows actor names without avatar icons in the audit table", () => { const source = readAuditLogsView(); expect(source).toContain('{{ scope.row.actorName }}'); expect(source).not.toContain("actor-avatar"); expect(source).not.toContain("getInitials(scope.row.actorName)"); }); it("lets target and detail columns absorb remaining table width", () => { const source = readAuditLogsView(); expect(source).toContain(''); expect(source).toContain(''); expect(source).not.toContain(':label="TEXT.modules.adminAuditLogs.columns.target" width="240"'); expect(source).not.toContain(':label="TEXT.modules.adminAuditLogs.columns.diff" width="360"'); }); it("parses diff lines by the last field separator before the arrow", () => { const source = readAuditLogsView(); expect(source).toMatch(/line\.match\(\/\^\(\.\+\)\[::\]\\s\*\(\.\*\?\)\\s\*->\\s\*\(\.\+\)\$\/\)/); expect(source).not.toMatch(/line\.match\(\/\^\(\.\+\?\):\(\.\+\?\)\\s\*->\\s\*\(\.\+\)\$\/\)/); }); it("renders detail object and action as two cards in one row", () => { const source = readAuditLogsView(); expect(source).toContain('对象'); expect(source).toContain('动作'); expect(source).not.toContain("detail-meta-full"); expect(source).not.toContain("meta-card-full"); }); it("groups setup diff detail rows by business item path", () => { const source = readAuditLogsView(); expect(source).toContain("const buildDetailDiffGroups = (lines: any[]): DetailDiffGroup[] => {"); expect(source).toContain('const groupedIndex = new Map();'); expect(source).toContain('title: parts.slice(0, -1).join(" / "),'); expect(source).toContain("field: parts[parts.length - 1],"); expect(source).toContain('class="diff-group-title"'); expect(source).toContain("{{ selectedDiffGroups.length }} 项"); }); it("does not expose legacy startup ethics business labels", () => { const auditSource = readFileSync(resolve(__dirname, "../../audit/index.ts"), "utf8"); const overviewSource = readFileSync(resolve(__dirname, "../ia/project-overview/overview.adapter.ts"), "utf8"); const combined = `${auditSource}\n${overviewSource}`; expect(combined).not.toContain("可行性评估"); expect(combined).not.toContain("伦理审批"); expect(combined).not.toContain("立项可行性"); expect(combined).not.toContain("立项伦理"); expect(auditSource).toContain('startup_feasibility: "立项记录"'); expect(auditSource).toContain('startup_ethics: "伦理记录"'); expect(overviewSource).toContain('label: "伦理记录"'); }); });