mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
test: isolate agent hotspot scans
This commit is contained in:
@@ -14,14 +14,19 @@ const tempHarness = createBundleMcpTempHarness();
|
||||
let bundleProbeHomeDir = "";
|
||||
let bundleProbeWorkspaceDir = "";
|
||||
let bundleProbeServerPath = "";
|
||||
let envSnapshot: ReturnType<typeof captureEnv> | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
envSnapshot = captureEnv(["OPENCLAW_BUNDLED_PLUGINS_DIR"]);
|
||||
bundleProbeHomeDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-home-");
|
||||
bundleProbeWorkspaceDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-workspace-");
|
||||
const emptyBundledDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-bundled-");
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = emptyBundledDir;
|
||||
({ serverPath: bundleProbeServerPath } = await createBundleProbePlugin(bundleProbeHomeDir));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
envSnapshot?.restore();
|
||||
await tempHarness.cleanup();
|
||||
});
|
||||
|
||||
|
||||
@@ -25,11 +25,4 @@ describe("agents/context eager warmup", () => {
|
||||
|
||||
expect(loadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not eager-load config when plugin-sdk command-auth is imported", async () => {
|
||||
process.argv = ["node", "openclaw", "onboard"];
|
||||
await importFreshModule(import.meta.url, "../plugin-sdk/command-auth.js?scope=onboard");
|
||||
|
||||
expect(loadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.js";
|
||||
|
||||
const ANTHROPIC_VERTEX_DISCOVERY_ENV = {
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "anthropic",
|
||||
} satisfies NodeJS.ProcessEnv;
|
||||
|
||||
async function withAdcCredentialsFile(
|
||||
credentials: Record<string, string>,
|
||||
run: (params: { agentDir: string; credentialsPath: string }) => Promise<void>,
|
||||
) {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
const adcDir = mkdtempSync(join(tmpdir(), "openclaw-adc-"));
|
||||
const credentialsPath = join(adcDir, "application_default_credentials.json");
|
||||
writeFileSync(credentialsPath, JSON.stringify(credentials), "utf8");
|
||||
|
||||
try {
|
||||
await run({ agentDir, credentialsPath });
|
||||
} finally {
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
rmSync(adcDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("anthropic-vertex implicit provider", () => {
|
||||
it("does not auto-enable from GOOGLE_CLOUD_PROJECT_ID alone", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
try {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_CLOUD_PROJECT_ID: "vertex-project",
|
||||
},
|
||||
});
|
||||
expect(providers?.["anthropic-vertex"]).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts ADC credentials when the file includes a project_id", async () => {
|
||||
await withAdcCredentialsFile(
|
||||
{ project_id: "vertex-project" },
|
||||
async ({ agentDir, credentialsPath }) => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_LOCATION: "us-east1",
|
||||
},
|
||||
});
|
||||
expect(providers?.["anthropic-vertex"]?.baseUrl).toBe(
|
||||
"https://us-east1-aiplatform.googleapis.com",
|
||||
);
|
||||
expect(providers?.["anthropic-vertex"]?.models).toMatchObject([
|
||||
{ id: "claude-opus-4-6", maxTokens: 128000, contextWindow: 1_000_000 },
|
||||
{ id: "claude-sonnet-4-6", maxTokens: 128000, contextWindow: 1_000_000 },
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts explicit metadata auth opt-in without local credential files", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
try {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
},
|
||||
});
|
||||
expect(providers?.["anthropic-vertex"]?.baseUrl).toBe(
|
||||
"https://us-east5-aiplatform.googleapis.com",
|
||||
);
|
||||
} finally {
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("merges the bundled catalog into explicit anthropic-vertex provider overrides", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
try {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
},
|
||||
explicitProviders: {
|
||||
"anthropic-vertex": {
|
||||
baseUrl: "https://europe-west4-aiplatform.googleapis.com",
|
||||
headers: { "x-test-header": "1" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(providers?.["anthropic-vertex"]?.baseUrl).toBe(
|
||||
"https://europe-west4-aiplatform.googleapis.com",
|
||||
);
|
||||
expect(providers?.["anthropic-vertex"]?.headers).toEqual({ "x-test-header": "1" });
|
||||
expect(providers?.["anthropic-vertex"]?.models?.map((model) => model.id)).toEqual([
|
||||
"claude-opus-4-6",
|
||||
"claude-sonnet-4-6",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,11 +8,14 @@ import {
|
||||
import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
|
||||
import { installSkill } from "./skills-install.js";
|
||||
import { installSkill, __testing as skillsInstallTesting } from "./skills-install.js";
|
||||
import {
|
||||
runCommandWithTimeoutMock,
|
||||
scanDirectoryWithSummaryMock,
|
||||
} from "./skills-install.test-mocks.js";
|
||||
import { resolveOpenClawMetadata, resolveSkillInvocationPolicy } from "./skills/frontmatter.js";
|
||||
import { loadSkillsFromDirSafe, readSkillFrontmatterSafe } from "./skills/local-loader.js";
|
||||
import type { SkillEntry } from "./skills/types.js";
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||
@@ -45,6 +48,32 @@ metadata: {"openclaw":{"install":[{"id":"deps","kind":"node","package":"example-
|
||||
return skillDir;
|
||||
}
|
||||
|
||||
function loadTestWorkspaceSkillEntries(workspaceDir: string): SkillEntry[] {
|
||||
const skills = loadSkillsFromDirSafe({
|
||||
dir: path.join(workspaceDir, "skills"),
|
||||
source: "openclaw-workspace",
|
||||
}).skills;
|
||||
return skills.map((skill) => {
|
||||
const frontmatter =
|
||||
readSkillFrontmatterSafe({
|
||||
rootDir: skill.baseDir,
|
||||
filePath: skill.filePath,
|
||||
}) ?? {};
|
||||
const invocation = resolveSkillInvocationPolicy(frontmatter);
|
||||
return {
|
||||
skill,
|
||||
frontmatter,
|
||||
metadata: resolveOpenClawMetadata(frontmatter),
|
||||
invocation,
|
||||
exposure: {
|
||||
includeInRuntimeRegistry: true,
|
||||
includeInAvailableSkillsPrompt: !invocation.disableModelInvocation,
|
||||
userInvocable: invocation.userInvocable,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const workspaceSuite = createFixtureSuite("openclaw-skills-install-");
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -53,6 +82,7 @@ beforeAll(async () => {
|
||||
|
||||
afterAll(async () => {
|
||||
resetGlobalHookRunner();
|
||||
skillsInstallTesting.setDepsForTest();
|
||||
await workspaceSuite.cleanup();
|
||||
});
|
||||
|
||||
@@ -73,6 +103,9 @@ async function withWorkspaceCase(
|
||||
describe("installSkill code safety scanning", () => {
|
||||
beforeEach(() => {
|
||||
resetGlobalHookRunner();
|
||||
skillsInstallTesting.setDepsForTest({
|
||||
loadWorkspaceSkillEntries: loadTestWorkspaceSkillEntries,
|
||||
});
|
||||
runCommandWithTimeoutMock.mockClear();
|
||||
scanDirectoryWithSummaryMock.mockClear();
|
||||
runCommandWithTimeoutMock.mockResolvedValue({
|
||||
|
||||
@@ -15,8 +15,8 @@ import { formatInstallFailureMessage } from "./skills-install-output.js";
|
||||
import type { SkillInstallResult } from "./skills-install.types.js";
|
||||
import {
|
||||
hasBinary as defaultHasBinary,
|
||||
loadWorkspaceSkillEntries,
|
||||
resolveSkillsInstallPreferences,
|
||||
loadWorkspaceSkillEntries as defaultLoadWorkspaceSkillEntries,
|
||||
resolveSkillsInstallPreferences as defaultResolveSkillsInstallPreferences,
|
||||
type SkillEntry,
|
||||
type SkillInstallSpec,
|
||||
type SkillsInstallPreferences,
|
||||
@@ -34,12 +34,16 @@ export type { SkillInstallResult } from "./skills-install.types.js";
|
||||
|
||||
type SkillsInstallDeps = {
|
||||
hasBinary: (bin: string) => boolean;
|
||||
loadWorkspaceSkillEntries: typeof defaultLoadWorkspaceSkillEntries;
|
||||
resolveBrewExecutable: () => string | undefined;
|
||||
resolveSkillsInstallPreferences: typeof defaultResolveSkillsInstallPreferences;
|
||||
};
|
||||
|
||||
const defaultSkillsInstallDeps: SkillsInstallDeps = {
|
||||
hasBinary: defaultHasBinary,
|
||||
loadWorkspaceSkillEntries: defaultLoadWorkspaceSkillEntries,
|
||||
resolveBrewExecutable: defaultResolveBrewExecutable,
|
||||
resolveSkillsInstallPreferences: defaultResolveSkillsInstallPreferences,
|
||||
};
|
||||
|
||||
let skillsInstallDeps = defaultSkillsInstallDeps;
|
||||
@@ -419,7 +423,8 @@ async function executeInstallCommand(params: {
|
||||
export async function installSkill(params: SkillInstallRequest): Promise<SkillInstallResult> {
|
||||
const timeoutMs = Math.min(Math.max(params.timeoutMs ?? 300_000, 1_000), 900_000);
|
||||
const workspaceDir = resolveUserPath(params.workspaceDir);
|
||||
const entries = loadWorkspaceSkillEntries(workspaceDir);
|
||||
const deps = getSkillsInstallDeps();
|
||||
const entries = deps.loadWorkspaceSkillEntries(workspaceDir);
|
||||
const entry = entries.find((item) => item.skill.name === params.skillName);
|
||||
if (!entry) {
|
||||
return {
|
||||
@@ -483,7 +488,7 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
||||
return withWarnings(downloadResult, warnings);
|
||||
}
|
||||
|
||||
const prefs = resolveSkillsInstallPreferences(params.config);
|
||||
const prefs = deps.resolveSkillsInstallPreferences(params.config);
|
||||
const command = buildInstallCommand(spec, prefs);
|
||||
if (command.error) {
|
||||
return withWarnings(
|
||||
@@ -498,7 +503,6 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
||||
);
|
||||
}
|
||||
|
||||
const deps = getSkillsInstallDeps();
|
||||
const brewExe = deps.hasBinary("brew") ? "brew" : deps.resolveBrewExecutable();
|
||||
if (spec.kind === "brew" && !brewExe) {
|
||||
return withWarnings(resolveBrewMissingFailure(spec), warnings);
|
||||
|
||||
@@ -87,7 +87,14 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
const buildPrompt = (
|
||||
workspaceDir: string,
|
||||
opts?: Parameters<typeof buildWorkspaceSkillsPrompt>[1],
|
||||
) => withEnv({ HOME: workspaceDir }, () => buildWorkspaceSkillsPrompt(workspaceDir, opts));
|
||||
) =>
|
||||
withEnv({ HOME: workspaceDir }, () =>
|
||||
buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
|
||||
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||
...opts,
|
||||
}),
|
||||
);
|
||||
|
||||
const cloneSourceTemplate = async () => {
|
||||
const sourceWorkspace = await createCaseDir("source");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os from "node:os";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createSubagentSpawnTestConfig,
|
||||
installSessionStoreCaptureMock,
|
||||
@@ -19,31 +19,47 @@ const hoisted = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
describe("spawnSubagentDirect thread binding delivery", () => {
|
||||
function loadThreadBindingSpawnModule(
|
||||
overrides: Partial<Parameters<typeof loadSubagentSpawnModuleForTest>[0]> = {},
|
||||
) {
|
||||
return loadSubagentSpawnModuleForTest({
|
||||
type SpawnModule = Awaited<ReturnType<typeof loadSubagentSpawnModuleForTest>>;
|
||||
type SessionBindingService = NonNullable<
|
||||
Parameters<typeof loadSubagentSpawnModuleForTest>[0]["getSessionBindingService"]
|
||||
>;
|
||||
type DeliveryTargetResolver = NonNullable<
|
||||
Parameters<typeof loadSubagentSpawnModuleForTest>[0]["resolveConversationDeliveryTarget"]
|
||||
>;
|
||||
|
||||
let spawnSubagentDirect: SpawnModule["spawnSubagentDirect"];
|
||||
let currentConfig: Record<string, unknown>;
|
||||
let currentSessionBindingService: ReturnType<SessionBindingService>;
|
||||
let currentDeliveryTargetResolver: DeliveryTargetResolver;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock: hoisted.callGatewayMock,
|
||||
loadConfig: () =>
|
||||
createSubagentSpawnTestConfig(os.tmpdir(), {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: os.tmpdir(),
|
||||
},
|
||||
list: [{ id: "main", workspace: "/tmp/workspace-main" }],
|
||||
},
|
||||
}),
|
||||
loadConfig: () => currentConfig,
|
||||
updateSessionStoreMock: hoisted.updateSessionStoreMock,
|
||||
registerSubagentRunMock: hoisted.registerSubagentRunMock,
|
||||
emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock,
|
||||
hookRunner: hoisted.hookRunner,
|
||||
resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4",
|
||||
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
getSessionBindingService: () => currentSessionBindingService,
|
||||
resolveConversationDeliveryTarget: (params) => currentDeliveryTargetResolver(params),
|
||||
}));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
currentConfig = createSubagentSpawnTestConfig(os.tmpdir(), {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: os.tmpdir(),
|
||||
},
|
||||
list: [{ id: "main", workspace: "/tmp/workspace-main" }],
|
||||
},
|
||||
});
|
||||
currentSessionBindingService = { listBySession: () => [] };
|
||||
currentDeliveryTargetResolver = (params) => ({
|
||||
to: params.conversationId ? `channel:${String(params.conversationId)}` : undefined,
|
||||
});
|
||||
hoisted.callGatewayMock.mockReset();
|
||||
hoisted.updateSessionStoreMock.mockReset();
|
||||
hoisted.registerSubagentRunMock.mockReset();
|
||||
@@ -83,43 +99,33 @@ describe("spawnSubagentDirect thread binding delivery", () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
const { spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock: hoisted.callGatewayMock,
|
||||
loadConfig: () =>
|
||||
createSubagentSpawnTestConfig(os.tmpdir(), {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: os.tmpdir(),
|
||||
subagents: {
|
||||
allowAgents: ["bot-alpha"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{ id: "main", workspace: "/tmp/workspace-main" },
|
||||
{ id: "bot-alpha", workspace: "/tmp/workspace-bot-alpha" },
|
||||
],
|
||||
currentConfig = createSubagentSpawnTestConfig(os.tmpdir(), {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: os.tmpdir(),
|
||||
subagents: {
|
||||
allowAgents: ["bot-alpha"],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: {
|
||||
channel: "matrix",
|
||||
peer: {
|
||||
kind: "channel",
|
||||
id: boundRoom,
|
||||
},
|
||||
accountId: "bot-alpha",
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{ id: "main", workspace: "/tmp/workspace-main" },
|
||||
{ id: "bot-alpha", workspace: "/tmp/workspace-bot-alpha" },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: {
|
||||
channel: "matrix",
|
||||
peer: {
|
||||
kind: "channel",
|
||||
id: boundRoom,
|
||||
},
|
||||
],
|
||||
}),
|
||||
updateSessionStoreMock: hoisted.updateSessionStoreMock,
|
||||
registerSubagentRunMock: hoisted.registerSubagentRunMock,
|
||||
emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock,
|
||||
hookRunner: hoisted.hookRunner,
|
||||
resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4",
|
||||
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
|
||||
accountId: "bot-alpha",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
@@ -175,22 +181,20 @@ describe("spawnSubagentDirect thread binding delivery", () => {
|
||||
status: "ok",
|
||||
threadBindingReady: true,
|
||||
});
|
||||
const { spawnSubagentDirect } = await loadThreadBindingSpawnModule({
|
||||
getSessionBindingService: () => ({
|
||||
listBySession: () => [
|
||||
{
|
||||
status: "active",
|
||||
conversation: {
|
||||
channel: "feishu",
|
||||
accountId: "work",
|
||||
conversationId: "oc_dm_chat_1",
|
||||
},
|
||||
currentSessionBindingService = {
|
||||
listBySession: () => [
|
||||
{
|
||||
status: "active",
|
||||
conversation: {
|
||||
channel: "feishu",
|
||||
accountId: "work",
|
||||
conversationId: "oc_dm_chat_1",
|
||||
},
|
||||
],
|
||||
}),
|
||||
resolveConversationDeliveryTarget: () => ({
|
||||
to: "channel:oc_dm_chat_1",
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
currentDeliveryTargetResolver = () => ({
|
||||
to: "channel:oc_dm_chat_1",
|
||||
});
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
|
||||
Reference in New Issue
Block a user