Merge branch 'main' into feat/chat-delete-session
This commit is contained in:
commit
6eb233ea1e
@ -1,3 +1,4 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
@ -8,6 +9,24 @@ import { buildDockerExecArgs } from "./bash-tools.shared.js";
|
|||||||
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
||||||
|
|
||||||
const isWin = process.platform === "win32";
|
const isWin = process.platform === "win32";
|
||||||
|
const resolveShellFromPath = (name: string) => {
|
||||||
|
const envPath = process.env.PATH ?? "";
|
||||||
|
if (!envPath) return undefined;
|
||||||
|
const entries = envPath.split(path.delimiter).filter(Boolean);
|
||||||
|
for (const entry of entries) {
|
||||||
|
const candidate = path.join(entry, name);
|
||||||
|
try {
|
||||||
|
fs.accessSync(candidate, fs.constants.X_OK);
|
||||||
|
return candidate;
|
||||||
|
} catch {
|
||||||
|
// ignore missing or non-executable entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
const defaultShell = isWin
|
||||||
|
? undefined
|
||||||
|
: process.env.CLAWDBOT_TEST_SHELL || resolveShellFromPath("bash") || process.env.SHELL || "sh";
|
||||||
// PowerShell: Start-Sleep for delays, ; for command separation, $null for null device
|
// PowerShell: Start-Sleep for delays, ; for command separation, $null for null device
|
||||||
const shortDelayCmd = isWin ? "Start-Sleep -Milliseconds 50" : "sleep 0.05";
|
const shortDelayCmd = isWin ? "Start-Sleep -Milliseconds 50" : "sleep 0.05";
|
||||||
const yieldDelayCmd = isWin ? "Start-Sleep -Milliseconds 200" : "sleep 0.2";
|
const yieldDelayCmd = isWin ? "Start-Sleep -Milliseconds 200" : "sleep 0.2";
|
||||||
@ -52,7 +71,7 @@ describe("exec tool backgrounding", () => {
|
|||||||
const originalShell = process.env.SHELL;
|
const originalShell = process.env.SHELL;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
if (!isWin) process.env.SHELL = "/bin/bash";
|
if (!isWin && defaultShell) process.env.SHELL = defaultShell;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -282,7 +301,7 @@ describe("exec PATH handling", () => {
|
|||||||
const originalShell = process.env.SHELL;
|
const originalShell = process.env.SHELL;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
if (!isWin) process.env.SHELL = "/bin/bash";
|
if (!isWin && defaultShell) process.env.SHELL = defaultShell;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -35,8 +35,8 @@ function isAlive(pid: number): boolean {
|
|||||||
function releaseAllLocksSync(): void {
|
function releaseAllLocksSync(): void {
|
||||||
for (const [sessionFile, held] of HELD_LOCKS) {
|
for (const [sessionFile, held] of HELD_LOCKS) {
|
||||||
try {
|
try {
|
||||||
if (typeof held.handle.fd === "number") {
|
if (typeof held.handle.close === "function") {
|
||||||
fsSync.closeSync(held.handle.fd);
|
void held.handle.close().catch(() => {});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore errors during cleanup - best effort
|
// Ignore errors during cleanup - best effort
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import * as ssrf from "../../infra/net/ssrf.js";
|
||||||
|
|
||||||
const lookupMock = vi.fn();
|
const lookupMock = vi.fn();
|
||||||
|
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||||
vi.mock("node:dns/promises", () => ({
|
|
||||||
lookup: lookupMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
function makeHeaders(map: Record<string, string>): { get: (key: string) => string | null } {
|
function makeHeaders(map: Record<string, string>): { get: (key: string) => string | null } {
|
||||||
return {
|
return {
|
||||||
@ -33,6 +32,12 @@ function textResponse(body: string): Response {
|
|||||||
describe("web_fetch SSRF protection", () => {
|
describe("web_fetch SSRF protection", () => {
|
||||||
const priorFetch = global.fetch;
|
const priorFetch = global.fetch;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation((hostname) =>
|
||||||
|
resolvePinnedHostname(hostname, lookupMock),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// @ts-expect-error restore
|
// @ts-expect-error restore
|
||||||
global.fetch = priorFetch;
|
global.fetch = priorFetch;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import * as ssrf from "../../infra/net/ssrf.js";
|
||||||
import { createWebFetchTool } from "./web-tools.js";
|
import { createWebFetchTool } from "./web-tools.js";
|
||||||
|
|
||||||
type MockResponse = {
|
type MockResponse = {
|
||||||
@ -73,6 +74,18 @@ function requestUrl(input: RequestInfo): string {
|
|||||||
describe("web_fetch extraction fallbacks", () => {
|
describe("web_fetch extraction fallbacks", () => {
|
||||||
const priorFetch = global.fetch;
|
const priorFetch = global.fetch;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation(async (hostname) => {
|
||||||
|
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||||
|
const addresses = ["93.184.216.34", "93.184.216.35"];
|
||||||
|
return {
|
||||||
|
hostname: normalized,
|
||||||
|
addresses,
|
||||||
|
lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// @ts-expect-error restore
|
// @ts-expect-error restore
|
||||||
global.fetch = priorFetch;
|
global.fetch = priorFetch;
|
||||||
|
|||||||
@ -202,6 +202,16 @@ describe("canvas host", () => {
|
|||||||
|
|
||||||
it("serves the gateway-hosted A2UI scaffold", async () => {
|
it("serves the gateway-hosted A2UI scaffold", async () => {
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-canvas-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-canvas-"));
|
||||||
|
const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui");
|
||||||
|
const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js");
|
||||||
|
let createdBundle = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.stat(bundlePath);
|
||||||
|
} catch {
|
||||||
|
await fs.writeFile(bundlePath, "window.moltbotA2UI = {};", "utf8");
|
||||||
|
createdBundle = true;
|
||||||
|
}
|
||||||
|
|
||||||
const server = await startCanvasHost({
|
const server = await startCanvasHost({
|
||||||
runtime: defaultRuntime,
|
runtime: defaultRuntime,
|
||||||
@ -226,6 +236,9 @@ describe("canvas host", () => {
|
|||||||
expect(js).toContain("moltbotA2UI");
|
expect(js).toContain("moltbotA2UI");
|
||||||
} finally {
|
} finally {
|
||||||
await server.close();
|
await server.close();
|
||||||
|
if (createdBundle) {
|
||||||
|
await fs.rm(bundlePath, { force: true });
|
||||||
|
}
|
||||||
await fs.rm(dir, { recursive: true, force: true });
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||||
import { logWarn } from "../logger.js";
|
import { logWarn } from "../logger.js";
|
||||||
|
import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js";
|
||||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||||
import { isSubagentSessionKey } from "../routing/session-key.js";
|
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||||
@ -33,6 +34,7 @@ import {
|
|||||||
} from "./http-common.js";
|
} from "./http-common.js";
|
||||||
|
|
||||||
const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
|
const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
|
||||||
|
const MEMORY_TOOL_NAMES = new Set(["memory_search", "memory_get"]);
|
||||||
|
|
||||||
type ToolsInvokeBody = {
|
type ToolsInvokeBody = {
|
||||||
tool?: unknown;
|
tool?: unknown;
|
||||||
@ -47,6 +49,26 @@ function resolveSessionKeyFromBody(body: ToolsInvokeBody): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMemoryToolDisableReasons(cfg: ReturnType<typeof loadConfig>): string[] {
|
||||||
|
if (!process.env.VITEST) return [];
|
||||||
|
const reasons: string[] = [];
|
||||||
|
const plugins = cfg.plugins;
|
||||||
|
const slotRaw = plugins?.slots?.memory;
|
||||||
|
const slotDisabled =
|
||||||
|
slotRaw === null || (typeof slotRaw === "string" && slotRaw.trim().toLowerCase() === "none");
|
||||||
|
const pluginsDisabled = plugins?.enabled === false;
|
||||||
|
const defaultDisabled = isTestDefaultMemorySlotDisabled(cfg);
|
||||||
|
|
||||||
|
if (pluginsDisabled) reasons.push("plugins.enabled=false");
|
||||||
|
if (slotDisabled) {
|
||||||
|
reasons.push(slotRaw === null ? "plugins.slots.memory=null" : 'plugins.slots.memory="none"');
|
||||||
|
}
|
||||||
|
if (!pluginsDisabled && !slotDisabled && defaultDisabled) {
|
||||||
|
reasons.push("memory plugin disabled by test default");
|
||||||
|
}
|
||||||
|
return reasons;
|
||||||
|
}
|
||||||
|
|
||||||
function mergeActionIntoArgsIfSupported(params: {
|
function mergeActionIntoArgsIfSupported(params: {
|
||||||
toolSchema: unknown;
|
toolSchema: unknown;
|
||||||
action: string | undefined;
|
action: string | undefined;
|
||||||
@ -103,6 +125,23 @@ export async function handleToolsInvokeHttpRequest(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.VITEST && MEMORY_TOOL_NAMES.has(toolName)) {
|
||||||
|
const reasons = resolveMemoryToolDisableReasons(cfg);
|
||||||
|
if (reasons.length > 0) {
|
||||||
|
const suffix = reasons.length > 0 ? ` (${reasons.join(", ")})` : "";
|
||||||
|
sendJson(res, 400, {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
type: "invalid_request",
|
||||||
|
message:
|
||||||
|
`memory tools are disabled in tests${suffix}. ` +
|
||||||
|
'Enable by setting plugins.slots.memory="memory-core" (and ensure plugins.enabled is not false).',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const action = typeof body.action === "string" ? body.action.trim() : undefined;
|
const action = typeof body.action === "string" ? body.action.trim() : undefined;
|
||||||
|
|
||||||
const argsRaw = body.args;
|
const argsRaw = body.args;
|
||||||
|
|||||||
@ -64,6 +64,72 @@ export const normalizePluginsConfig = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasExplicitMemorySlot = (plugins?: MoltbotConfig["plugins"]) =>
|
||||||
|
Boolean(plugins?.slots && Object.prototype.hasOwnProperty.call(plugins.slots, "memory"));
|
||||||
|
|
||||||
|
const hasExplicitMemoryEntry = (plugins?: MoltbotConfig["plugins"]) =>
|
||||||
|
Boolean(plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, "memory-core"));
|
||||||
|
|
||||||
|
const hasExplicitPluginConfig = (plugins?: MoltbotConfig["plugins"]) => {
|
||||||
|
if (!plugins) return false;
|
||||||
|
if (typeof plugins.enabled === "boolean") return true;
|
||||||
|
if (Array.isArray(plugins.allow) && plugins.allow.length > 0) return true;
|
||||||
|
if (Array.isArray(plugins.deny) && plugins.deny.length > 0) return true;
|
||||||
|
if (plugins.load?.paths && Array.isArray(plugins.load.paths) && plugins.load.paths.length > 0)
|
||||||
|
return true;
|
||||||
|
if (plugins.slots && Object.keys(plugins.slots).length > 0) return true;
|
||||||
|
if (plugins.entries && Object.keys(plugins.entries).length > 0) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function applyTestPluginDefaults(
|
||||||
|
cfg: MoltbotConfig,
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): MoltbotConfig {
|
||||||
|
if (!env.VITEST) return cfg;
|
||||||
|
const plugins = cfg.plugins;
|
||||||
|
const explicitConfig = hasExplicitPluginConfig(plugins);
|
||||||
|
if (explicitConfig) {
|
||||||
|
if (hasExplicitMemorySlot(plugins) || hasExplicitMemoryEntry(plugins)) {
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
plugins: {
|
||||||
|
...plugins,
|
||||||
|
slots: {
|
||||||
|
...plugins?.slots,
|
||||||
|
memory: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
plugins: {
|
||||||
|
...plugins,
|
||||||
|
enabled: false,
|
||||||
|
slots: {
|
||||||
|
...plugins?.slots,
|
||||||
|
memory: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTestDefaultMemorySlotDisabled(
|
||||||
|
cfg: MoltbotConfig,
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): boolean {
|
||||||
|
if (!env.VITEST) return false;
|
||||||
|
const plugins = cfg.plugins;
|
||||||
|
if (hasExplicitMemorySlot(plugins) || hasExplicitMemoryEntry(plugins)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveEnableState(
|
export function resolveEnableState(
|
||||||
id: string,
|
id: string,
|
||||||
origin: PluginRecord["origin"],
|
origin: PluginRecord["origin"],
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { resolveUserPath } from "../utils.js";
|
|||||||
import { discoverMoltbotPlugins } from "./discovery.js";
|
import { discoverMoltbotPlugins } from "./discovery.js";
|
||||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||||
import {
|
import {
|
||||||
|
applyTestPluginDefaults,
|
||||||
normalizePluginsConfig,
|
normalizePluginsConfig,
|
||||||
resolveEnableState,
|
resolveEnableState,
|
||||||
resolveMemorySlotDecision,
|
resolveMemorySlotDecision,
|
||||||
@ -162,7 +163,7 @@ function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnost
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function loadMoltbotPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
export function loadMoltbotPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
||||||
const cfg = options.config ?? {};
|
const cfg = applyTestPluginDefaults(options.config ?? {});
|
||||||
const logger = options.logger ?? defaultLogger();
|
const logger = options.logger ?? defaultLogger();
|
||||||
const validateOnly = options.mode === "validate";
|
const validateOnly = options.mode === "validate";
|
||||||
const normalized = normalizePluginsConfig(cfg.plugins);
|
const normalized = normalizePluginsConfig(cfg.plugins);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user