mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
287 lines
7.6 KiB
JavaScript
287 lines
7.6 KiB
JavaScript
import http from "node:http";
|
|
|
|
import { handleAdmin } from "./admin.mjs";
|
|
import {
|
|
json,
|
|
normalizeHeaders,
|
|
readBody,
|
|
requestOrigin,
|
|
setCors,
|
|
tryParseJson,
|
|
} from "./http.mjs";
|
|
import { handleAuth } from "./routes/auth.mjs";
|
|
import { handleAudio } from "./routes/audio.mjs";
|
|
import { handleConversations } from "./routes/conversations.mjs";
|
|
import { handleCron } from "./routes/cron.mjs";
|
|
import { handleIntegrations } from "./routes/integrations.mjs";
|
|
import { handleInvites } from "./routes/invites.mjs";
|
|
import { handleLlmCompletions } from "./routes/llm.mjs";
|
|
import { handleOAuth } from "./routes/oauth.mjs";
|
|
import { handlePayments } from "./routes/payments.mjs";
|
|
import { handleUser } from "./routes/user.mjs";
|
|
import { handleVersion } from "./routes/version.mjs";
|
|
import { handleWebhooks } from "./routes/webhooks.mjs";
|
|
import { handleSocketRequest, handleWebSocketUpgrade } from "./socket.mjs";
|
|
import {
|
|
appendRequest,
|
|
behavior,
|
|
DEFAULT_PORT,
|
|
hashString,
|
|
MAX_PORT_RETRY_ATTEMPTS,
|
|
openSockets,
|
|
parseBehaviorJson,
|
|
sleep,
|
|
} from "./state.mjs";
|
|
|
|
let server = null;
|
|
|
|
// Order matters: admin & socket.io short-circuit early; the rest fall through
|
|
// in domain order so the cheapest predicates run first.
|
|
const ROUTE_HANDLERS = [
|
|
handleOAuth,
|
|
handleAuth,
|
|
handleUser,
|
|
handleInvites,
|
|
handlePayments,
|
|
handleAudio,
|
|
// LLM completions must run before the catch-all stub in
|
|
// `handleIntegrations` so keyword-driven test scripts can override
|
|
// the default "Hello from e2e mock agent" reply.
|
|
handleLlmCompletions,
|
|
handleIntegrations,
|
|
handleWebhooks,
|
|
handleCron,
|
|
handleConversations,
|
|
handleVersion,
|
|
];
|
|
|
|
async function handleRequest(req, res) {
|
|
const method = req.method ?? "GET";
|
|
const url = req.url ?? "/";
|
|
const body = await readBody(req);
|
|
const parsedBody = tryParseJson(body);
|
|
const origin = requestOrigin(req);
|
|
|
|
appendRequest({
|
|
method,
|
|
url,
|
|
body,
|
|
headers: normalizeHeaders(req.headers),
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
if (method === "OPTIONS") {
|
|
setCors(res);
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const ctx = {
|
|
method,
|
|
url,
|
|
body,
|
|
parsedBody,
|
|
origin,
|
|
req,
|
|
res,
|
|
getPort: getMockServerPort,
|
|
};
|
|
|
|
if (handleAdmin(ctx)) return;
|
|
|
|
const maybeShortCircuit = await maybeApplyGlobalBehavior(ctx);
|
|
if (maybeShortCircuit) return;
|
|
|
|
if (url.startsWith("/socket.io/")) {
|
|
handleSocketRequest(ctx);
|
|
return;
|
|
}
|
|
|
|
for (const handler of ROUTE_HANDLERS) {
|
|
if (await handler(ctx)) return;
|
|
}
|
|
|
|
// Catch-all: fail fast so tests notice missing mock endpoints.
|
|
console.log(`[MockServer] UNHANDLED ${method} ${url}`);
|
|
json(res, 404, {
|
|
success: false,
|
|
error: `Mock server: no handler for ${method} ${url}`,
|
|
});
|
|
}
|
|
|
|
function requestHash(ctx) {
|
|
return hashString(`${ctx.method}:${ctx.url}:${ctx.body || ""}`);
|
|
}
|
|
|
|
function ruleMatches(rule, ctx) {
|
|
if (!rule || typeof rule !== "object") return false;
|
|
if (rule.method && String(rule.method).toUpperCase() !== ctx.method)
|
|
return false;
|
|
if (typeof rule.path === "string" && rule.path !== ctx.url) return false;
|
|
if (typeof rule.pathRegex === "string") {
|
|
try {
|
|
const regex = new RegExp(rule.pathRegex);
|
|
if (!regex.test(ctx.url)) return false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
if (typeof rule.contains === "string" && !ctx.url.includes(rule.contains)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function maybeApplyGlobalBehavior(ctx) {
|
|
const mockBehavior = behavior();
|
|
const baseDelay = Number(mockBehavior.globalDelayMs || 0);
|
|
const jitterMax = Number(mockBehavior.globalJitterMs || 0);
|
|
const jitter =
|
|
Number.isFinite(jitterMax) && jitterMax > 0
|
|
? requestHash(ctx) % Math.min(jitterMax, 5000)
|
|
: 0;
|
|
const totalDelay = Math.max(0, baseDelay) + jitter;
|
|
if (totalDelay > 0) {
|
|
await sleep(Math.min(totalDelay, 30_000));
|
|
}
|
|
|
|
const rules = parseBehaviorJson("httpFaultRules", []);
|
|
if (!Array.isArray(rules)) return false;
|
|
|
|
for (const rule of rules) {
|
|
if (!ruleMatches(rule, ctx)) continue;
|
|
const status = Number(rule.status || 500);
|
|
const body =
|
|
rule.body && typeof rule.body === "object"
|
|
? rule.body
|
|
: {
|
|
success: false,
|
|
error: rule.error || "Injected mock fault",
|
|
};
|
|
console.warn(
|
|
`[MockServer] Injected fault ${ctx.method} ${ctx.url} -> ${status}`,
|
|
);
|
|
json(ctx.res, status, body);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function getMockServerPort() {
|
|
const address = server?.address();
|
|
return typeof address === "object" && address ? address.port : null;
|
|
}
|
|
|
|
function createServerInstance() {
|
|
const nextServer = http.createServer((req, res) => {
|
|
handleRequest(req, res).catch((err) => {
|
|
console.error("[MockServer] Unhandled error:", err);
|
|
json(res, 500, { success: false, error: "Internal mock error" });
|
|
});
|
|
});
|
|
nextServer.on("connection", (socket) => {
|
|
openSockets.add(socket);
|
|
socket.on("close", () => openSockets.delete(socket));
|
|
});
|
|
nextServer.on("upgrade", (req, socket, head) =>
|
|
handleWebSocketUpgrade(req, socket, head),
|
|
);
|
|
return nextServer;
|
|
}
|
|
|
|
function listen(serverInstance, port) {
|
|
return new Promise((resolve, reject) => {
|
|
const onError = (err) => {
|
|
serverInstance.off("listening", onListening);
|
|
reject(err);
|
|
};
|
|
const onListening = () => {
|
|
serverInstance.off("error", onError);
|
|
const address = serverInstance.address();
|
|
const resolvedPort =
|
|
typeof address === "object" && address ? address.port : port;
|
|
resolve(resolvedPort);
|
|
};
|
|
serverInstance.once("error", onError);
|
|
serverInstance.once("listening", onListening);
|
|
serverInstance.listen(port, "127.0.0.1");
|
|
});
|
|
}
|
|
|
|
export async function startMockServer(port = DEFAULT_PORT, options = {}) {
|
|
if (server) {
|
|
return { port: getMockServerPort() ?? port, alreadyRunning: true };
|
|
}
|
|
|
|
const preferredPort =
|
|
Number.isInteger(port) && port > 0 ? port : DEFAULT_PORT;
|
|
const retryIfInUse = options.retryIfInUse === true;
|
|
const candidatePorts = retryIfInUse
|
|
? [
|
|
preferredPort,
|
|
...Array.from(
|
|
{ length: MAX_PORT_RETRY_ATTEMPTS },
|
|
(_, i) => preferredPort + i + 1,
|
|
),
|
|
0,
|
|
]
|
|
: [preferredPort];
|
|
|
|
let lastError = null;
|
|
for (const candidatePort of candidatePorts) {
|
|
const nextServer = createServerInstance();
|
|
try {
|
|
const resolvedPort = await listen(nextServer, candidatePort);
|
|
server = nextServer;
|
|
const retryNote =
|
|
resolvedPort === preferredPort
|
|
? ""
|
|
: ` (preferred ${preferredPort} unavailable)`;
|
|
console.log(
|
|
`[MockServer] Listening on http://127.0.0.1:${resolvedPort}${retryNote}`,
|
|
);
|
|
return {
|
|
port: resolvedPort,
|
|
alreadyRunning: false,
|
|
requestedPort: preferredPort,
|
|
retried: resolvedPort !== preferredPort,
|
|
};
|
|
} catch (err) {
|
|
try {
|
|
nextServer.close();
|
|
} catch {
|
|
// The failed candidate may never have reached the listening state.
|
|
}
|
|
lastError = err;
|
|
if (!retryIfInUse || err?.code !== "EADDRINUSE") {
|
|
throw err;
|
|
}
|
|
console.warn(
|
|
`[MockServer] Port ${candidatePort} unavailable; trying another local port`,
|
|
);
|
|
}
|
|
}
|
|
|
|
throw lastError ?? new Error("Mock server failed to start");
|
|
}
|
|
|
|
export function stopMockServer() {
|
|
return new Promise((resolve) => {
|
|
if (!server) {
|
|
resolve();
|
|
return;
|
|
}
|
|
for (const socket of openSockets) {
|
|
socket.destroy();
|
|
}
|
|
openSockets.clear();
|
|
server.close(() => {
|
|
console.log("[MockServer] Stopped");
|
|
server = null;
|
|
resolve();
|
|
});
|
|
});
|
|
}
|