Files
openhuman/scripts/mock-api/state.mjs
T

556 lines
15 KiB
JavaScript

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;
}