import { describe, expect, it } from 'vitest'; import { ActivityResponseSchema, ChatSyncStatusSchema, ChatsResponseSchema, ChatSyncTriggerResponseSchema, ConfigResponseSchema, ConfigUpdateRequestSchema, ControlResponseSchema, ConversationDetailResponseSchema, ConversationsResponseSchema, DashboardEventSchema, ErrorResponseSchema, LoginStatusResponseSchema, LegacyDashboardStreamEventSchema, MediaResponseSchema, MediaSyncStatusSchema, MessagesResponseSchema, ProcessedPostsResponseSchema, StatsResponseSchema, StrategyPerformanceResponseSchema, SubredditPerformanceResponseSchema, DailyTrendResponseSchema, FunnelResponseSchema, SuccessfulExamplesResponseSchema, TimersResponseSchema, } from '../src/index.js'; const activity = { timestamp: 1_700_000_000_000, type: 'sent', message: 'Sent message', recipient: 'alice', }; const chat = { username: 'alice', roomName: 'Alice', lastMessageTime: 1_700_000_000_000, lastMessageSnippet: 'hello', isFromUs: false, unread: true, roomId: '!room:reddit.com', messages: [{ sender: 'alice', text: 'hello', timestamp: 1_700_000_000_000, isSelf: false }], }; describe('dashboard endpoint contracts', () => { it('accepts current stats and rejects malformed stats', () => { const payload = { isRunning: true, dryRunMode: false, totalProcessed: 10, totalMessagesSent: 8, totalResponses: 3, responseRate: 38, messagesThisHour: 2, maxMessagesPerHour: 8, subreddits: [{ name: 'example', type: 'image' }], targetGenders: ['female'], recentActivity: [activity], invitationCooldown: null, }; expect(StatsResponseSchema.parse(payload)).toEqual(payload); expect(StatsResponseSchema.safeParse({ ...payload, isRunning: 'yes' }).success).toBe(false); }); it('validates authoritative start and stop responses', () => { expect(ControlResponseSchema.safeParse({ success: true, running: true }).success).toBe(true); expect(ControlResponseSchema.safeParse({ success: true, running: false }).success).toBe(true); expect(ControlResponseSchema.safeParse({ success: true }).success).toBe(false); expect(ControlResponseSchema.safeParse({ success: false, running: true }).success).toBe(false); }); it('validates timer, activity, message, and successful-example payloads', () => { expect(TimersResponseSchema.safeParse({ nextMessageAvailableAt: null, hourlyResetAt: 1_700_000_100_000, isHourlyLimitReached: false, messagesRemaining: 6, }).success).toBe(true); expect(ActivityResponseSchema.safeParse([activity]).success).toBe(true); expect(MessagesResponseSchema.safeParse([{ id: 1, postId: 'post-1', recipient: 'alice', subject: 'Hi', message: 'Hello', sentAt: 1_700_000_000_000, gotResponse: false, gotImage: false, responseCheckedAt: null, strategyId: null, chatRoomId: null, experimentId: 'message:variant-a:abc123', promptVersion: 'abc123', modelId: 'openai/gpt-5.2', selectionReason: 'exploit', sourceType: 'image', }]).success).toBe(true); expect(SuccessfulExamplesResponseSchema.safeParse([{ id: 1, sentMessageId: null, originalMessage: 'Hello', imageAnalysis: null, postTitle: null, responseSnippet: 'Hi', subreddit: 'example', createdAt: 1_700_000_000_000, }]).success).toBe(true); expect(TimersResponseSchema.safeParse({ hourlyResetAt: 'later' }).success).toBe(false); expect(ActivityResponseSchema.safeParse([{ ...activity, type: 'bogus' }]).success).toBe(false); expect(MessagesResponseSchema.safeParse([{ id: '1' }]).success).toBe(false); const processedPost = { id: 'post-1', subreddit: 'example', author: 'alice', title: 'Example', imageUrl: null, imageAnalysis: null, generatedMessage: 'Hello', postUrl: 'https://www.reddit.com/r/example/comments/post-1/', processedAt: 1_700_000_000_000, messageSent: true, error: null, }; expect(ProcessedPostsResponseSchema.safeParse([processedPost]).success).toBe(true); expect(ProcessedPostsResponseSchema.safeParse([{ ...processedPost, messageSent: 'yes' }]).success).toBe(false); }); it('validates each analytics response shape', () => { expect(SubredditPerformanceResponseSchema.safeParse([{ subreddit: 'example', totalSent: 10, totalResponses: 4, responseRate: 40, totalImages: 2, imageRate: 20, }]).success).toBe(true); expect(StrategyPerformanceResponseSchema.safeParse([{ strategyId: 'direct', totalSent: 10, totalResponses: 4, responseRate: 40, totalImages: 2, imageRate: 20, configured: true, }]).success).toBe(true); expect(DailyTrendResponseSchema.safeParse([{ date: '2026-08-08', totalSent: 10, totalResponses: 4, responseRate: 40, totalImages: 2, imageRate: 20, }]).success).toBe(true); expect(FunnelResponseSchema.safeParse({ responseWindowHours: 48, processed: 20, sent: 10, pending: 2, mature: 8, responded: 4, engaged: 3, declined: 1, boundary: 0, upsell: 0, images: 2, }).success).toBe(true); expect(DailyTrendResponseSchema.safeParse([{ date: '2026-08-08', totalSent: '10' }]).success).toBe(false); }); it('validates runtime config returned by the dashboard', () => { const payload = { rateLimit: { minDelayBetweenMessagesMs: 300_000, maxMessagesPerHour: 8, jitterMs: 60_000 }, browser: { dryRun: false }, reddit: { prioritizeRecentPosts: true, recentWindowMinutes: 60 }, subreddits: [{ name: 'example', enabled: true, type: 'chat', weight: 2, tone: 'direct' }], tones: ['default', 'direct'], }; expect(ConfigResponseSchema.parse(payload)).toEqual({ ...payload, subreddits: [{ ...payload.subreddits[0], conversation: { mode: 'legacy', maxAutomatedTurns: 3, rapportTurnsBeforeMediaAsk: 3 }, }], }); expect(ConfigResponseSchema.safeParse({ ...payload, tones: [1] }).success).toBe(false); expect(ConfigUpdateRequestSchema.safeParse({ rateLimit: { maxMessagesPerHour: 10 }, browser: { dryRun: true }, advanced: { media: { syncEnabled: false } }, }).success).toBe(true); expect(ConfigUpdateRequestSchema.safeParse({ browser: { dryRun: 'yes' } }).success).toBe(false); }); it('validates chats, sync state, conversation detail, and malformed messages', () => { expect(ChatsResponseSchema.safeParse([chat]).success).toBe(true); expect(ConversationsResponseSchema.safeParse([chat]).success).toBe(true); expect(ChatSyncStatusSchema.safeParse({ syncing: true, syncingKind: 'full', lastFullSyncAt: null, lastDeltaSyncAt: 1_700_000_000_000, hasEverFullSynced: false, cachedChats: 12, progress: { kind: 'full', phase: 'loading-details', current: 3, total: 12, label: 'alice', startedAt: 1_700_000_000_000, }, }).success).toBe(true); expect(ConversationDetailResponseSchema.safeParse({ ...chat, triggeringPost: { id: 'post-1', subreddit: 'example', title: 'Example', url: 'https://www.reddit.com/r/example/comments/post-1/', imageUrl: 'https://i.redd.it/example.jpg', postedAt: 1_699_999_000_000, sentAt: 1_700_000_000_000, }, automation: { turnsUsed: 1, remainingTurns: 2, status: 'active', handoffReason: null }, syncInProgress: null, }).success).toBe(true); expect(ConversationDetailResponseSchema.safeParse({ ...chat, triggeringPost: null, automation: { turnsUsed: 4, remainingTurns: 2, status: 'active', handoffReason: null }, syncInProgress: null, }).success).toBe(true); expect(ChatsResponseSchema.safeParse([{ ...chat, messages: [{ sender: 'alice' }] }]).success).toBe(false); expect(ChatSyncTriggerResponseSchema.safeParse({ success: true, kind: 'delta' }).success).toBe(true); expect(ChatSyncTriggerResponseSchema.safeParse({ success: false, reason: 'Already running full sync' }).success).toBe(true); }); it('validates media items and media sync status', () => { expect(MediaResponseSchema.safeParse([{ id: 'sha1', chatUsername: 'alice', chatRoomId: '!room:reddit.com', sender: 'alice', timestamp: 1_700_000_000_000, originalUrl: 'https://example.com/a.jpg', fileName: 'a.jpg', relativePath: 'a.jpg', createdAt: 1_700_000_000_100, urlPath: '/media/a.jpg', }]).success).toBe(true); expect(MediaSyncStatusSchema.safeParse({ isSyncing: false, lastSyncAt: null, lastSyncDurationMs: null, lastSyncChats: null, lastError: null, nextSyncAt: null, }).success).toBe(true); expect(MediaResponseSchema.safeParse([{ id: 'sha1', timestamp: 'now' }]).success).toBe(false); }); it('validates every current login-status branch', () => { expect(LoginStatusResponseSchema.safeParse({ ready: true, loggedIn: true, status: 'authenticated', }).success).toBe(true); expect(LoginStatusResponseSchema.safeParse({ ready: false, error: 'browser not initialised' }).success).toBe(true); expect(LoginStatusResponseSchema.safeParse({ ready: true, loggedIn: false, error: 'check failed' }).success).toBe(true); expect(LoginStatusResponseSchema.safeParse({ ready: true, loggedIn: true, status: 'expired' }).success).toBe(false); }); it('validates the common error response and typed realtime event envelope', () => { expect(ErrorResponseSchema.safeParse({ error: 'Conversation not found' }).success).toBe(true); const occurredAt = '2026-08-08T13:55:00.000Z'; const events = [ { id: '1', occurredAt, type: 'bot.status.changed', payload: { status: 'running', dryRun: false, reason: 'startup complete' } }, { id: '2', occurredAt, type: 'message.sent', payload: { messageId: 'm1', recipient: 'alice', conversationId: 'c1' } }, { id: '3', occurredAt, type: 'message.failed', payload: { recipient: 'alice', reason: 'rate limited', messageId: 'm2', conversationId: 'c1', code: 'limited', retryable: true } }, { id: '4', occurredAt, type: 'conversation.updated', payload: { conversationId: 'c1', status: 'replied', unreadCount: 1 } }, { id: '5', occurredAt, type: 'reply.received', payload: { conversationId: 'c1', replyId: 'r1', sender: 'alice', preview: 'Hi' } }, { id: '6', occurredAt, type: 'sync.started', payload: { syncId: 's1', kind: 'media' } }, { id: '7', occurredAt, type: 'sync.progress', payload: { syncId: 's1', kind: 'media', completed: 3, total: 10, message: 'Downloading' } }, { id: '8', occurredAt, type: 'sync.completed', payload: { syncId: 's1', kind: 'media', processed: 10 } }, { id: '9', occurredAt, type: 'sync.failed', payload: { syncId: 's1', kind: 'media', reason: 'network', code: 'offline' } }, { id: '10', occurredAt, type: 'rate-limit.changed', payload: { limited: true, remaining: 0, resetsAt: occurredAt, reason: 'hourly' } }, { id: '11', occurredAt, type: 'reddit.session.changed', payload: { status: 'authenticated', username: 'bot-user', reason: 'login completed' } }, { id: '12', occurredAt, type: 'log.created', payload: { level: 'info', message: 'Started', data: { port: 3000 } } }, ]; for (const event of events) { expect(DashboardEventSchema.safeParse(event).success, event.type).toBe(true); } expect(DashboardEventSchema.safeParse({ id: 'event-13', type: 'log.created', occurredAt: 1_700_000_000_000, payload: { level: 'info', message: 'Started' }, }).success).toBe(false); expect(DashboardEventSchema.safeParse({ id: 'event-14', type: 'sync.completed', occurredAt, payload: { syncId: 's1', kind: 'media' }, }).success).toBe(false); expect(LegacyDashboardStreamEventSchema.safeParse({ type: 'connected', timestamp: 1_700_000_000_000, }).success).toBe(true); expect(LegacyDashboardStreamEventSchema.safeParse({ type: 'sync-progress', progress: null, }).success).toBe(true); }); });