import path from "node:path"; import { fileURLToPath } from "node:url"; import { createServer, type Plugin, type ViteDevServer } from "vite"; import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../src/gateway/control-ui-contract.js"; import { createControlUiMockBootstrapConfig, createControlUiMockGatewayInitScript, type ControlUiMockGatewayScenario, } from "../ui/src/test-helpers/control-ui-e2e.ts"; type CliOptions = { host: string; port: number; }; type SessionListOptions = { hasMore: boolean; nextOffset: number | null; offset?: number; totalCount: number; }; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const uiRoot = path.join(repoRoot, "ui"); function parseArgs(args: string[]): CliOptions { const options: CliOptions = { host: "127.0.0.1", port: 5187 }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; if (arg === "--host") { options.host = args[++i] ?? options.host; } else if (arg.startsWith("--host=")) { options.host = arg.slice("--host=".length) || options.host; } else if (arg === "--port") { options.port = parsePort(args[++i], options.port); } else if (arg.startsWith("--port=")) { options.port = parsePort(arg.slice("--port=".length), options.port); } } return options; } function parsePort(value: string | undefined, fallback: number): number { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 && parsed < 65_536 ? parsed : fallback; } function sessionRow(key: string, label: string, updatedAt: number) { return { contextTokens: null, displayName: label, hasActiveRun: false, key, kind: "direct", label, model: "gpt-5.5", modelProvider: "openai", status: "done", totalTokens: 0, updatedAt, }; } function sessionsListResponse(sessions: unknown[], options: SessionListOptions) { return { count: sessions.length, defaults: { contextTokens: null, model: "gpt-5.5", modelProvider: "openai", }, hasMore: options.hasMore, limitApplied: 50, nextOffset: options.nextOffset, offset: options.offset ?? 0, path: "", sessions, totalCount: options.totalCount, ts: Date.now(), }; } function createChatPickerScenario(): ControlUiMockGatewayScenario { const baseTime = Date.parse("2026-05-22T09:00:00.000Z"); return { assistantAgentId: "openclaw-mock", assistantName: "OpenClaw mock", defaultAgentId: "openclaw-mock", historyMessages: [ { content: [ { text: 'Mock Control UI is running. Open the chat picker, search for "telegram", then use Load more.', type: "text", }, ], role: "assistant", timestamp: baseTime, }, ], methodResponses: { "sessions.list": { cases: [ { match: { offset: 50, search: "telegram" }, response: sessionsListResponse( [ sessionRow("agent:telegram-51", "Telegram archive page 51", baseTime - 180_000), sessionRow("agent:telegram-52", "Telegram archive page 52", baseTime - 240_000), ], { hasMore: false, nextOffset: null, offset: 50, totalCount: 4 }, ), }, { match: { search: "telegram" }, response: sessionsListResponse( [ sessionRow("agent:telegram", "Telegram follow-up", baseTime - 60_000), sessionRow("agent:telegram-mobile", "Telegram mobile handoff", baseTime - 120_000), ], { hasMore: true, nextOffset: 50, totalCount: 4 }, ), }, { match: {}, response: sessionsListResponse( [ sessionRow("agent:alpha", "Alpha planning", baseTime - 1_000), sessionRow("agent:design", "Design review", baseTime - 30_000), ], { hasMore: true, nextOffset: 50, totalCount: 125 }, ), }, ], }, }, models: [{ id: "gpt-5.5", name: "gpt-5.5", provider: "openai" }], sessionKey: "agent:alpha", }; } function escapeScriptContent(script: string): string { return script.replaceAll(" { res.statusCode = 200; res.setHeader("content-type", "application/json"); res.end(bootstrapBody); }); }, name: "openclaw-control-ui-mock-gateway", transformIndexHtml(html) { return html.replace( "", ` \n `, ); }, }; } function hostForUrl(boundAddress: string, requestedHost: string): string { const host = boundAddress === "0.0.0.0" || boundAddress === "::" ? requestedHost : boundAddress; const reachableHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host; return reachableHost.includes(":") ? `[${reachableHost}]` : reachableHost; } function resolveServerUrl(server: ViteDevServer, requestedHost: string): string { const address = server.httpServer?.address(); if (!address || typeof address === "string") { throw new Error("Control UI mock server did not expose a TCP port"); } return `http://${hostForUrl(address.address, requestedHost)}:${address.port}/chat`; } async function waitForShutdown(): Promise { await new Promise((resolve) => { process.once("SIGINT", resolve); process.once("SIGTERM", resolve); }); } const options = parseArgs(process.argv.slice(2)); const scenario = createChatPickerScenario(); const server = await createServer({ base: "/", cacheDir: path.join(repoRoot, ".artifacts", "control-ui-mock-vite"), clearScreen: false, configFile: false, define: { OPENCLAW_CONTROL_UI_BUILD_ID: JSON.stringify("mock"), }, logLevel: "error", optimizeDeps: { include: ["lit/directives/repeat.js"], }, plugins: [createMockGatewayPlugin(scenario)], publicDir: path.join(uiRoot, "public"), root: uiRoot, server: { host: options.host, port: options.port, strictPort: false, }, }); await server.listen(); console.log(`[control-ui-mock] ${resolveServerUrl(server, options.host)}`); await waitForShutdown(); await server.close();