mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-30 01:06:11 +00:00
fix(agents): restore embedded pi and websocket typings
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClientOptions } from "ws";
|
||||
import type {
|
||||
ClientEvent,
|
||||
OpenAIWebSocketEvent,
|
||||
@@ -34,12 +35,12 @@ const { MockWebSocket } = vi.hoisted(() => {
|
||||
|
||||
readyState: number = MockWebSocket.CONNECTING;
|
||||
url: string;
|
||||
options: Record<string, unknown>;
|
||||
options: ClientOptions | undefined;
|
||||
sentMessages: string[] = [];
|
||||
|
||||
private _listeners: Map<string, AnyFn[]> = new Map();
|
||||
|
||||
constructor(url: string, options?: Record<string, unknown>) {
|
||||
constructor(url: string, options?: ClientOptions) {
|
||||
this.url = url;
|
||||
this.options = options ?? {};
|
||||
MockWebSocket.lastInstance = this;
|
||||
@@ -167,8 +168,7 @@ function buildManager(opts?: ConstructorParameters<typeof OpenAIWebSocketManager
|
||||
return new OpenAIWebSocketManager({
|
||||
// Use faster backoff in tests to avoid slow timer waits
|
||||
backoffDelaysMs: [10, 20, 40, 80, 160],
|
||||
socketFactory: (url, options) =>
|
||||
new MockWebSocket(url, options as Record<string, unknown>) as never,
|
||||
socketFactory: (url, options) => new MockWebSocket(url, options) as never,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "node:events";
|
||||
import WebSocket from "ws";
|
||||
import WebSocket, { type ClientOptions } from "ws";
|
||||
import { resolveProviderAttributionHeaders } from "./provider-attribution.js";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -268,7 +268,7 @@ export interface OpenAIWebSocketManagerOptions {
|
||||
/** Custom backoff delays in ms (default: [1000, 2000, 4000, 8000, 16000]) */
|
||||
backoffDelaysMs?: readonly number[];
|
||||
/** Custom socket factory for tests. */
|
||||
socketFactory?: (url: string, options: ConstructorParameters<typeof WebSocket>[1]) => WebSocket;
|
||||
socketFactory?: (url: string, options: ClientOptions) => WebSocket;
|
||||
}
|
||||
|
||||
type InternalEvents = {
|
||||
@@ -308,10 +308,7 @@ export class OpenAIWebSocketManager extends EventEmitter<InternalEvents> {
|
||||
private readonly wsUrl: string;
|
||||
private readonly maxRetries: number;
|
||||
private readonly backoffDelaysMs: readonly number[];
|
||||
private readonly socketFactory: (
|
||||
url: string,
|
||||
options: ConstructorParameters<typeof WebSocket>[1],
|
||||
) => WebSocket;
|
||||
private readonly socketFactory: (url: string, options: ClientOptions) => WebSocket;
|
||||
|
||||
constructor(options: OpenAIWebSocketManagerOptions = {}) {
|
||||
super();
|
||||
|
||||
@@ -175,9 +175,9 @@ vi.mock("@mariozechner/pi-ai", async () => {
|
||||
const sawBundleResult = toolResultText.some((text) => text.includes("FROM-BUNDLE"));
|
||||
if (!sawBundleResult) {
|
||||
stream.push({
|
||||
type: "done",
|
||||
type: "error",
|
||||
reason: "error",
|
||||
message: {
|
||||
error: {
|
||||
role: "assistant" as const,
|
||||
content: [],
|
||||
stopReason: "error" as const,
|
||||
|
||||
@@ -1,205 +1,222 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
loadPluginManifestRegistry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: (...args: unknown[]) => hoisted.loadPluginManifestRegistry(...args),
|
||||
}));
|
||||
|
||||
const { loadEnabledBundlePiSettingsSnapshot } = await import("./pi-project-settings.js");
|
||||
import { loadEnabledBundlePiSettingsSnapshot } from "./pi-project-settings.js";
|
||||
|
||||
const tempDirs = createTrackedTempDirs();
|
||||
|
||||
function buildRegistry(params: {
|
||||
pluginRoot: string;
|
||||
settingsFiles?: string[];
|
||||
}): PluginManifestRegistry {
|
||||
return {
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "claude-bundle",
|
||||
name: "Claude Bundle",
|
||||
format: "bundle",
|
||||
bundleFormat: "claude",
|
||||
bundleCapabilities: ["settings"],
|
||||
channels: [],
|
||||
providers: [],
|
||||
skills: [],
|
||||
settingsFiles: params.settingsFiles ?? ["settings.json"],
|
||||
hooks: [],
|
||||
origin: "workspace",
|
||||
rootDir: params.pluginRoot,
|
||||
source: params.pluginRoot,
|
||||
manifestPath: path.join(params.pluginRoot, ".claude-plugin", "plugin.json"),
|
||||
},
|
||||
],
|
||||
};
|
||||
async function createHomeAndWorkspace() {
|
||||
const homeDir = await tempDirs.make("openclaw-bundle-home-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
return { homeDir, workspaceDir };
|
||||
}
|
||||
|
||||
async function createClaudeBundlePlugin(params: {
|
||||
homeDir: string;
|
||||
pluginId: string;
|
||||
pluginJson?: Record<string, unknown>;
|
||||
settingsJson?: Record<string, unknown>;
|
||||
mcpJson?: Record<string, unknown>;
|
||||
}) {
|
||||
const pluginRoot = path.join(params.homeDir, ".openclaw", "extensions", params.pluginId);
|
||||
await fs.mkdir(path.join(pluginRoot, ".claude-plugin"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, ".claude-plugin", "plugin.json"),
|
||||
`${JSON.stringify({ name: params.pluginId, ...params.pluginJson }, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
if (params.settingsJson) {
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "settings.json"),
|
||||
`${JSON.stringify(params.settingsJson, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
if (params.mcpJson) {
|
||||
await fs.mkdir(path.join(pluginRoot, "servers"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, ".mcp.json"),
|
||||
`${JSON.stringify(params.mcpJson, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
hoisted.loadPluginManifestRegistry.mockReset();
|
||||
clearPluginManifestRegistryCache();
|
||||
await tempDirs.cleanup();
|
||||
});
|
||||
|
||||
describe("loadEnabledBundlePiSettingsSnapshot", () => {
|
||||
it("loads sanitized settings from enabled bundle plugins", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await tempDirs.make("openclaw-bundle-");
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "settings.json"),
|
||||
JSON.stringify({
|
||||
hideThinkingBlock: true,
|
||||
shellPath: "/tmp/blocked-shell",
|
||||
compaction: { keepRecentTokens: 64_000 },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
hoisted.loadPluginManifestRegistry.mockReturnValue(buildRegistry({ pluginRoot }));
|
||||
const env = captureEnv(["HOME", "USERPROFILE", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]);
|
||||
try {
|
||||
const { homeDir, workspaceDir } = await createHomeAndWorkspace();
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
await createClaudeBundlePlugin({
|
||||
homeDir,
|
||||
pluginId: "claude-bundle",
|
||||
settingsJson: {
|
||||
hideThinkingBlock: true,
|
||||
shellPath: "/tmp/blocked-shell",
|
||||
compaction: { keepRecentTokens: 64_000 },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(snapshot.hideThinkingBlock).toBe(true);
|
||||
expect(snapshot.shellPath).toBeUndefined();
|
||||
expect(snapshot.compaction?.keepRecentTokens).toBe(64_000);
|
||||
});
|
||||
|
||||
it("loads enabled bundle MCP servers into the Pi settings snapshot", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await tempDirs.make("openclaw-bundle-");
|
||||
await fs.mkdir(path.join(pluginRoot, ".claude-plugin"), { recursive: true });
|
||||
await fs.mkdir(path.join(pluginRoot, "servers"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, ".claude-plugin", "plugin.json"),
|
||||
JSON.stringify({
|
||||
name: "claude-bundle",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, ".mcp.json"),
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
bundleProbe: {
|
||||
command: "node",
|
||||
args: ["./servers/probe.mjs"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
hoisted.loadPluginManifestRegistry.mockReturnValue(
|
||||
buildRegistry({ pluginRoot, settingsFiles: [] }),
|
||||
);
|
||||
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot.mcpServers).toEqual({
|
||||
bundleProbe: {
|
||||
command: "node",
|
||||
args: [path.join(pluginRoot, "servers", "probe.mjs")],
|
||||
cwd: pluginRoot,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("lets top-level MCP config override bundle MCP defaults", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await tempDirs.make("openclaw-bundle-");
|
||||
await fs.mkdir(path.join(pluginRoot, ".claude-plugin"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, ".claude-plugin", "plugin.json"),
|
||||
JSON.stringify({
|
||||
name: "claude-bundle",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, ".mcp.json"),
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
sharedServer: {
|
||||
command: "node",
|
||||
args: ["./servers/bundle.mjs"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
hoisted.loadPluginManifestRegistry.mockReturnValue(
|
||||
buildRegistry({ pluginRoot, settingsFiles: [] }),
|
||||
);
|
||||
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
mcp: {
|
||||
servers: {
|
||||
sharedServer: {
|
||||
url: "https://example.com/mcp",
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
});
|
||||
|
||||
expect(snapshot.hideThinkingBlock).toBe(true);
|
||||
expect(snapshot.shellPath).toBeUndefined();
|
||||
expect(snapshot.compaction?.keepRecentTokens).toBe(64_000);
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("loads enabled bundle MCP servers into the Pi settings snapshot", async () => {
|
||||
const env = captureEnv(["HOME", "USERPROFILE", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]);
|
||||
try {
|
||||
const { homeDir, workspaceDir } = await createHomeAndWorkspace();
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
|
||||
const pluginRoot = await createClaudeBundlePlugin({
|
||||
homeDir,
|
||||
pluginId: "claude-bundle",
|
||||
mcpJson: {
|
||||
mcpServers: {
|
||||
bundleProbe: {
|
||||
command: "node",
|
||||
args: ["./servers/probe.mjs"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(snapshot.mcpServers).toEqual({
|
||||
sharedServer: {
|
||||
url: "https://example.com/mcp",
|
||||
},
|
||||
});
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const resolvedPluginRoot = await fs.realpath(pluginRoot);
|
||||
|
||||
expect(snapshot.mcpServers).toEqual({
|
||||
bundleProbe: {
|
||||
command: "node",
|
||||
args: [path.join(resolvedPluginRoot, "servers", "probe.mjs")],
|
||||
cwd: resolvedPluginRoot,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("lets top-level MCP config override bundle MCP defaults", async () => {
|
||||
const env = captureEnv(["HOME", "USERPROFILE", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]);
|
||||
try {
|
||||
const { homeDir, workspaceDir } = await createHomeAndWorkspace();
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
|
||||
await createClaudeBundlePlugin({
|
||||
homeDir,
|
||||
pluginId: "claude-bundle",
|
||||
mcpJson: {
|
||||
mcpServers: {
|
||||
sharedServer: {
|
||||
command: "node",
|
||||
args: ["./servers/bundle.mjs"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
mcp: {
|
||||
servers: {
|
||||
sharedServer: {
|
||||
url: "https://example.com/mcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot.mcpServers).toEqual({
|
||||
sharedServer: {
|
||||
url: "https://example.com/mcp",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores disabled bundle plugins", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-workspace-");
|
||||
const pluginRoot = await tempDirs.make("openclaw-bundle-");
|
||||
await fs.writeFile(
|
||||
path.join(pluginRoot, "settings.json"),
|
||||
JSON.stringify({ hideThinkingBlock: true }),
|
||||
"utf-8",
|
||||
);
|
||||
hoisted.loadPluginManifestRegistry.mockReturnValue(buildRegistry({ pluginRoot }));
|
||||
const env = captureEnv(["HOME", "USERPROFILE", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]);
|
||||
try {
|
||||
const { homeDir, workspaceDir } = await createHomeAndWorkspace();
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: false },
|
||||
await createClaudeBundlePlugin({
|
||||
homeDir,
|
||||
pluginId: "claude-bundle",
|
||||
settingsJson: {
|
||||
hideThinkingBlock: true,
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = loadEnabledBundlePiSettingsSnapshot({
|
||||
cwd: workspaceDir,
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"claude-bundle": { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(snapshot).toEqual({});
|
||||
expect(snapshot).toEqual({});
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { applyMergePatch } from "../config/merge-patch.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { BundleMcpServerConfig } from "../plugins/bundle-mcp.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
@@ -18,7 +19,9 @@ export const SANITIZED_PROJECT_PI_KEYS = ["shellPath", "shellCommandPrefix"] as
|
||||
|
||||
export type EmbeddedPiProjectSettingsPolicy = "trusted" | "sanitize" | "ignore";
|
||||
|
||||
type PiSettingsSnapshot = ReturnType<SettingsManager["getGlobalSettings"]>;
|
||||
type PiSettingsSnapshot = ReturnType<SettingsManager["getGlobalSettings"]> & {
|
||||
mcpServers?: Record<string, BundleMcpServerConfig>;
|
||||
};
|
||||
|
||||
function sanitizePiSettingsSnapshot(settings: PiSettingsSnapshot): PiSettingsSnapshot {
|
||||
const sanitized = { ...settings };
|
||||
|
||||
@@ -10,6 +10,7 @@ import { defaultRuntime } from "../runtime.js";
|
||||
function fail(message: string): never {
|
||||
defaultRuntime.error(message);
|
||||
defaultRuntime.exit(1);
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
function printJson(value: unknown): void {
|
||||
|
||||
Reference in New Issue
Block a user