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
This commit is contained in:
Peter Steinberger
2026-07-14 07:42:17 -07:00
committed by GitHub
parent 3b9c0efadd
commit 242cdca058
4 changed files with 33 additions and 12 deletions

View File

@@ -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 = {

View File

@@ -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(

View File

@@ -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);
}

View File

@@ -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<void>; 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 });