import { createHmac } from 'node:crypto'; import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { FastifyRequest, FastifyReply } from 'fastify'; import { buildServer } from '../../packages/server/src/index.js'; import { users, teams, teamMembers, tasks, messages, teamEntities, teamResources, agentAuditLog, } from '../../packages/server/src/db/schema.js'; import { sql } from 'drizzle-orm'; const SIGNING_KEY = Buffer.from('waggle-m3-full-stack-test-secret'); const SIGNING_SECRET = `whsec_${SIGNING_KEY.toString('base64')}`; function signedHeaders(payload: object) { const id = 'msg_waggle_m3_full_stack'; const timestamp = Math.floor(Date.now() / 1000); const signature = createHmac('sha256', SIGNING_KEY) .update(`${id}.${timestamp}.${JSON.stringify(payload)}`) .digest('base64'); return { 'svix-id': id, 'svix-timestamp': String(timestamp), 'svix-signature': `v1,${signature}`, }; } describe('M3 Full Stack Integration', () => { let server: Awaited>; let ownerId: string; let memberId: string; let teamId: string; let originalSigningSecret: string | undefined; const teamSlug = 'integ-team'; beforeAll(async () => { originalSigningSecret = process.env.CLERK_WEBHOOK_SIGNING_SECRET; process.env.CLERK_WEBHOOK_SIGNING_SECRET = SIGNING_SECRET; server = await buildServer(); // Clean up leftover test data from previous runs (reverse dependency order) await server.db.execute(sql`DELETE FROM agent_audit_log WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'integ_%')`); await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_relations WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_entities WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM messages WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM tasks WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM agent_jobs WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM cron_schedules WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM teams WHERE slug = 'integ-team'`); await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'integ_%'`); // 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 () => { try { // Clean up all test data await server.db.execute(sql`DELETE FROM agent_audit_log WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'integ_%')`); await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_relations WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_entities WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM messages WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM tasks WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM agent_jobs WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM cron_schedules WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug = 'integ-team')`); await server.db.execute(sql`DELETE FROM teams WHERE slug = 'integ-team'`); await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'integ_%'`); } finally { try { await server?.close(); } finally { if (originalSigningSecret === undefined) { delete process.env.CLERK_WEBHOOK_SIGNING_SECRET; } else { process.env.CLERK_WEBHOOK_SIGNING_SECRET = originalSigningSecret; } } } }); it('Step 1: Creates users via webhook', async () => { // Create owner const ownerEvent = { type: 'user.created', data: { id: 'integ_owner', first_name: 'Owner', last_name: 'User', email_addresses: [{ email_address: 'integ_owner@test.com' }], }, }; let res = await server.inject({ method: 'POST', url: '/api/webhooks/clerk', headers: signedHeaders(ownerEvent), payload: ownerEvent, }); expect(res.statusCode).toBe(200); // Create member const memberEvent = { type: 'user.created', data: { id: 'integ_member', first_name: 'Member', last_name: 'User', email_addresses: [{ email_address: 'integ_member@test.com' }], }, }; res = await server.inject({ method: 'POST', url: '/api/webhooks/clerk', headers: signedHeaders(memberEvent), payload: memberEvent, }); expect(res.statusCode).toBe(200); // Get user IDs const [owner] = await server.db.select().from(users).where(sql`clerk_id = 'integ_owner'`); const [member] = await server.db.select().from(users).where(sql`clerk_id = 'integ_member'`); expect(owner).toBeDefined(); expect(member).toBeDefined(); ownerId = owner.id; memberId = member.id; }); it('Step 2: Creates team and invites member', async () => { let res = await server.inject({ method: 'POST', url: '/api/teams', headers: { 'x-test-user-id': ownerId }, payload: { name: 'Integration Team', slug: teamSlug }, }); expect(res.statusCode).toBe(201); teamId = JSON.parse(res.body).id; // Add member res = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/members`, headers: { 'x-test-user-id': ownerId }, payload: { email: 'integ_member@test.com', role: 'member' }, }); expect(res.statusCode).toBe(201); }); it('Step 3: Creates task on team board', async () => { const res = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/tasks`, headers: { 'x-test-user-id': ownerId }, payload: { title: 'Research competitor pricing', priority: 'high' }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.title).toBe('Research competitor pricing'); expect(body.priority).toBe('high'); expect(body.status).toBe('open'); }); it('Step 4: Sends Waggle Dance message', async () => { const res = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/messages`, headers: { 'x-test-user-id': ownerId }, payload: { type: 'request', subtype: 'knowledge_check', content: { topic: 'competitor pricing' }, }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.type).toBe('request'); expect(body.subtype).toBe('knowledge_check'); }); it('Step 5: Shares knowledge entity to team graph', async () => { const res = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/entities`, headers: { 'x-test-user-id': ownerId }, payload: { entityType: 'competitor', name: 'Acme Corp', properties: { pricing: '$99/mo' } }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.entityType).toBe('competitor'); expect(body.name).toBe('Acme Corp'); }); it('Step 6: Shares team resource', async () => { const res = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/resources`, headers: { 'x-test-user-id': ownerId }, payload: { resourceType: 'model_recipe', name: 'Fast Research Config', config: { model: 'claude-haiku-4-5', temperature: 0.3 }, }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.resourceType).toBe('model_recipe'); expect(body.name).toBe('Fast Research Config'); }); it('Step 7: Creates audit trail entry', async () => { const { AuditService } = await import('../../packages/server/src/services/audit-service.js'); const auditService = new AuditService(server.db); const entry = await auditService.log({ userId: ownerId, teamId, agentName: 'memory-weaver', actionType: 'consolidation', description: 'Consolidated meeting notes', }); expect(entry.agentName).toBe('memory-weaver'); // Verify via admin API // First promote owner to admin role (owner already has admin+ perms) const res = await server.inject({ method: 'GET', url: `/api/admin/teams/${teamSlug}/audit`, headers: { 'x-test-user-id': ownerId }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.length).toBeGreaterThanOrEqual(1); }); it('Step 8: Member cannot access admin audit endpoint', async () => { const res = await server.inject({ method: 'GET', url: `/api/admin/teams/${teamSlug}/audit`, headers: { 'x-test-user-id': memberId }, }); expect(res.statusCode).toBe(403); }); it('Step 9: Lists tasks, messages, entities for the team', async () => { // Tasks const tasksRes = await server.inject({ method: 'GET', url: `/api/teams/${teamSlug}/tasks`, headers: { 'x-test-user-id': memberId }, }); expect(tasksRes.statusCode).toBe(200); expect(JSON.parse(tasksRes.body).length).toBeGreaterThanOrEqual(1); // Messages const msgsRes = await server.inject({ method: 'GET', url: `/api/teams/${teamSlug}/messages`, headers: { 'x-test-user-id': memberId }, }); expect(msgsRes.statusCode).toBe(200); expect(JSON.parse(msgsRes.body).length).toBeGreaterThanOrEqual(1); // Entities const entRes = await server.inject({ method: 'GET', url: `/api/teams/${teamSlug}/entities`, headers: { 'x-test-user-id': memberId }, }); expect(entRes.statusCode).toBe(200); expect(JSON.parse(entRes.body).length).toBeGreaterThanOrEqual(1); }); it('Step 10: Health check passes', async () => { const res = await server.inject({ method: 'GET', url: '/health' }); expect(res.statusCode).toBe(200); expect(JSON.parse(res.body).status).toBe('ok'); }); });