import crypto from "node:crypto"; export const DEFAULT_PORT = 18473; // Valid JWT format so isPlausibleSessionToken() in CoreStateProvider // recognizes it and triggers the auth-refresh path (clears logoutGuard). // exp = 4102444800 ≈ year 2099 — effectively never expires in tests. export const MOCK_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0" + ".eyJzdWIiOiJ1c2VyLTEyMyIsInVzZXJJZCI6InVzZXItMTIzIiwidGdVc2VySWQiOiJ1c2VyLTEyMyIsImV4cCI6NDEwMjQ0NDgwMH0" + ".e2e"; export const MAX_PORT_RETRY_ATTEMPTS = 10; export const MAX_MOCK_DELAY_MS = 30_000; let requestLog = []; let mockBehavior = {}; let mockTunnels = []; let mockConversations = []; let mockMessages = []; let mockCronJobs = []; let mockWebhookTriggers = []; let socketEventLog = []; let mockLlmThreads = new Map(); let nextSequence = 1; // ── Telegram Bot API mock state ──────────────────────────────────────────── let mockTelegramUpdates = []; let mockTelegramSentMessages = []; let mockTelegramMessageIdSeq = 1000; const socketSessions = new Map(); export const openSockets = new Set(); export function getRequestLog() { return [...requestLog]; } export function clearRequestLog() { requestLog = []; } export function appendRequest(entry) { requestLog.push(entry); } export function nextMockSequence() { const value = nextSequence; nextSequence += 1; return value; } export function getMockBehavior() { return { ...mockBehavior }; } export function setMockBehavior(key, value) { mockBehavior[key] = String(value); } export function setMockBehaviors(behavior, mode = "merge") { if (mode === "replace") { mockBehavior = {}; } for (const [key, value] of Object.entries(behavior || {})) { mockBehavior[key] = String(value); } } export function resetMockBehavior() { mockBehavior = {}; } export function behavior() { return mockBehavior; } export function parseInteger(value, fallback = 0) { const parsed = Number(value); return Number.isInteger(parsed) ? parsed : fallback; } export function hashString(input) { const text = String(input ?? ""); let hash = 2166136261; for (let i = 0; i < text.length; i += 1) { hash ^= text.charCodeAt(i); hash = Math.imul(hash, 16777619); } return hash >>> 0; } export function behaviorSeed() { return String(mockBehavior.seed || "openhuman-mock"); } export function fuzzyNumber(label, min, max) { const lower = Math.min(min, max); const upper = Math.max(min, max); if (lower === upper) return lower; const hash = hashString(`${behaviorSeed()}:${label}`); return lower + (hash % (upper - lower + 1)); } export function fuzzyPick(label, values, fallback = null) { if (!Array.isArray(values) || values.length === 0) return fallback; const index = fuzzyNumber(label, 0, values.length - 1); return values[index]; } export function fuzzyTimestamp(label, spreadMs = 14 * 24 * 60 * 60 * 1000) { const now = Date.now(); const offset = fuzzyNumber(label, 0, spreadMs); return new Date(now - offset).toISOString(); } export function createMockId(prefix = "mock") { return `${prefix}_${nextMockSequence()}_${hashString( `${prefix}:${behaviorSeed()}:${Date.now()}:${Math.random()}`, ) .toString(16) .slice(0, 8)}`; } export function parseBehaviorJson(key, fallback) { const raw = mockBehavior[key]; if (!raw) return JSON.parse(JSON.stringify(fallback)); try { return JSON.parse(raw); } catch { return JSON.parse(JSON.stringify(fallback)); } } export function getDelayMs(key) { const value = Number(mockBehavior[key] || 0); if (!Number.isFinite(value) || value <= 0) return 0; return Math.min(value, MAX_MOCK_DELAY_MS); } export function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } export function getMockTunnels() { return mockTunnels; } export function setMockTunnels(next) { mockTunnels = Array.isArray(next) ? next : []; } export function resetMockTunnels() { mockTunnels = []; } export function getMockConversations() { return mockConversations; } export function setMockConversations(next) { mockConversations = Array.isArray(next) ? next : []; } export function resetMockConversations() { mockConversations = []; } export function getMockMessages() { return mockMessages; } export function setMockMessages(next) { mockMessages = Array.isArray(next) ? next : []; } export function resetMockMessages() { mockMessages = []; } export function getMockCronJobs() { return mockCronJobs; } export function setMockCronJobs(next) { mockCronJobs = Array.isArray(next) ? next : []; } export function resetMockCronJobs() { mockCronJobs = []; } export function getMockWebhookTriggers() { return mockWebhookTriggers; } export function setMockWebhookTriggers(next) { mockWebhookTriggers = Array.isArray(next) ? next : []; } export function resetMockWebhookTriggers() { mockWebhookTriggers = []; } export function listMockLlmThreads() { return Array.from(mockLlmThreads.values()).map((thread) => ({ key: thread.key, createdAt: thread.createdAt, updatedAt: thread.updatedAt, turnCount: thread.turnCount || 0, lastModel: thread.lastModel || null, lastFamily: thread.lastFamily || null, lastUserMessage: thread.lastUserMessage || null, lastAssistantContent: thread.lastAssistantContent || null, lastToolCallCount: Array.isArray(thread.lastToolCalls) ? thread.lastToolCalls.length : 0, })); } export function getMockLlmThread(key) { return mockLlmThreads.get(String(key)) || null; } export function touchMockLlmThread(key, patch = {}) { if (!key) return null; const threadKey = String(key); const current = getMockLlmThread(threadKey) || { key: threadKey, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), turnCount: 0, history: [], lastModel: null, lastFamily: null, lastUserMessage: null, lastAssistantContent: null, lastToolCalls: [], lastToolResult: null, lastCodeLanguage: null, }; const next = { ...current, ...patch, updatedAt: new Date().toISOString(), }; mockLlmThreads.set(threadKey, next); return next; } export function recordMockLlmTurn(key, turn = {}) { if (!key) return null; const thread = touchMockLlmThread(key); const history = Array.isArray(thread.history) ? [...thread.history] : []; history.push({ timestamp: new Date().toISOString(), requestText: turn.requestText || null, responseText: turn.responseText || null, toolCalls: Array.isArray(turn.toolCalls) ? turn.toolCalls : [], toolResultText: turn.toolResultText || null, model: turn.model || null, family: turn.family || null, }); const next = touchMockLlmThread(key, { history: history.slice(-12), turnCount: (thread.turnCount || 0) + 1, lastModel: turn.model || thread.lastModel || null, lastFamily: turn.family || thread.lastFamily || null, lastUserMessage: turn.requestText || thread.lastUserMessage || null, lastAssistantContent: turn.responseText ?? thread.lastAssistantContent ?? null, lastToolCalls: Array.isArray(turn.toolCalls) ? turn.toolCalls : (thread.lastToolCalls ?? []), lastToolResult: turn.toolResultText ?? thread.lastToolResult ?? null, lastCodeLanguage: turn.codeLanguage ?? thread.lastCodeLanguage ?? null, }); return next; } export function resetMockLlmThreads() { mockLlmThreads = new Map(); } export function listSocketSessions() { return Array.from(socketSessions.values()).map((session) => ({ sid: session.sid, socketId: session.socketId, connected: session.connected === true, upgradedToWebSocket: session.upgradedToWebSocket === true, transport: session.transport || "polling", userId: session.userId || null, tokenPresent: Boolean(session.token), createdAt: session.createdAt, lastSeenAt: session.lastSeenAt, outgoingQueueLength: Array.isArray(session.pendingPackets) ? session.pendingPackets.length : 0, })); } export function getSocketSession(sid) { return socketSessions.get(String(sid)) || null; } export function registerSocketSession(session) { const next = { sid: session.sid, socketId: session.socketId || session.sid, connected: false, upgradedToWebSocket: false, transport: session.transport || "polling", token: session.token || null, userId: session.userId || null, createdAt: session.createdAt || new Date().toISOString(), lastSeenAt: new Date().toISOString(), pendingPackets: [], webSocket: null, }; socketSessions.set(next.sid, next); return next; } export function touchSocketSession(sid, patch = {}) { const session = getSocketSession(sid); if (!session) return null; Object.assign(session, patch, { lastSeenAt: new Date().toISOString() }); return session; } export function attachWebSocketToSession(sid, webSocket, options = {}) { return touchSocketSession(sid, { webSocket, ...(options.transport ? { transport: options.transport } : {}), ...(typeof options.upgraded === "boolean" ? { upgradedToWebSocket: options.upgraded } : {}), }); } export function dropSocketSession(sid) { const session = getSocketSession(sid); if (!session) return; try { session.webSocket?.close?.(); session.webSocket?.terminate?.(); session.webSocket?.destroy?.(); } catch { // noop } socketSessions.delete(session.sid); } export function queueSocketPacket(sid, packet) { const session = getSocketSession(sid); if (!session) return false; session.pendingPackets.push(String(packet)); session.lastSeenAt = new Date().toISOString(); return true; } export function drainSocketPackets(sid) { const session = getSocketSession(sid); if (!session) return []; const packets = [...session.pendingPackets]; session.pendingPackets = []; session.lastSeenAt = new Date().toISOString(); return packets; } export function appendSocketEvent(entry) { socketEventLog.push({ id: createMockId("sockevt"), timestamp: new Date().toISOString(), ...entry, }); } export function getSocketEventLog() { return [...socketEventLog]; } export function clearSocketEventLog() { socketEventLog = []; } export function resetSocketSessions() { for (const sid of socketSessions.keys()) { dropSocketSession(sid); } socketSessions.clear(); clearSocketEventLog(); } export function buildSocketReadyPayload(session) { return { sid: session.socketId, userId: session.userId || "mock-user", transport: session.transport, }; } export function createMockTunnel(payload = {}) { const now = new Date().toISOString(); return { id: crypto.randomUUID(), uuid: crypto.randomUUID(), name: String(payload.name || "Mock Tunnel").trim(), description: String(payload.description || "").trim(), isActive: payload.isActive ?? true, createdAt: now, updatedAt: now, }; } export function getMockUser() { const firstName = mockBehavior.firstName || fuzzyPick( "user:first", ["Test", "Casey", "Robin", "Jordan", "Taylor"], "Test", ); const lastName = mockBehavior.lastName || fuzzyPick( "user:last", ["User", "Walker", "Lane", "Rivera", "Stone"], "User", ); const username = mockBehavior.username || `${String(firstName).toLowerCase()}${fuzzyNumber("user:username", 10, 99)}`; return { _id: mockBehavior.userId || "user-123", telegramId: fuzzyNumber("user:telegramId", 10_000_000, 99_999_999), hasAccess: true, magicWord: mockBehavior.magicWord || fuzzyPick( "user:magicWord", ["alpha", "delta", "spruce", "harbor", "ember"], "alpha", ), firstName, lastName, username, role: "user", activeTeamId: "team-1", referral: {}, subscription: { hasActiveSubscription: false, plan: "FREE" }, settings: { dailySummariesEnabled: false, dailySummaryChatIds: [], autoCompleteEnabled: false, autoCompleteVisibility: "always", autoCompleteWhitelistChatIds: [], autoCompleteBlacklistChatIds: [], }, usage: { cycleBudgetUsd: 10, remainingUsd: 10, spentThisCycleUsd: 0, spentTodayUsd: 0, cycleStartDate: new Date().toISOString(), }, autoDeleteTelegramMessagesAfterDays: 30, autoDeleteThreadsAfterDays: 30, }; } export function getMockTeam() { const plan = mockBehavior.plan || "FREE"; const isActive = mockBehavior.planActive === "true"; const expiry = mockBehavior.planExpiry || null; const teamName = mockBehavior.teamName || fuzzyPick( "team:name", ["Personal", "Studio", "Field Ops", "Research"], "Personal", ); return { team: { _id: "team-1", name: teamName, slug: String(teamName).toLowerCase().replace(/\s+/g, "-"), createdBy: "test-user-123", isPersonal: true, maxMembers: 1, subscription: { plan, hasActiveSubscription: isActive, planExpiry: expiry, }, usage: { dailyTokenLimit: fuzzyNumber("team:dailyTokenLimit", 500, 5000), remainingTokens: fuzzyNumber("team:remainingTokens", 100, 5000), activeSessionCount: listSocketSessions().length, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, role: "ADMIN", }; } // ── Telegram Bot API mock state helpers ──────────────────────────────────── /** * Push a single Telegram Update object into the pending queue. * The queue is drained by each `getUpdates` poll. */ export function pushMockTelegramUpdate(update) { mockTelegramUpdates.push(update); } /** * Drain and return all pending Telegram updates. Each `getUpdates` call * should call this so each update is delivered exactly once. */ export function drainMockTelegramUpdates() { const updates = [...mockTelegramUpdates]; mockTelegramUpdates = []; return updates; } /** * Record a message sent by the bot (sendMessage, sendPhoto, etc.). * @param {object} entry - { method, body, message_id, ... } */ export function recordMockTelegramSent(entry) { mockTelegramSentMessages.push({ timestamp: new Date().toISOString(), ...entry, }); } /** Return all recorded outbound Telegram API calls (non-destructive). */ export function getMockTelegramSent() { return [...mockTelegramSentMessages]; } /** Increment and return the next mock message_id. */ export function nextMockTelegramMessageId() { const id = mockTelegramMessageIdSeq; mockTelegramMessageIdSeq += 1; return id; } /** Reset all Telegram mock state (updates queue, sent log, id counter). */ export function resetMockTelegram() { mockTelegramUpdates = []; mockTelegramSentMessages = []; mockTelegramMessageIdSeq = 1000; }