import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { FastifyRequest, FastifyReply } from 'fastify'; import { buildServer } from '../src/index.js'; import { users, teams, teamMembers, agentAuditLog } from '../src/db/schema.js'; import { sql } from 'drizzle-orm'; import { AuditService } from '../src/services/audit-service.js'; describe('Audit & Traceability (Task 3.23)', () => { let server: Awaited>; let ownerId: string; let memberId: string; let teamSlug: string; let teamId: string; beforeAll(async () => { server = await buildServer(); // Clean up leftover test data await server.db.execute(sql`DELETE FROM agent_audit_log WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'audit_%')`); await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'audit-%'`); await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'audit_%'`); // Create test users const [owner] = await server.db.insert(users).values({ clerkId: 'audit_owner', displayName: 'Audit Owner', email: 'audit_owner@test.com', }).returning(); ownerId = owner.id; const [member] = await server.db.insert(users).values({ clerkId: 'audit_member', displayName: 'Audit Member', email: 'audit_member@test.com', }).returning(); memberId = member.id; // Create team const [team] = await server.db.insert(teams).values({ name: 'Audit Test Team', slug: 'audit-test', ownerId, }).returning(); teamId = team.id; teamSlug = team.slug; await server.db.insert(teamMembers).values([ { teamId, userId: ownerId, role: 'owner' }, { teamId, userId: memberId, role: 'member' }, ]); // Override auth handler for testing server._authHandler.fn = async function (request: FastifyRequest, reply: FastifyReply) { const testUserId = request.headers['x-test-user-id'] as string; if (!testUserId) { return reply.code(401).send({ error: 'Missing x-test-user-id header' }); } request.userId = testUserId; request.clerkId = 'test'; }; }); afterAll(async () => { await server.db.execute(sql`DELETE FROM agent_audit_log WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'audit_%')`); await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'audit-%')`); await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'audit-%'`); await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'audit_%'`); await server.close(); }); it('logs an audit entry via AuditService', async () => { const auditService = new AuditService(server.db); const entry = await auditService.log({ userId: ownerId, teamId, agentName: 'memory-weaver', actionType: 'consolidation', description: 'Consolidated 5 memory fragments into 1 summary', beforeState: { fragmentCount: 5 }, afterState: { summaryId: 'abc123' }, }); expect(entry).toBeDefined(); expect(entry.agentName).toBe('memory-weaver'); expect(entry.actionType).toBe('consolidation'); expect(entry.description).toBe('Consolidated 5 memory fragments into 1 summary'); expect(entry.beforeState).toEqual({ fragmentCount: 5 }); expect(entry.afterState).toEqual({ summaryId: 'abc123' }); expect(entry.requiresApproval).toBe(false); expect(entry.approved).toBeNull(); }); it('lists audit entries for team', async () => { const response = await server.inject({ method: 'GET', url: `/api/admin/teams/${teamSlug}/audit`, headers: { 'x-test-user-id': ownerId }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(Array.isArray(body)).toBe(true); expect(body.length).toBeGreaterThanOrEqual(1); expect(body[0].agentName).toBe('memory-weaver'); }); it('filters by actionType', async () => { // Add another entry with different actionType const auditService = new AuditService(server.db); await auditService.log({ userId: ownerId, teamId, agentName: 'scout', actionType: 'discovery', description: 'Found new relevant paper', }); const response = await server.inject({ method: 'GET', url: `/api/admin/teams/${teamSlug}/audit?actionType=discovery`, headers: { 'x-test-user-id': ownerId }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.length).toBe(1); expect(body[0].actionType).toBe('discovery'); expect(body[0].agentName).toBe('scout'); }); it('approves a pending entry', async () => { const auditService = new AuditService(server.db); const pending = await auditService.log({ userId: ownerId, teamId, agentName: 'hive-mind', actionType: 'share_knowledge', description: 'Share pricing analysis with team graph', requiresApproval: true, }); const response = await server.inject({ method: 'POST', url: `/api/admin/teams/${teamSlug}/audit/${pending.id}/approve`, headers: { 'x-test-user-id': ownerId }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.approved).toBe(true); expect(body.approvedBy).toBe(ownerId); }); it('rejects a pending entry', async () => { const auditService = new AuditService(server.db); const pending = await auditService.log({ userId: ownerId, teamId, agentName: 'subconscious', actionType: 'auto_task', description: 'Create follow-up task from meeting notes', requiresApproval: true, }); const response = await server.inject({ method: 'POST', url: `/api/admin/teams/${teamSlug}/audit/${pending.id}/reject`, headers: { 'x-test-user-id': ownerId }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.approved).toBe(false); expect(body.approvedBy).toBe(ownerId); }); it('only team admins can access audit endpoints (non-admin gets 403)', async () => { const response = await server.inject({ method: 'GET', url: `/api/admin/teams/${teamSlug}/audit`, headers: { 'x-test-user-id': memberId }, }); expect(response.statusCode).toBe(403); const body = JSON.parse(response.body); expect(body.error).toBe('Admin access required'); }); it('returns 404 for non-existent audit entry on approve', async () => { const fakeId = '00000000-0000-0000-0000-000000000000'; const response = await server.inject({ method: 'POST', url: `/api/admin/teams/${teamSlug}/audit/${fakeId}/approve`, headers: { 'x-test-user-id': ownerId }, }); expect(response.statusCode).toBe(404); }); it('lists pending approvals', async () => { const auditService = new AuditService(server.db); // Create a new pending entry await auditService.log({ userId: ownerId, teamId, agentName: 'hive-mind', actionType: 'bulk_share', description: 'Share 10 entities with team', requiresApproval: true, }); const response = await server.inject({ method: 'GET', url: `/api/admin/teams/${teamSlug}/audit/pending`, headers: { 'x-test-user-id': ownerId }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(Array.isArray(body)).toBe(true); // All returned entries should require approval and have null approved for (const entry of body) { expect(entry.requiresApproval).toBe(true); expect(entry.approved).toBeNull(); } }); });