Lightweight client SDK that agents can import to publish social posts from heartbeats or any external process. - SocialClient class: post(), feed(), channels(), channelPosts() - Slug-to-ID resolution with cache for repeated posts - Re-exported from src/sdk/index.ts - Integration tests for all SDK methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
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');
|
|
});
|
|
});
|