Files
openhuman/test/e2e/mock-server.ts
T
Steven EnamakelandGitHub bfaabd3b86 fix/rename (#20)
* chore: update AlphaHuman version to 0.49.3 and configure updater plugin in tauri.conf.json

- Bumped the AlphaHuman package version in Cargo.lock to 0.49.3.
- Added updater configuration in tauri.conf.json to enable automatic updates with specified endpoints.

* refactor: rename AlphaHuman to OpenHuman across the codebase

- Updated all instances of "AlphaHuman" to "OpenHuman" in comments, tooltips, and constants to reflect the new branding.
- Adjusted relevant documentation and prompts to ensure consistency with the new name.

* refactor: update documentation and configurations to reflect OpenHuman branding

- Replaced all instances of "AlphaHuman" with "OpenHuman" in documentation, comments, and configuration files to ensure consistency with the new branding.
- Updated deep link URLs and related authentication flows to use the new "openhuman://" scheme.
- Adjusted paths and references in the skills system and other related files to align with the new project name.te

* refactor: standardize OpenHuman references and update configurations

- Replaced all instances of "AlphaHuman" with "OpenHuman" across documentation, comments, and configuration files to maintain branding consistency.
- Updated URLs and paths to reflect the new "openhuman://" scheme.
- Adjusted environment variable names and related settings to align with the new project identity.
- Enhanced documentation for clarity and accuracy regarding the OpenHuman framework.r

* chore: update subproject commit reference in skills directory

* refactor: update backend URL to reflect new service domain

- Changed default backend URL from "https://api.openhuman.xyz" to "https://api.tinyhumans.ai" in both JavaScript and Rust configuration files.
- Ensured consistency across the codebase regarding the new backend service endpoint.

* feat: introduce identity and migration modules for OpenHuman

- Added a new identity module to support AIEOS v1.1 JSON format, including structures for identity, psychology, linguistics, motivations, capabilities, physicality, history, and interests.
- Implemented a migration module to facilitate data migration from OpenClaw memory, including SQLite and Markdown sources, with detailed reporting on migration statistics and warnings.
- Established utility functions for handling multimodal content and image processing within the OpenHuman framework.
- Enhanced the agent system with new dispatcher and classifier functionalities to improve tool management and message classification.

* chore: remove Android project files and configurations

- Deleted various Android project files including .editorconfig, .gitignore, build.gradle.kts, gradle.properties, and others to clean up the project structure.
- Removed all related resources, layouts, and source files from the Android app directory to streamline the codebase.
- This cleanup is part of a larger effort to refactor and simplify the project structure.

* refactor: update login flow and remove Telegram integration

- Removed the TelegramLoginButton component and its references from the OAuthLoginSection, streamlining the login options.
- Updated the AppRoutes to remove the login route, reflecting changes in the authentication flow.
- Enhanced the RotatingTetrahedronCanvas component with improved geometry and lighting effects for better visual presentation.
- Adjusted the TypewriterGreeting component's styling for consistency.
- Cleaned up the Welcome page to integrate the OAuthLoginSection directly, improving user experience.

* chore: update subproject commit reference in skills directory

* chore: update test configurations and improve test assertions

- Modified test scripts in package.json to use a specific Vitest configuration file for consistency.
- Updated assertions in loader tests to ensure loading durations are non-negative.
- Enhanced tool loading tests to clarify expected behavior regarding localStorage and cache management.
- Adjusted agent tool registry tests to improve error handling and ensure accurate statistics.
- Refined device detection tests to reflect updated fallback URLs.

* fix: enhance parameter formatting and remove unused components

- Updated the `formatParameters` function to handle cases where schema properties are empty, returning a more informative response.
- Deleted the `DownloadScreen` component and associated device detection utilities to streamline the codebase and remove unused functionality.
- Adjusted tests to reflect changes in the tool loading and agent tool registry, ensuring accuracy in assertions.

* chore: simplify Vitest configuration by removing unused include patterns

- Updated the Vitest configuration to remove unnecessary test file patterns, streamlining the test setup for better clarity and maintainability.

* refactor: update paths and comments for AI configuration and file watching

- Modified Vite configuration to ignore only the `src-tauri` directory.
- Updated logging messages to reflect the correct path for writing AI configuration files.
- Adjusted fetch calls in the file watcher to use the new path for `TOOLS.md`.
- Revised comments and logic in Rust code to clarify the handling of AI configuration file paths, including legacy fallback options.

* chore: remove unused updater secrets from GitHub Actions workflow

- Deleted UPDATER_GIST_URL and UPDATER_GIST_ID environment variables from the package-and-publish workflow, streamlining the configuration.

* chore: comment out Vitest thresholds for clarity

- Commented out the thresholds section in the Vitest configuration to improve clarity and maintainability, as it is currently not in use.

* ran formatter

* chore: update updater public key in tauri configuration

- Replaced the existing public key in the updater plugin configuration with a new value to ensure proper functionality and security.

* chore: update ESLint configuration and refactor components

- Added `localStorage` and `sessionStorage` as readonly globals in ESLint configuration for better linting support.
- Removed unused imports from `SkillsPanel.tsx` to clean up the code.
- Changed the type of `watcherInterval` in `file-watcher.ts` for improved type safety.
- Refactored toast management logic in `Intelligence.tsx` to enhance clarity and maintainability.
- Simplified import statements in `IntelligenceProvider.tsx` for consistency.
- Streamlined object property shorthand in `agentToolRegistry.ts` for cleaner code.

* refactor: improve error handling and type safety in Intelligence component

- Enhanced toast notification logic to defer state updates, preventing potential issues with setState in effects.
- Updated the source filter dispatch to use a more specific type for improved type safety.

* refactor: enhance type safety across various components and services

- Updated type definitions from `any` to `unknown` in multiple files to improve type safety and prevent potential runtime errors.
- Refactored state management in `TauriCommandsPanel` to use more specific types.
- Adjusted context and parameters in several interfaces to ensure consistent typing.
- Added ESLint directive to `polyfills.ts` for intentional global assignments.
- Streamlined type handling in utility functions and API responses for better clarity and maintainability.

* refactor: streamline import statements and improve code clarity

- Consolidated import statements in `agentToolRegistry.ts` and `intelligenceSlice.ts` for better readability.
- Simplified the `createTestStore` function in `test-utils.tsx` to enhance code conciseness.
- Cleaned up the `isExecutionStepProgressEvent` function in `intelligence-chat-api.ts` for improved clarity and maintainability.
2026-03-26 17:04:46 -07:00

754 lines
22 KiB
TypeScript

/* eslint-disable */
// @ts-nocheck
/**
* Local HTTP mock server for E2E tests.
*
* Replaces the real backend so login-flow tests are fully self-contained.
* Uses only `node:http` and `node:crypto` — no extra npm dependencies.
*
* Also handles WebSocket upgrades for the Socket.IO/Engine.IO endpoint so the
* Rust-native socket manager doesn't crash from repeated connection failures.
*
* Default port: 18473 (high ephemeral, avoids Vite 1420 / Appium 4723 / backend 5005).
*/
import crypto from 'node:crypto';
import http from 'node:http';
const DEFAULT_PORT = 18_473;
// ---------------------------------------------------------------------------
// Request log
// ---------------------------------------------------------------------------
let requestLog = [];
export function getRequestLog() {
return [...requestLog];
}
export function clearRequestLog() {
requestLog = [];
}
// ---------------------------------------------------------------------------
// Mock behavior toggles — tests can change responses at runtime
// ---------------------------------------------------------------------------
let mockBehavior: Record<string, string> = {};
export function setMockBehavior(key: string, value: string) {
mockBehavior[key] = value;
}
export function resetMockBehavior() {
mockBehavior = {};
}
// ---------------------------------------------------------------------------
// Mock data — shapes taken from src/test/handlers.ts (MSW unit-test mocks)
// ---------------------------------------------------------------------------
const MOCK_JWT = 'e2e-mock-jwt-token';
const MOCK_USER = {
_id: 'test-user-123',
telegramId: 12345678,
hasAccess: true,
magicWord: 'alpha',
firstName: 'Test',
lastName: 'User',
username: 'testuser',
role: 'user',
activeTeamId: 'team-1',
referral: {},
subscription: { hasActiveSubscription: false, plan: 'FREE' },
settings: {
dailySummariesEnabled: false,
dailySummaryChatIds: [],
autoCompleteEnabled: false,
autoCompleteVisibility: 'always',
autoCompleteWhitelistChatIds: [],
autoCompleteBlacklistChatIds: [],
},
usage: {
cycleBudgetUsd: 10,
spentThisCycleUsd: 0,
spentTodayUsd: 0,
cycleStartDate: new Date().toISOString(),
},
autoDeleteTelegramMessagesAfterDays: 30,
autoDeleteThreadsAfterDays: 30,
};
// ---------------------------------------------------------------------------
// Dynamic mock data helpers
// ---------------------------------------------------------------------------
/**
* Build a team object whose subscription reflects the current mockBehavior.
* mockBehavior['plan'] → 'FREE' | 'BASIC' | 'PRO' (default: 'FREE')
* mockBehavior['planActive'] → 'true' to mark subscription active
* mockBehavior['planExpiry'] → ISO date string for renewal display
*/
function getMockTeam() {
const plan = mockBehavior['plan'] || 'FREE';
const isActive = mockBehavior['planActive'] === 'true';
const expiry = mockBehavior['planExpiry'] || null;
return {
team: {
_id: 'team-1',
name: 'Personal',
slug: 'personal',
createdBy: 'test-user-123',
isPersonal: true,
maxMembers: 1,
subscription: { plan, hasActiveSubscription: isActive, planExpiry: expiry },
usage: { dailyTokenLimit: 1000, remainingTokens: 1000, activeSessionCount: 0 },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
role: 'ADMIN',
};
}
// ---------------------------------------------------------------------------
// CORS helpers
// ---------------------------------------------------------------------------
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
function setCors(res) {
for (const [key, value] of Object.entries(CORS_HEADERS)) {
res.setHeader(key, value);
}
}
function json(res, status, body) {
setCors(res);
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(body));
}
// ---------------------------------------------------------------------------
// Route handling (HTTP)
// ---------------------------------------------------------------------------
function readBody(req) {
return new Promise(resolve => {
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
});
}
async function handleRequest(req, res) {
const method = req.method ?? 'GET';
const url = req.url ?? '/';
const body = await readBody(req);
// Log every request for test assertions
requestLog.push({ method, url, body, timestamp: Date.now() });
// CORS preflight
if (method === 'OPTIONS') {
setCors(res);
res.writeHead(204);
res.end();
return;
}
// Socket.IO polling transport (GET /socket.io/?EIO=4&transport=polling)
// Respond with Engine.IO OPEN packet so polling clients don't error out.
if (url.startsWith('/socket.io/')) {
const eioOpen = JSON.stringify({
sid: 'mock-sid-' + Date.now(),
upgrades: ['websocket'],
pingInterval: 25000,
pingTimeout: 20000,
});
// Engine.IO packet type 0 = OPEN, prefixed with byte count for polling
const packet = `${eioOpen.length + 1}:0${eioOpen}`;
setCors(res);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(packet);
return;
}
// POST /telegram/login-tokens/:token/consume
if (method === 'POST' && /^\/telegram\/login-tokens\/[^/]+\/consume\/?$/.test(url)) {
if (mockBehavior['token'] === 'expired') {
json(res, 401, { success: false, error: 'Token expired or invalid' });
return;
}
if (mockBehavior['token'] === 'invalid') {
json(res, 401, { success: false, error: 'Invalid token' });
return;
}
const jwt = mockBehavior['jwt'] ? `${MOCK_JWT}-${mockBehavior['jwt']}` : MOCK_JWT;
json(res, 200, { success: true, data: { jwtToken: jwt } });
return;
}
// GET /telegram/me
if (method === 'GET' && /^\/telegram\/me\/?(\?.*)?$/.test(url)) {
if (mockBehavior['session'] === 'revoked') {
json(res, 401, { success: false, error: 'Unauthorized' });
return;
}
json(res, 200, { success: true, data: MOCK_USER });
return;
}
// GET /teams
if (method === 'GET' && /^\/teams\/?(\?.*)?$/.test(url)) {
json(res, 200, { success: true, data: [getMockTeam()] });
return;
}
// POST /invite/redeem
if (method === 'POST' && /^\/invite\/redeem\/?$/.test(url)) {
json(res, 200, { success: true, data: { message: 'Invite code redeemed successfully' } });
return;
}
// GET /invite/my-codes
if (method === 'GET' && /^\/invite\/my-codes\/?(\?.*)?$/.test(url)) {
json(res, 200, { success: true, data: [] });
return;
}
// GET /invite/status
if (method === 'GET' && /^\/invite\/status/.test(url)) {
json(res, 200, { success: true, data: { valid: true } });
return;
}
// POST /telegram/settings/onboarding-complete
if (method === 'POST' && /^\/telegram\/settings\/onboarding-complete\/?$/.test(url)) {
json(res, 200, { success: true, data: {} });
return;
}
// GET /billing/current-plan (legacy alias)
// GET /payments/stripe/currentPlan
if (
(method === 'GET' && /^\/billing\/current-plan\/?(\?.*)?$/.test(url)) ||
(method === 'GET' && /^\/payments\/stripe\/currentPlan\/?(\?.*)?$/.test(url))
) {
const plan = mockBehavior['plan'] || 'FREE';
const isActive = mockBehavior['planActive'] === 'true';
const expiry = mockBehavior['planExpiry'] || null;
json(res, 200, {
success: true,
data: {
plan,
hasActiveSubscription: isActive,
planExpiry: expiry,
subscription: isActive
? {
id: 'sub_mock_123',
status: 'active',
currentPeriodEnd: expiry || new Date(Date.now() + 30 * 86400000).toISOString(),
}
: null,
},
});
return;
}
// POST /payments/stripe/purchasePlan
if (method === 'POST' && /^\/payments\/stripe\/purchasePlan\/?$/.test(url)) {
if (mockBehavior['purchaseError'] === 'true') {
json(res, 500, { success: false, error: 'Payment service unavailable' });
return;
}
json(res, 200, {
success: true,
data: {
checkoutUrl: 'http://127.0.0.1:18473/mock-checkout',
sessionId: 'cs_mock_' + Date.now(),
},
});
return;
}
// POST /payments/stripe/portal
if (method === 'POST' && /^\/payments\/stripe\/portal\/?$/.test(url)) {
json(res, 200, { success: true, data: { portalUrl: 'http://127.0.0.1:18473/mock-portal' } });
return;
}
// POST /payments/coinbase/charge
if (method === 'POST' && /^\/payments\/coinbase\/charge\/?$/.test(url)) {
if (mockBehavior['coinbaseError'] === 'true') {
json(res, 500, { success: false, error: 'Coinbase service unavailable' });
return;
}
const chargeId = 'coinbase_mock_' + Date.now();
json(res, 200, {
success: true,
data: {
gatewayTransactionId: chargeId,
hostedUrl: 'http://127.0.0.1:18473/mock-coinbase-checkout',
status: 'NEW',
expiresAt: new Date(Date.now() + 3600000).toISOString(),
},
});
return;
}
// GET /payments/coinbase/charge/:id — crypto payment status check
if (method === 'GET' && /^\/payments\/coinbase\/charge\/[^/]+\/?(\?.*)?$/.test(url)) {
const status = mockBehavior['cryptoStatus'] || 'NEW';
const underpaidAmount = mockBehavior['cryptoUnderpaidAmount'] || '0';
const overpaidAmount = mockBehavior['cryptoOverpaidAmount'] || '0';
json(res, 200, {
success: true,
data: {
status,
payment: {
status,
amountPaid:
status === 'UNDERPAID' ? '150.00' : status === 'OVERPAID' ? '350.00' : '250.00',
amountExpected: '250.00',
currency: 'USDC',
underpaidAmount,
overpaidAmount,
},
expiresAt: new Date(Date.now() + 3600000).toISOString(),
},
});
return;
}
// GET /auth/telegram/connect — OAuth connect URL for Telegram setup
if (method === 'GET' && /^\/auth\/telegram\/connect\/?(\?.*)?$/.test(url)) {
if (mockBehavior['telegramDuplicate'] === 'true') {
json(res, 409, { success: false, error: 'Telegram account already linked to another user' });
return;
}
json(res, 200, {
success: true,
data: { oauthUrl: 'http://127.0.0.1:18473/mock-telegram-oauth' },
});
return;
}
// POST /telegram/command — process a Telegram command
if (method === 'POST' && /^\/telegram\/command\/?$/.test(url)) {
if (mockBehavior['telegramUnauthorized'] === 'true') {
json(res, 403, { success: false, error: 'Unauthorized: insufficient permissions' });
return;
}
if (mockBehavior['telegramCommandError'] === 'true') {
json(res, 400, { success: false, error: 'Invalid command format' });
return;
}
json(res, 200, { success: true, data: { result: 'Command executed successfully' } });
return;
}
// GET /telegram/permissions — get current permission levels
if (method === 'GET' && /^\/telegram\/permissions\/?(\?.*)?$/.test(url)) {
const level = mockBehavior['telegramPermission'] || 'read';
json(res, 200, {
success: true,
data: { level, canRead: true, canWrite: level !== 'read', canInitiate: level === 'admin' },
});
return;
}
// POST /telegram/webhook/configure — configure webhook
if (method === 'POST' && /^\/telegram\/webhook\/configure\/?$/.test(url)) {
json(res, 200, {
success: true,
data: { webhookUrl: 'https://api.example.com/webhook/telegram', active: true },
});
return;
}
// POST /telegram/disconnect — disconnect Telegram skill
if (method === 'POST' && /^\/telegram\/disconnect\/?$/.test(url)) {
json(res, 200, { success: true, data: { disconnected: true } });
return;
}
// GET /skills — list available skills
if (method === 'GET' && /^\/skills\/?(\?.*)?$/.test(url)) {
json(res, 200, {
success: true,
data: [
{
id: 'telegram',
name: 'Telegram',
status: mockBehavior['telegramSkillStatus'] || 'installed',
setupComplete: mockBehavior['telegramSetupComplete'] === 'true',
},
{
id: 'notion',
name: 'Notion',
status: mockBehavior['notionSkillStatus'] || 'installed',
setupComplete: mockBehavior['notionSetupComplete'] === 'true',
},
{
id: 'email',
name: 'Email',
status: mockBehavior['gmailSkillStatus'] || 'installed',
setupComplete: mockBehavior['gmailSetupComplete'] === 'true',
},
],
});
return;
}
// GET /mock-telegram-oauth — mock OAuth page
if (method === 'GET' && /^\/mock-telegram-oauth\/?(\?.*)?$/.test(url)) {
setCors(res);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<html><body><h1>Mock Telegram OAuth</h1></body></html>');
return;
}
// GET /auth/notion/connect — OAuth connect URL for Notion setup
if (method === 'GET' && /^\/auth\/notion\/connect\/?(\?.*)?$/.test(url)) {
if (mockBehavior['notionTokenRevoked'] === 'true') {
json(res, 401, { success: false, error: 'OAuth token has been revoked' });
return;
}
const workspace = mockBehavior['notionWorkspace'] || "Test User's Workspace";
json(res, 200, {
success: true,
data: { oauthUrl: 'http://127.0.0.1:18473/mock-notion-oauth', workspace },
});
return;
}
// GET /notion/permissions — get current Notion permission level
if (method === 'GET' && /^\/notion\/permissions\/?(\?.*)?$/.test(url)) {
const level = mockBehavior['notionPermission'] || 'read';
json(res, 200, {
success: true,
data: {
level,
canRead: true,
canWrite: level === 'write' || level === 'admin',
canCreate: level === 'write' || level === 'admin',
},
});
return;
}
// GET /mock-notion-oauth — mock Notion OAuth page
if (method === 'GET' && /^\/mock-notion-oauth\/?(\?.*)?$/.test(url)) {
setCors(res);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(
'<html><body><h1>Mock Notion OAuth</h1><p>Authorize workspace access</p></body></html>'
);
return;
}
// GET /auth/google/connect — OAuth connect URL for Gmail/Email setup
if (method === 'GET' && /^\/auth\/google\/connect\/?(\?.*)?$/.test(url)) {
if (mockBehavior['gmailTokenRevoked'] === 'true') {
json(res, 401, { success: false, error: 'OAuth token has been revoked' });
return;
}
if (mockBehavior['gmailTokenExpired'] === 'true') {
json(res, 401, { success: false, error: 'OAuth token has expired' });
return;
}
json(res, 200, {
success: true,
data: { oauthUrl: 'http://127.0.0.1:18473/mock-google-oauth' },
});
return;
}
// GET /gmail/permissions — get current Gmail/Email permission level
if (method === 'GET' && /^\/gmail\/permissions\/?(\?.*)?$/.test(url)) {
const level = mockBehavior['gmailPermission'] || 'read';
json(res, 200, {
success: true,
data: {
level,
canRead: true,
canWrite: level === 'write' || level === 'admin',
canInitiate: level === 'admin',
},
});
return;
}
// POST /gmail/disconnect — disconnect Gmail/Email skill
if (method === 'POST' && /^\/gmail\/disconnect\/?$/.test(url)) {
json(res, 200, { success: true, data: { disconnected: true } });
return;
}
// GET /gmail/emails — fetch emails (mock list)
if (method === 'GET' && /^\/gmail\/emails\/?(\?.*)?$/.test(url)) {
json(res, 200, {
success: true,
data: [
{
id: 'msg-1',
subject: 'Welcome to OpenHuman',
from: 'team@openhuman.com',
date: new Date().toISOString(),
snippet: 'Welcome to the platform!',
hasAttachments: false,
},
{
id: 'msg-2',
subject: 'Weekly Crypto Report',
from: 'reports@crypto.com',
date: new Date(Date.now() - 86400000).toISOString(),
snippet: 'Your weekly summary is ready.',
hasAttachments: true,
},
],
});
return;
}
// GET /mock-google-oauth — mock Google OAuth page
if (method === 'GET' && /^\/mock-google-oauth\/?(\?.*)?$/.test(url)) {
setCors(res);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<html><body><h1>Mock Google OAuth</h1><p>Authorize email access</p></body></html>');
return;
}
// Catch-all — prevents app crashes from unexpected API calls
json(res, 200, { success: true, data: {} });
}
// ---------------------------------------------------------------------------
// WebSocket upgrade handler (minimal Engine.IO + Socket.IO)
//
// The Rust SocketManager connects via WebSocket to
// ws://host/socket.io/?EIO=4&transport=websocket
// and expects:
// 1. Engine.IO OPEN packet (type 0): JSON with sid, pingInterval, etc.
// 2. Socket.IO CONNECT ACK (type 40): JSON with sid
// 3. Periodic Engine.IO PING (type 2) which we respond to with PONG (3)
//
// Without this, the Rust ws_loop retries forever and may destabilize the app.
// ---------------------------------------------------------------------------
function handleWebSocketUpgrade(req, socket, head) {
// Only handle /socket.io/ WebSocket upgrades
if (!req.url?.startsWith('/socket.io/')) {
socket.destroy();
return;
}
// Perform WebSocket handshake (RFC 6455)
const key = req.headers['sec-websocket-key'];
if (!key) {
socket.destroy();
return;
}
const acceptKey = crypto
.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-5AB5DC085B11')
.digest('base64');
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
'\r\n'
);
const mockSid = 'mock-ws-' + Date.now();
// Send Engine.IO OPEN packet (type 0)
const eioOpen = JSON.stringify({
sid: mockSid,
upgrades: [],
pingInterval: 25000,
pingTimeout: 60000,
maxPayload: 1000000,
});
sendWsText(socket, `0${eioOpen}`);
// Buffer for partial frames
let buffer = Buffer.alloc(0);
socket.on('data', chunk => {
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length >= 2) {
const firstByte = buffer[0];
const opcode = firstByte & 0x0f;
const secondByte = buffer[1];
const masked = (secondByte & 0x80) !== 0;
let payloadLen = secondByte & 0x7f;
let offset = 2;
if (payloadLen === 126) {
if (buffer.length < 4) return; // need more data
payloadLen = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLen === 127) {
if (buffer.length < 10) return;
payloadLen = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
const maskSize = masked ? 4 : 0;
const totalLen = offset + maskSize + payloadLen;
if (buffer.length < totalLen) return; // need more data
let payload = buffer.subarray(offset + maskSize, totalLen);
if (masked) {
const mask = buffer.subarray(offset, offset + 4);
payload = Buffer.from(payload); // make writable copy
for (let i = 0; i < payload.length; i++) {
payload[i] ^= mask[i % 4];
}
}
// Consume the frame from the buffer
buffer = buffer.subarray(totalLen);
// Handle by opcode
if (opcode === 0x08) {
// Close
socket.end();
return;
}
if (opcode === 0x09) {
// Ping → Pong
sendWsFrame(socket, 0x0a, payload);
continue;
}
if (opcode === 0x01) {
// Text frame
const text = payload.toString('utf-8');
handleSocketIOMessage(socket, text, mockSid);
}
}
});
socket.on('error', () => {});
socket.on('close', () => {});
}
function handleSocketIOMessage(socket, text, sid) {
// Engine.IO PING (type "2") → respond with PONG ("3")
if (text === '2') {
sendWsText(socket, '3');
return;
}
// Socket.IO CONNECT (type "40") → respond with CONNECT ACK
if (text.startsWith('40')) {
sendWsText(socket, `40{"sid":"${sid}"}`);
return;
}
// Socket.IO EVENT (type "42") → log but ignore
// e.g. 42["tool:sync", {...}]
}
function sendWsText(socket, text) {
sendWsFrame(socket, 0x01, Buffer.from(text, 'utf-8'));
}
function sendWsFrame(socket, opcode, payload) {
if (socket.destroyed) return;
const len = payload.length;
let header;
if (len < 126) {
header = Buffer.alloc(2);
header[0] = 0x80 | opcode; // FIN + opcode
header[1] = len;
} else if (len < 65536) {
header = Buffer.alloc(4);
header[0] = 0x80 | opcode;
header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
header = Buffer.alloc(10);
header[0] = 0x80 | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
try {
socket.write(header);
socket.write(payload);
} catch {
// socket may have been destroyed
}
}
// ---------------------------------------------------------------------------
// Server lifecycle
// ---------------------------------------------------------------------------
let server = null;
const openSockets = new Set();
export function startMockServer(port = DEFAULT_PORT) {
return new Promise((resolve, reject) => {
server = http.createServer((req, res) => {
handleRequest(req, res).catch(err => {
console.error('[MockServer] Unhandled error:', err);
json(res, 500, { success: false, error: 'Internal mock error' });
});
});
// Track all connections so stopMockServer can force-close them
server.on('connection', socket => {
openSockets.add(socket);
socket.on('close', () => openSockets.delete(socket));
});
// Handle WebSocket upgrades for Socket.IO
server.on('upgrade', (req, socket, head) => {
handleWebSocketUpgrade(req, socket, head);
});
server.on('error', reject);
server.listen(port, '127.0.0.1', () => {
console.log(`[MockServer] Listening on http://127.0.0.1:${port}`);
resolve({ port });
});
});
}
export function stopMockServer() {
return new Promise(resolve => {
if (!server) {
resolve();
return;
}
// Destroy all open sockets so server.close() doesn't hang
for (const socket of openSockets) {
socket.destroy();
}
openSockets.clear();
server.close(() => {
console.log('[MockServer] Stopped');
server = null;
resolve();
});
});
}