test: isolate openclaw plugin context coverage

This commit is contained in:
Peter Steinberger
2026-04-05 23:46:44 +01:00
parent d13821f1c6
commit 579c50dd60
4 changed files with 277 additions and 260 deletions

View File

@@ -1,103 +1,99 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveOpenClawAgentDir } from "./agent-paths.js";
import {
installModelsConfigTestHooks,
mockCopilotTokenExchangeSuccess,
withCopilotGithubToken,
withUnsetCopilotTokenEnv,
withModelsTempHome as withTempHome,
} from "./models-config.e2e-harness.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
import { describe, expect, it, vi } from "vitest";
import { planOpenClawModelsJson } from "./models-config.plan.js";
import { createProviderAuthResolver } from "./models-config.providers.secrets.js";
installModelsConfigTestHooks({ restoreFetch: true });
async function writeAuthProfiles(agentDir: string, profiles: Record<string, unknown>) {
await fs.mkdir(agentDir, { recursive: true });
await fs.writeFile(
path.join(agentDir, "auth-profiles.json"),
JSON.stringify({ version: 1, profiles }, null, 2),
);
}
function expectBearerAuthHeader(fetchMock: { mock: { calls: unknown[][] } }, token: string) {
const [, opts] = fetchMock.mock.calls[0] as [string, { headers?: Record<string, string> }];
expect(opts?.headers?.Authorization).toBe(`Bearer ${token}`);
}
vi.mock("./models-config.providers.js", () => ({
applyNativeStreamingUsageCompat: (providers: unknown) => providers,
enforceSourceManagedProviderSecrets: ({ providers }: { providers: unknown }) => providers,
normalizeProviders: ({ providers }: { providers: unknown }) => providers,
resolveImplicitProviders: async ({
explicitProviders,
}: {
explicitProviders?: Record<string, unknown>;
}) => explicitProviders ?? {},
}));
describe("models-config", () => {
it("uses the first github-copilot profile when env tokens are missing", async () => {
await withTempHome(async (home) => {
await withUnsetCopilotTokenEnv(async () => {
const fetchMock = mockCopilotTokenExchangeSuccess();
const agentDir = path.join(home, "agent-profiles");
await writeAuthProfiles(agentDir, {
"github-copilot:alpha": {
type: "token",
provider: "github-copilot",
token: "alpha-token",
},
"github-copilot:beta": {
type: "token",
provider: "github-copilot",
token: "beta-token",
},
});
it("uses the first github-copilot profile when env tokens are missing", () => {
const auth = createProviderAuthResolver({} as NodeJS.ProcessEnv, {
version: 1,
profiles: {
"github-copilot:alpha": {
type: "token",
provider: "github-copilot",
token: "alpha-token",
},
"github-copilot:beta": {
type: "token",
provider: "github-copilot",
token: "beta-token",
},
},
});
await ensureOpenClawModelsJson({ models: { providers: {} } }, agentDir);
expectBearerAuthHeader(fetchMock, "alpha-token");
});
expect(auth("github-copilot")).toEqual({
apiKey: "alpha-token",
discoveryApiKey: "alpha-token",
mode: "token",
source: "profile",
profileId: "github-copilot:alpha",
});
});
it("does not override explicit github-copilot provider config", async () => {
await withTempHome(async () => {
await withCopilotGithubToken("gh-token", async () => {
await ensureOpenClawModelsJson({
models: {
providers: {
"github-copilot": {
baseUrl: "https://copilot.local",
api: "openai-responses",
models: [],
},
const plan = await planOpenClawModelsJson({
cfg: {
models: {
providers: {
"github-copilot": {
baseUrl: "https://copilot.local",
api: "openai-responses",
models: [],
},
},
});
const agentDir = resolveOpenClawAgentDir();
const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8");
const parsed = JSON.parse(raw) as {
providers: Record<string, { baseUrl?: string }>;
};
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://copilot.local");
});
},
},
agentDir: "/tmp/openclaw-agent",
env: {} as NodeJS.ProcessEnv,
existingRaw: "",
existingParsed: null,
});
expect(plan.action).toBe("write");
expect(
plan.action === "write"
? (
JSON.parse(plan.contents) as {
providers?: Record<string, { baseUrl?: string }>;
}
).providers?.["github-copilot"]?.baseUrl
: undefined,
).toBe("https://copilot.local");
});
it("uses tokenRef env var when github-copilot profile omits plaintext token", async () => {
await withTempHome(async (home) => {
await withUnsetCopilotTokenEnv(async () => {
const fetchMock = mockCopilotTokenExchangeSuccess();
const agentDir = path.join(home, "agent-profiles");
process.env.COPILOT_REF_TOKEN = "token-from-ref-env";
try {
await writeAuthProfiles(agentDir, {
"github-copilot:default": {
type: "token",
provider: "github-copilot",
tokenRef: { source: "env", provider: "default", id: "COPILOT_REF_TOKEN" },
},
});
it("uses tokenRef env var when github-copilot profile omits plaintext token", () => {
const auth = createProviderAuthResolver(
{
COPILOT_REF_TOKEN: "token-from-ref-env",
} as NodeJS.ProcessEnv,
{
version: 1,
profiles: {
"github-copilot:default": {
type: "token",
provider: "github-copilot",
tokenRef: { source: "env", provider: "default", id: "COPILOT_REF_TOKEN" },
},
},
},
);
await ensureOpenClawModelsJson({ models: { providers: {} } }, agentDir);
expectBearerAuthHeader(fetchMock, "token-from-ref-env");
} finally {
delete process.env.COPILOT_REF_TOKEN;
}
});
expect(auth("github-copilot")).toEqual({
apiKey: "COPILOT_REF_TOKEN",
discoveryApiKey: "token-from-ref-env",
mode: "token",
source: "profile",
profileId: "github-copilot:default",
});
});
});

View File

@@ -1,176 +1,148 @@
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { resolveOpenClawPluginToolInputs } from "./openclaw-tools.plugin-context.js";
import { applyPluginToolDeliveryDefaults } from "./plugin-tool-delivery-defaults.js";
import type { AnyAgentTool } from "./tools/common.js";
const { resolvePluginToolsMock } = vi.hoisted(() => ({
resolvePluginToolsMock: vi.fn((params?: unknown) => {
void params;
return [];
}),
}));
vi.mock("../plugins/tools.js", () => ({
resolvePluginTools: resolvePluginToolsMock,
copyPluginToolMeta: vi.fn(),
getPluginToolMeta: vi.fn(() => undefined),
}));
let createOpenClawTools: typeof import("./openclaw-tools.js").createOpenClawTools;
let createOpenClawCodingTools: typeof import("./pi-tools.js").createOpenClawCodingTools;
describe("createOpenClawTools plugin context", () => {
beforeEach(async () => {
resolvePluginToolsMock.mockClear();
vi.resetModules();
({ createOpenClawTools } = await import("./openclaw-tools.js"));
({ createOpenClawCodingTools } = await import("./pi-tools.js"));
});
it("forwards trusted requester sender identity to plugin tool context", () => {
createOpenClawTools({
config: {} as never,
requesterSenderId: "trusted-sender",
senderIsOwner: true,
describe("openclaw plugin tool context", () => {
it("forwards trusted requester sender identity", () => {
const result = resolveOpenClawPluginToolInputs({
options: {
config: {} as never,
requesterSenderId: "trusted-sender",
senderIsOwner: true,
},
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect(result.context).toEqual(
expect.objectContaining({
context: expect.objectContaining({
requesterSenderId: "trusted-sender",
senderIsOwner: true,
}),
requesterSenderId: "trusted-sender",
senderIsOwner: true,
}),
);
});
it("forwards ephemeral sessionId to plugin tool context", () => {
createOpenClawTools({
config: {} as never,
agentSessionKey: "agent:main:telegram:direct:12345",
sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
it("forwards ephemeral sessionId", () => {
const result = resolveOpenClawPluginToolInputs({
options: {
config: {} as never,
agentSessionKey: "agent:main:telegram:direct:12345",
sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
},
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect(result.context).toEqual(
expect.objectContaining({
context: expect.objectContaining({
sessionKey: "agent:main:telegram:direct:12345",
sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
}),
sessionKey: "agent:main:telegram:direct:12345",
sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
}),
);
});
it("infers the default agent workspace for plugin tools when workspaceDir is omitted", () => {
it("infers the default agent workspace when workspaceDir is omitted", () => {
const workspaceDir = path.join(process.cwd(), "tmp-main-workspace");
createOpenClawTools({
config: {
const result = resolveOpenClawPluginToolInputs({
options: {
config: {
agents: {
defaults: { workspace: workspaceDir },
list: [{ id: "main", default: true }],
},
} as never,
agentSessionKey: "main",
},
resolvedConfig: {
agents: {
defaults: { workspace: workspaceDir },
list: [{ id: "main", default: true }],
},
} as never,
agentSessionKey: "main",
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect(result.context).toEqual(
expect.objectContaining({
context: expect.objectContaining({
agentId: "main",
workspaceDir,
}),
agentId: "main",
workspaceDir,
}),
);
});
it("infers the session agent workspace for plugin tools when workspaceDir is omitted", () => {
it("infers the session agent workspace when workspaceDir is omitted", () => {
const supportWorkspace = path.join(process.cwd(), "tmp-support-workspace");
createOpenClawTools({
config: {
agents: {
defaults: { workspace: path.join(process.cwd(), "tmp-default-workspace") },
list: [
{ id: "main", default: true },
{ id: "support", workspace: supportWorkspace },
],
const config = {
agents: {
defaults: { workspace: path.join(process.cwd(), "tmp-default-workspace") },
list: [
{ id: "main", default: true },
{ id: "support", workspace: supportWorkspace },
],
},
} as never;
const result = resolveOpenClawPluginToolInputs({
options: {
config,
agentSessionKey: "agent:support:main",
},
resolvedConfig: config,
});
expect(result.context).toEqual(
expect.objectContaining({
agentId: "support",
workspaceDir: supportWorkspace,
}),
);
});
it("forwards browser session wiring", () => {
const result = resolveOpenClawPluginToolInputs({
options: {
config: {} as never,
sandboxBrowserBridgeUrl: "http://127.0.0.1:9999",
allowHostBrowserControl: true,
},
});
expect(result.context).toEqual(
expect.objectContaining({
browser: {
sandboxBridgeUrl: "http://127.0.0.1:9999",
allowHostControl: true,
},
} as never,
agentSessionKey: "agent:support:main",
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
context: expect.objectContaining({
agentId: "support",
workspaceDir: supportWorkspace,
}),
}),
);
});
it("forwards browser session wiring to plugin tool context", () => {
createOpenClawTools({
config: {} as never,
sandboxBrowserBridgeUrl: "http://127.0.0.1:9999",
allowHostBrowserControl: true,
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
context: expect.objectContaining({
browser: {
sandboxBridgeUrl: "http://127.0.0.1:9999",
allowHostControl: true,
},
}),
}),
);
});
it("forwards gateway subagent binding for plugin tools", () => {
createOpenClawTools({
config: {} as never,
allowGatewaySubagentBinding: true,
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
it("forwards gateway subagent binding", () => {
const result = resolveOpenClawPluginToolInputs({
options: {
config: {} as never,
allowGatewaySubagentBinding: true,
}),
);
});
it("forwards gateway subagent binding through coding tools", () => {
createOpenClawCodingTools({
config: {} as never,
allowGatewaySubagentBinding: true,
},
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
allowGatewaySubagentBinding: true,
}),
);
expect(result.allowGatewaySubagentBinding).toBe(true);
});
it("forwards ambient deliveryContext to plugin tool context", () => {
createOpenClawTools({
config: {} as never,
agentChannel: "slack",
agentTo: "channel:C123",
agentAccountId: "work",
agentThreadId: "1710000000.000100",
it("forwards ambient deliveryContext", () => {
const result = resolveOpenClawPluginToolInputs({
options: {
config: {} as never,
agentChannel: "slack",
agentTo: "channel:C123",
agentAccountId: "work",
agentThreadId: "1710000000.000100",
},
});
expect(resolvePluginToolsMock).toHaveBeenCalledWith(
expect(result.context).toEqual(
expect.objectContaining({
context: expect.objectContaining({
deliveryContext: {
channel: "slack",
to: "channel:C123",
accountId: "work",
threadId: "1710000000.000100",
},
}),
deliveryContext: {
channel: "slack",
to: "channel:C123",
accountId: "work",
threadId: "1710000000.000100",
},
}),
);
});
@@ -192,19 +164,16 @@ describe("createOpenClawTools plugin context", () => {
},
execute: executeMock,
};
resolvePluginToolsMock.mockImplementation(() => [sharedTool] as never);
const first = createOpenClawTools({
config: {} as never,
agentThreadId: "111.222",
}).find((tool) => tool.name === "plugin-thread-default");
const second = createOpenClawTools({
config: {} as never,
agentThreadId: "333.444",
}).find((tool) => tool.name === "plugin-thread-default");
const [first] = applyPluginToolDeliveryDefaults({
tools: [sharedTool],
deliveryContext: { threadId: "111.222" },
});
const [second] = applyPluginToolDeliveryDefaults({
tools: [sharedTool],
deliveryContext: { threadId: "333.444" },
});
expect(first).toBeDefined();
expect(second).toBeDefined();
expect(first).toBe(sharedTool);
expect(second).toBe(sharedTool);
@@ -232,12 +201,11 @@ describe("createOpenClawTools plugin context", () => {
},
execute: executeMock,
};
resolvePluginToolsMock.mockReturnValue([tool] as never);
const wrapped = createOpenClawTools({
config: {} as never,
agentThreadId: "77",
}).find((candidate) => candidate.name === tool.name);
const [wrapped] = applyPluginToolDeliveryDefaults({
tools: [tool],
deliveryContext: { threadId: "77" },
});
await wrapped?.execute("call-1", undefined);
@@ -261,12 +229,11 @@ describe("createOpenClawTools plugin context", () => {
},
execute: executeMock,
};
resolvePluginToolsMock.mockReturnValue([tool] as never);
const wrapped = createOpenClawTools({
config: {} as never,
agentThreadId: "77",
}).find((candidate) => candidate.name === tool.name);
const [wrapped] = applyPluginToolDeliveryDefaults({
tools: [tool],
deliveryContext: { threadId: "77" },
});
await wrapped?.execute("call-1", {});
@@ -290,12 +257,11 @@ describe("createOpenClawTools plugin context", () => {
},
execute: executeMock,
};
resolvePluginToolsMock.mockReturnValue([tool] as never);
const wrapped = createOpenClawTools({
config: {} as never,
agentThreadId: "111.222",
}).find((candidate) => candidate.name === tool.name);
const [wrapped] = applyPluginToolDeliveryDefaults({
tools: [tool],
deliveryContext: { threadId: "111.222" },
});
await wrapped?.execute("call-1", { threadId: "explicit" });

View File

@@ -0,0 +1,69 @@
import type { OpenClawConfig } from "../config/config.js";
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
import type { GatewayMessageChannel } from "../utils/message-channel.js";
import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js";
import { resolveWorkspaceRoot } from "./workspace-dir.js";
export type OpenClawPluginToolOptions = {
agentSessionKey?: string;
agentChannel?: GatewayMessageChannel;
agentAccountId?: string;
agentTo?: string;
agentThreadId?: string | number;
agentDir?: string;
workspaceDir?: string;
config?: OpenClawConfig;
requesterSenderId?: string | null;
senderIsOwner?: boolean;
sessionId?: string;
sandboxBrowserBridgeUrl?: string;
allowHostBrowserControl?: boolean;
sandboxed?: boolean;
allowGatewaySubagentBinding?: boolean;
};
export function resolveOpenClawPluginToolInputs(params: {
options?: OpenClawPluginToolOptions;
resolvedConfig?: OpenClawConfig;
runtimeConfig?: OpenClawConfig;
}) {
const { options, resolvedConfig, runtimeConfig } = params;
const sessionAgentId = resolveSessionAgentId({
sessionKey: options?.agentSessionKey,
config: resolvedConfig,
});
const inferredWorkspaceDir =
options?.workspaceDir || !resolvedConfig
? undefined
: resolveAgentWorkspaceDir(resolvedConfig, sessionAgentId);
const workspaceDir = resolveWorkspaceRoot(options?.workspaceDir ?? inferredWorkspaceDir);
const deliveryContext = normalizeDeliveryContext({
channel: options?.agentChannel,
to: options?.agentTo,
accountId: options?.agentAccountId,
threadId: options?.agentThreadId,
});
return {
context: {
config: options?.config,
runtimeConfig,
workspaceDir,
agentDir: options?.agentDir,
agentId: sessionAgentId,
sessionKey: options?.agentSessionKey,
sessionId: options?.sessionId,
browser: {
sandboxBridgeUrl: options?.sandboxBrowserBridgeUrl,
allowHostControl: options?.allowHostBrowserControl,
},
messageChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId,
deliveryContext,
requesterSenderId: options?.requesterSenderId ?? undefined,
senderIsOwner: options?.senderIsOwner ?? undefined,
sandboxed: options?.sandboxed,
},
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
};
}

View File

@@ -8,6 +8,7 @@ import {
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
import type { GatewayMessageChannel } from "../utils/message-channel.js";
import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js";
import { resolveOpenClawPluginToolInputs } from "./openclaw-tools.plugin-context.js";
import { applyPluginToolDeliveryDefaults } from "./plugin-tool-delivery-defaults.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
import type { SpawnedToolContext } from "./spawned-context.js";
@@ -291,28 +292,13 @@ export function createOpenClawTools(
}
const pluginTools = resolvePluginTools({
context: {
config: options?.config,
...resolveOpenClawPluginToolInputs({
options,
resolvedConfig,
runtimeConfig: runtimeSnapshot?.config,
workspaceDir,
agentDir: options?.agentDir,
agentId: sessionAgentId,
sessionKey: options?.agentSessionKey,
sessionId: options?.sessionId,
browser: {
sandboxBridgeUrl: options?.sandboxBrowserBridgeUrl,
allowHostControl: options?.allowHostBrowserControl,
},
messageChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId,
deliveryContext,
requesterSenderId: options?.requesterSenderId ?? undefined,
senderIsOwner: options?.senderIsOwner ?? undefined,
sandboxed: options?.sandboxed,
},
}),
existingToolNames: new Set(tools.map((tool) => tool.name)),
toolAllowlist: options?.pluginToolAllowlist,
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
});
const wrappedPluginTools = applyPluginToolDeliveryDefaults({