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("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("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: "伦理记录"'); }); });