import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import type { FastifyInstance } from 'fastify'; import { buildApp } from '../src/app.js'; import { loadConfig } from '../src/config.js'; import { pool, closePool } from '../src/db/pool.js'; import { drizzle } from 'drizzle-orm/node-postgres'; import { agents, apiTokens, auditEvents, socialChannels, socialPosts, messages, roomMembers, rooms, } from '../src/db/schema.js'; import { SocialClient } from '../src/sdk/social.js'; describe('Social SDK', () => { let app: FastifyInstance; let baseUrl: string; let adminId: string; let agentId: string; beforeAll(async () => { const config = loadConfig({ ...process.env, NODE_ENV: 'test', JWT_SECRET: 'test-secret-with-at-least-32-chars-for-jwt-security', }); app = await buildApp({ config }); await app.ready(); const address = await app.listen({ port: 0, host: '127.0.0.1' }); baseUrl = address; const db = drizzle(pool); await db.delete(socialPosts); await db.delete(socialChannels); await db.delete(auditEvents); await db.delete(messages); await db.delete(roomMembers); await db.delete(rooms); await db.delete(apiTokens); await db.delete(agents); const adminRes = await request(app.server) .post('/api/v1/agents') .send({ name: 'sdk-admin', displayName: 'SDK Admin', role: 'admin' }); adminId = adminRes.body.id; const agentRes = await request(app.server) .post('/api/v1/agents') .send({ name: 'sdk-agent', displayName: 'SDK Agent', role: 'agent' }); agentId = agentRes.body.id; // Create a channel for testing await request(app.server) .post('/api/v1/social/channels') .set('x-agent-id', adminId) .send({ slug: 'heartbeat', name: 'Heartbeat', description: 'Agent heartbeat posts' }); }); afterAll(async () => { await app.close(); await closePool(); }); it('should post to a channel by slug', async () => { const client = new SocialClient({ baseUrl, agentId }); const post = await client.post('heartbeat', 'Heartbeat check-in: all systems nominal.'); expect(post.id).toBeTruthy(); expect(post.channelSlug).toBe('heartbeat'); expect(post.authorAgentId).toBe(agentId); expect(post.authorName).toBe('SDK Agent'); expect(post.body).toBe('Heartbeat check-in: all systems nominal.'); }); it('should list channels', async () => { const client = new SocialClient({ baseUrl, agentId }); const channels = await client.channels(); expect(channels.length).toBe(1); expect(channels[0]!.slug).toBe('heartbeat'); }); it('should read the feed', async () => { const client = new SocialClient({ baseUrl, agentId }); const feed = await client.feed(); expect(feed.posts.length).toBeGreaterThan(0); expect(feed.posts[0]!.channelSlug).toBe('heartbeat'); }); it('should read channel posts', async () => { const client = new SocialClient({ baseUrl, agentId }); const result = await client.channelPosts('heartbeat'); expect(result.channel.slug).toBe('heartbeat'); expect(result.posts.length).toBeGreaterThan(0); }); it('should cache channel slugs', async () => { const client = new SocialClient({ baseUrl, agentId }); // First call resolves and caches await client.post('heartbeat', 'Post 1'); // Second call uses cache (no extra API call) const post2 = await client.post('heartbeat', 'Post 2'); expect(post2.body).toBe('Post 2'); }); it('should throw on unknown channel', async () => { const client = new SocialClient({ baseUrl, agentId }); await expect(client.post('nonexistent', 'hello')).rejects.toThrow('Channel "nonexistent" not found'); }); });