From 242cdca05865cb5bc2ee0a2bbfc83ea0b157ba5c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 07:42:17 -0700 Subject: [PATCH] fix(docker): keep gateway ws client self-contained (#107561) * fix(docker): keep gateway ws client self-contained * test(raft): narrow optional runtime schema * test(tlon): narrow signed URL mock input * fix(tui): type searchable selector metadata --- scripts/lib/gateway-ws-client.ts | 11 ++++++++++- src/tui/components/searchable-select-list.ts | 18 +++++++++++------- src/tui/components/selectors.ts | 6 +++--- test/scripts/gateway-ws-client.test.ts | 10 +++++++++- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/scripts/lib/gateway-ws-client.ts b/scripts/lib/gateway-ws-client.ts index 1a53ee2d7054..bc2cf8e9869f 100644 --- a/scripts/lib/gateway-ws-client.ts +++ b/scripts/lib/gateway-ws-client.ts @@ -1,7 +1,16 @@ // Gateway Ws Client script supports OpenClaw repository automation. +import { Buffer } from "node:buffer"; import { randomUUID } from "node:crypto"; import WebSocket from "ws"; -import { rawDataToString } from "../../src/infra/ws.js"; + +// Release Docker images ship this script without src/, so keep transport +// normalization self-contained instead of importing a core-only helper. +export function rawDataToString(data: WebSocket.RawData): string { + if (Array.isArray(data)) { + return Buffer.concat(data).toString("utf8"); + } + return data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : data.toString("utf8"); +} type GatewayReqFrame = { type: "req"; id: string; method: string; params?: unknown }; type GatewayResFrame = { diff --git a/src/tui/components/searchable-select-list.ts b/src/tui/components/searchable-select-list.ts index 53329477aa89..313f1087e480 100644 --- a/src/tui/components/searchable-select-list.ts +++ b/src/tui/components/searchable-select-list.ts @@ -22,12 +22,16 @@ export interface SearchableSelectListTheme extends SelectListTheme { matchHighlight: (text: string) => string; } +export interface SearchableSelectItem extends SelectItem { + searchText?: string; +} + /** * A select list with a search input at the top for fuzzy filtering. */ export class SearchableSelectList implements Component { - private items: SelectItem[]; - private filteredItems: SelectItem[]; + private items: SearchableSelectItem[]; + private filteredItems: SearchableSelectItem[]; private selectedIndex = 0; private maxVisible: number; private theme: SearchableSelectListTheme; @@ -44,7 +48,7 @@ export class SearchableSelectList implements Component { // Keep a small right margin so we don't risk wrapping due to styling/terminal quirks. private static readonly RIGHT_MARGIN_WIDTH = 2; - constructor(items: SelectItem[], maxVisible: number, theme: SearchableSelectListTheme) { + constructor(items: SearchableSelectItem[], maxVisible: number, theme: SearchableSelectListTheme) { this.items = items; this.filteredItems = items; this.maxVisible = maxVisible; @@ -81,10 +85,10 @@ export class SearchableSelectList implements Component { * 2. Exact substring in description * 3. Fuzzy match (lowest priority) */ - private smartFilter(query: string): SelectItem[] { + private smartFilter(query: string): SearchableSelectItem[] { const q = normalizeLowercaseStringOrEmpty(query); - type ScoredItem = { item: SelectItem; tier: number; score: number }; - type FuzzyCandidate = { item: SelectItem; searchText: string }; + type ScoredItem = { item: SearchableSelectItem; tier: number; score: number }; + type FuzzyCandidate = { item: SearchableSelectItem; searchText: string }; const scoredItems: ScoredItem[] = []; const fuzzyCandidates: FuzzyCandidate[] = []; @@ -107,7 +111,7 @@ export class SearchableSelectList implements Component { continue; } // Tier 3: Fuzzy match - const searchText = (item as { searchText?: string }).searchText ?? ""; + const searchText = item.searchText ?? ""; fuzzyCandidates.push({ item, searchText: normalizeLowercaseStringOrEmpty( diff --git a/src/tui/components/selectors.ts b/src/tui/components/selectors.ts index 9f5316462213..4d5d2f821c72 100644 --- a/src/tui/components/selectors.ts +++ b/src/tui/components/selectors.ts @@ -1,15 +1,15 @@ // Selector components adapt Pi TUI list controls for OpenClaw settings. -import { type SelectItem, type SettingItem, SettingsList } from "@earendil-works/pi-tui"; +import { type SettingItem, SettingsList } from "@earendil-works/pi-tui"; import { filterableSelectListTheme, searchableSelectListTheme, settingsListTheme, } from "../theme/theme.js"; import { FilterableSelectList, type FilterableSelectItem } from "./filterable-select-list.js"; -import { SearchableSelectList } from "./searchable-select-list.js"; +import { SearchableSelectList, type SearchableSelectItem } from "./searchable-select-list.js"; /** Creates a themed searchable select list for TUI overlays. */ -export function createSearchableSelectList(items: SelectItem[], maxVisible = 7) { +export function createSearchableSelectList(items: SearchableSelectItem[], maxVisible = 7) { return new SearchableSelectList(items, maxVisible, searchableSelectListTheme); } diff --git a/test/scripts/gateway-ws-client.test.ts b/test/scripts/gateway-ws-client.test.ts index 129595213aa2..c296524d6656 100644 --- a/test/scripts/gateway-ws-client.test.ts +++ b/test/scripts/gateway-ws-client.test.ts @@ -3,7 +3,7 @@ import { createServer, type Server } from "node:http"; import type { Duplex } from "node:stream"; import { afterEach, describe, expect, it } from "vitest"; import { WebSocket, WebSocketServer } from "ws"; -import { createGatewayWsClient } from "../../scripts/dev/gateway-ws-client.js"; +import { createGatewayWsClient, rawDataToString } from "../../scripts/dev/gateway-ws-client.js"; let server: Server | undefined; let wss: WebSocketServer | undefined; @@ -88,6 +88,14 @@ async function listenStalledUpgrade(): Promise<{ close: () => Promise; url } describe("createGatewayWsClient", () => { + it("decodes every ws raw-data shape without core source files", () => { + expect(rawDataToString(Buffer.from("buffer"))).toBe("buffer"); + expect(rawDataToString(Uint8Array.from(Buffer.from("array-buffer")).buffer)).toBe( + "array-buffer", + ); + expect(rawDataToString([Buffer.from("frag"), Buffer.from("ments")])).toBe("fragments"); + }); + it("rejects pending RPC requests when the client closes", async () => { const url = await listen(() => {}); const client = createGatewayWsClient({ url });