diff --git a/package-lock.json b/package-lock.json index fc8b2767a..6aad70d75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34990,6 +34990,7 @@ "devDependencies": { "@playwright/test": "^1.56.1", "@types/react": "~19.2.0", + "@types/ws": "^8.18.1", "eas-cli": "^16.24.1", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", @@ -34997,7 +34998,8 @@ "playwright": "^1.56.1", "typescript": "~5.9.2", "vitest": "^3.2.4", - "wrangler": "^4.59.1" + "wrangler": "^4.59.1", + "ws": "^8.20.0" } }, "packages/app/node_modules/expo-clipboard": { @@ -35021,6 +35023,28 @@ "react-native": "*" } }, + "packages/app/node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "packages/app/node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/packages/app/e2e/helpers/agent-bottom-anchor.ts b/packages/app/e2e/helpers/agent-bottom-anchor.ts index a0d2fd0c0..4bd8a3023 100644 --- a/packages/app/e2e/helpers/agent-bottom-anchor.ts +++ b/packages/app/e2e/helpers/agent-bottom-anchor.ts @@ -2,6 +2,7 @@ import { expect, type Page } from "@playwright/test"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { randomUUID } from "node:crypto"; +import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory"; import { buildHostWorkspaceRoute } from "../../src/utils/host-routes"; const NEAR_BOTTOM_THRESHOLD_PX = 72; @@ -83,33 +84,36 @@ export function createReplyTurn(label: string): { }; } +type DaemonClientConfig = { + url: string; + clientId: string; + clientType: "cli"; + webSocketFactory?: NodeWebSocketFactory; +}; + async function loadDaemonClientConstructor(): Promise< - new (config: { - url: string; - clientId: string; - clientType: "cli"; - }) => DaemonClientInstance + new ( + config: DaemonClientConfig, + ) => DaemonClientInstance > { const repoRoot = path.resolve(__dirname, "../../../../"); const moduleUrl = pathToFileURL( path.join(repoRoot, "packages/server/dist/server/server/exports.js"), ).href; const mod = (await import(moduleUrl)) as { - DaemonClient: new (config: { - url: string; - clientId: string; - clientType: "cli"; - }) => DaemonClientInstance; + DaemonClient: new (config: DaemonClientConfig) => DaemonClientInstance; }; return mod.DaemonClient; } export async function connectDaemonClient(): Promise { const DaemonClient = await loadDaemonClientConstructor(); + const webSocketFactory = createNodeWebSocketFactory(); const client = new DaemonClient({ url: getDaemonWsUrl(), clientId: `app-e2e-${randomUUID()}`, clientType: "cli", + webSocketFactory, }); await client.connect(); return client; diff --git a/packages/app/e2e/helpers/archive-tab.ts b/packages/app/e2e/helpers/archive-tab.ts index f75d7d60c..5c40d8ee1 100644 --- a/packages/app/e2e/helpers/archive-tab.ts +++ b/packages/app/e2e/helpers/archive-tab.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { expect, type Page } from "@playwright/test"; import { buildCreateAgentPreferences, buildSeededHost } from "./daemon-registry"; +import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory"; import { waitForWorkspaceTabsVisible } from "./workspace-tabs"; import { buildHostAgentDetailRoute, @@ -67,33 +68,36 @@ function buildSeededStoragePayload() { }; } +type ArchiveTabDaemonClientConfig = { + url: string; + clientId: string; + clientType: "cli"; + webSocketFactory?: NodeWebSocketFactory; +}; + async function loadDaemonClientConstructor(): Promise< - new (config: { - url: string; - clientId: string; - clientType: "cli"; - }) => ArchiveTabDaemonClient + new ( + config: ArchiveTabDaemonClientConfig, + ) => ArchiveTabDaemonClient > { const repoRoot = path.resolve(__dirname, "../../../../"); const moduleUrl = pathToFileURL( path.join(repoRoot, "packages/server/dist/server/server/exports.js"), ).href; const mod = (await import(moduleUrl)) as { - DaemonClient: new (config: { - url: string; - clientId: string; - clientType: "cli"; - }) => ArchiveTabDaemonClient; + DaemonClient: new (config: ArchiveTabDaemonClientConfig) => ArchiveTabDaemonClient; }; return mod.DaemonClient; } export async function connectArchiveTabDaemonClient(): Promise { const DaemonClient = await loadDaemonClientConstructor(); + const webSocketFactory = createNodeWebSocketFactory(); const client = new DaemonClient({ url: getDaemonWsUrl(), clientId: `app-e2e-archive-tab-${randomUUID()}`, clientType: "cli", + webSocketFactory, }); await client.connect(); return client; diff --git a/packages/app/e2e/helpers/node-ws-factory.ts b/packages/app/e2e/helpers/node-ws-factory.ts new file mode 100644 index 000000000..fed9af2a6 --- /dev/null +++ b/packages/app/e2e/helpers/node-ws-factory.ts @@ -0,0 +1,27 @@ +import WebSocket from "ws"; + +type WebSocketLike = { + readyState: number; + send: (data: string | Uint8Array | ArrayBuffer) => void; + close: (code?: number, reason?: string) => void; + binaryType?: string; + on?: (event: string, listener: (...args: any[]) => void) => void; + off?: (event: string, listener: (...args: any[]) => void) => void; + removeListener?: (event: string, listener: (...args: any[]) => void) => void; + addEventListener?: (event: string, listener: (event: any) => void) => void; + removeEventListener?: (event: string, listener: (event: any) => void) => void; + onopen?: ((event: any) => void) | null; + onclose?: ((event: any) => void) | null; + onerror?: ((event: any) => void) | null; + onmessage?: ((event: any) => void) | null; +}; + +export type NodeWebSocketFactory = ( + url: string, + options?: { headers?: Record }, +) => WebSocketLike; + +export function createNodeWebSocketFactory(): NodeWebSocketFactory { + return (url: string, options?: { headers?: Record }) => + new WebSocket(url, { headers: options?.headers }) as unknown as WebSocketLike; +} diff --git a/packages/app/e2e/helpers/terminal-perf.ts b/packages/app/e2e/helpers/terminal-perf.ts index 062562ce4..60e164625 100644 --- a/packages/app/e2e/helpers/terminal-perf.ts +++ b/packages/app/e2e/helpers/terminal-perf.ts @@ -2,6 +2,7 @@ import type { Page } from "@playwright/test"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { randomUUID } from "node:crypto"; +import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory"; import { buildHostWorkspaceRoute } from "../../src/utils/host-routes"; export type TerminalPerfDaemonClient = { @@ -43,33 +44,36 @@ function getServerId(): string { return serverId; } +type TerminalPerfDaemonClientConfig = { + url: string; + clientId: string; + clientType: "cli"; + webSocketFactory?: NodeWebSocketFactory; +}; + async function loadDaemonClientConstructor(): Promise< - new (config: { - url: string; - clientId: string; - clientType: "cli"; - }) => TerminalPerfDaemonClient + new ( + config: TerminalPerfDaemonClientConfig, + ) => TerminalPerfDaemonClient > { const repoRoot = path.resolve(__dirname, "../../../../"); const moduleUrl = pathToFileURL( path.join(repoRoot, "packages/server/dist/server/server/exports.js"), ).href; const mod = (await import(moduleUrl)) as { - DaemonClient: new (config: { - url: string; - clientId: string; - clientType: "cli"; - }) => TerminalPerfDaemonClient; + DaemonClient: new (config: TerminalPerfDaemonClientConfig) => TerminalPerfDaemonClient; }; return mod.DaemonClient; } export async function connectTerminalClient(): Promise { const DaemonClient = await loadDaemonClientConstructor(); + const webSocketFactory = createNodeWebSocketFactory(); const client = new DaemonClient({ url: getDaemonWsUrl(), clientId: `terminal-perf-${randomUUID()}`, clientType: "cli", + webSocketFactory, }); await client.connect(); return client; diff --git a/packages/app/package.json b/packages/app/package.json index f2064b26e..0059725d8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -108,6 +108,7 @@ "devDependencies": { "@playwright/test": "^1.56.1", "@types/react": "~19.2.0", + "@types/ws": "^8.18.1", "eas-cli": "^16.24.1", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", @@ -115,6 +116,7 @@ "playwright": "^1.56.1", "typescript": "~5.9.2", "vitest": "^3.2.4", - "wrangler": "^4.59.1" + "wrangler": "^4.59.1", + "ws": "^8.20.0" } } diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index 77ade96db..e01ac7ae2 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -230,8 +230,7 @@ try { assert( ids.some( (id) => - (id.startsWith("openai/") || id.startsWith("openrouter/openai/")) && - id.includes("codex"), + (id.startsWith("openai/") || id.startsWith("openrouter/openai/")) && id.includes("codex"), ), "opencode output should include at least one codex-optimized OpenAI model", );