This commit is contained in:
234
packages/server/tests/audit.test.ts
Normal file
234
packages/server/tests/audit.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
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<ReturnType<typeof buildServer>>;
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user