mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
501 lines
13 KiB
JavaScript
501 lines
13 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;
|
|
|
|
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",
|
|
};
|
|
}
|