From c286f56167449913e9782cf140fcfdc0a4f89d72 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 4 Jun 2026 03:29:33 +0200 Subject: [PATCH] test: align e2e fixtures with current runtime stores --- .../agent-tools.before-tool-call.e2e.test.ts | 52 +++++--- ...ed-agent.auth-profile-rotation.e2e.test.ts | 121 ++++++++---------- .../model-fallback.run-embedded.e2e.test.ts | 32 ++--- src/plugins/install.npm-spec.e2e.test.ts | 52 +++++++- 4 files changed, 151 insertions(+), 106 deletions(-) diff --git a/src/agents/agent-tools.before-tool-call.e2e.test.ts b/src/agents/agent-tools.before-tool-call.e2e.test.ts index a46a4612cb5..136c38c2aad 100644 --- a/src/agents/agent-tools.before-tool-call.e2e.test.ts +++ b/src/agents/agent-tools.before-tool-call.e2e.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { onInternalDiagnosticEvent, onDiagnosticEvent, @@ -30,7 +30,7 @@ vi.mock("../plugins/hook-runner-global.js", async () => { ); return { ...actual, - getGlobalHookRunner: vi.fn(), + getGlobalHookRunner: vi.fn(actual.getGlobalHookRunner), }; }); vi.mock("./tools/gateway.js", () => ({ @@ -38,6 +38,37 @@ vi.mock("./tools/gateway.js", () => ({ })); const mockGetGlobalHookRunner = vi.mocked(getGlobalHookRunner); +const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state"); + +function setGlobalHookRunnerForTest(hookRunner: unknown): void { + const hookRunnerGlobalState = globalThis as Record< + symbol, + { hookRunner: unknown; registry?: unknown } | undefined + >; + if (!hookRunnerGlobalState[hookRunnerGlobalStateKey]) { + hookRunnerGlobalState[hookRunnerGlobalStateKey] = { + hookRunner: null, + registry: null, + }; + } + hookRunnerGlobalState[hookRunnerGlobalStateKey].hookRunner = hookRunner; +} + +function getGlobalHookRunnerForTest(): unknown { + const hookRunnerGlobalState = globalThis as Record< + symbol, + { hookRunner: unknown; registry?: unknown } | undefined + >; + return hookRunnerGlobalState[hookRunnerGlobalStateKey]?.hookRunner ?? null; +} + +afterEach(() => { + setGlobalHookRunnerForTest(null); + mockGetGlobalHookRunner.mockReset(); + mockGetGlobalHookRunner.mockImplementation( + () => getGlobalHookRunnerForTest() as ReturnType, + ); +}); describe("before_tool_call loop detection behavior", () => { let hookRunner: { @@ -750,7 +781,7 @@ describe("before_tool_call loop detection behavior", () => { }); it("emits blocked diagnostics without error severity for intentional hook vetoes", async () => { - hookRunner.hasHooks.mockReturnValue(true); + hookRunner.hasHooks.mockImplementation((hookName: string) => hookName === "before_tool_call"); hookRunner.runBeforeToolCall.mockResolvedValue({ block: true, blockReason: "blocked by policy", @@ -924,24 +955,13 @@ describe("before_tool_call requireApproval handling", () => { resetDiagnosticSessionStateForTest(); resetDiagnosticEventsForTest(); hookRunner = { - hasHooks: vi.fn().mockReturnValue(true), + hasHooks: vi.fn((hookName: string) => hookName === "before_tool_call"), runBeforeToolCall: vi.fn(), }; mockGetGlobalHookRunner.mockReturnValue(hookRunner as any); // Keep the global singleton aligned as a fallback in case another setup path // preloads hook-runner-global before this test's module reset/mocks take effect. - const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state"); - const hookRunnerGlobalState = globalThis as Record< - symbol, - { hookRunner: unknown; registry?: unknown } | undefined - >; - if (!hookRunnerGlobalState[hookRunnerGlobalStateKey]) { - hookRunnerGlobalState[hookRunnerGlobalStateKey] = { - hookRunner: null, - registry: null, - }; - } - hookRunnerGlobalState[hookRunnerGlobalStateKey].hookRunner = hookRunner; + setGlobalHookRunnerForTest(hookRunner); mockCallGateway.mockReset(); setActivePluginRegistry(createEmptyPluginRegistry()); }); diff --git a/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts index 003825fe483..c62e5309ed9 100644 --- a/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts +++ b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite import type { OpenClawConfig } from "../config/config.js"; import { redactIdentifier } from "../logging/redact-identifier.js"; import type { AuthProfileFailureReason } from "./auth-profiles.js"; +import { ensureAuthProfileStore, saveAuthProfileStore } from "./auth-profiles/store.js"; import { buildAttemptReplayMetadata } from "./embedded-agent-runner/run/incomplete-turn.js"; import type { EmbeddedRunAttemptResult } from "./embedded-agent-runner/run/types.js"; import { @@ -330,56 +331,54 @@ const writeAuthStore = async ( >; }, ) => { - const authPath = path.join(agentDir, "auth-profiles.json"); - const statePath = path.join(agentDir, "auth-state.json"); - const authPayload = { - version: 1, - profiles: { - "openai:p1": { type: "api_key", provider: "openai", key: "sk-one" }, - "openai:p2": { type: "api_key", provider: "openai", key: "sk-two" }, - ...(opts?.includeAnthropic - ? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } } - : {}), + saveAuthProfileStore( + { + version: 1, + profiles: { + "openai:p1": { type: "api_key", provider: "openai", key: "sk-one" }, + "openai:p2": { type: "api_key", provider: "openai", key: "sk-two" }, + ...(opts?.includeAnthropic + ? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } } + : {}), + }, + ...(opts?.order ? { order: opts.order } : {}), + usageStats: + opts?.usageStats ?? + ({ + "openai:p1": { lastUsed: 1 }, + "openai:p2": { lastUsed: 2 }, + } as Record), }, - }; - const statePayload = { - version: 1, - ...(opts?.order ? { order: opts.order } : {}), - usageStats: - opts?.usageStats ?? - ({ - "openai:p1": { lastUsed: 1 }, - "openai:p2": { lastUsed: 2 }, - } as Record), - }; - await fs.writeFile(authPath, JSON.stringify(authPayload)); - await fs.writeFile(statePath, JSON.stringify(statePayload)); + agentDir, + ); }; const writeCopilotAuthStore = async (agentDir: string, token = "gh-token") => { - const authPath = path.join(agentDir, "auth-profiles.json"); - const payload = { - version: 1, - profiles: { - "github-copilot:github": { type: "token", provider: "github-copilot", token }, + saveAuthProfileStore( + { + version: 1, + profiles: { + "github-copilot:github": { type: "token", provider: "github-copilot", token }, + }, }, - }; - await fs.writeFile(authPath, JSON.stringify(payload)); + agentDir, + ); }; const writeOpenAiCodexAuthStore = async (agentDir: string) => { - const authPath = path.join(agentDir, "auth-profiles.json"); - const payload = { - version: 1, - profiles: { - "openai:work": { - type: "api_key", - provider: "openai", - key: "sk-codex", + saveAuthProfileStore( + { + version: 1, + profiles: { + "openai:work": { + type: "api_key", + provider: "openai", + key: "sk-codex", + }, }, }, - }; - await fs.writeFile(authPath, JSON.stringify(payload)); + agentDir, + ); }; const buildCopilotAssistant = (overrides: Partial = {}) => @@ -462,18 +461,7 @@ async function runAutoPinnedOpenAiTurn(params: { } async function readUsageStats(agentDir: string) { - const stored = JSON.parse(await fs.readFile(path.join(agentDir, "auth-state.json"), "utf-8")) as { - usageStats?: Record< - string, - { - lastUsed?: number; - cooldownUntil?: number; - disabledUntil?: number; - disabledReason?: AuthProfileFailureReason; - } - >; - }; - return stored.usageStats ?? {}; + return ensureAuthProfileStore(agentDir, { syncExternalCli: false }).usageStats ?? {}; } async function expectProfileP2UsageUnchanged(agentDir: string) { @@ -1619,22 +1607,23 @@ describe("runEmbeddedAgent auth profile rotation", () => { it("skips profiles in cooldown when rotating after failure", async () => { await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { - const authPath = path.join(agentDir, "auth-profiles.json"); const p2CooldownUntil = Date.now() + 60 * 60 * 1000; - const payload = { - version: 1, - profiles: { - "openai:p1": { type: "api_key", provider: "openai", key: "sk-one" }, - "openai:p2": { type: "api_key", provider: "openai", key: "sk-two" }, - "openai:p3": { type: "api_key", provider: "openai", key: "sk-three" }, + saveAuthProfileStore( + { + version: 1, + profiles: { + "openai:p1": { type: "api_key", provider: "openai", key: "sk-one" }, + "openai:p2": { type: "api_key", provider: "openai", key: "sk-two" }, + "openai:p3": { type: "api_key", provider: "openai", key: "sk-three" }, + }, + usageStats: { + "openai:p1": { lastUsed: 1 }, + "openai:p2": { cooldownUntil: p2CooldownUntil }, + "openai:p3": { lastUsed: 3 }, + }, }, - usageStats: { - "openai:p1": { lastUsed: 1 }, - "openai:p2": { cooldownUntil: p2CooldownUntil }, // p2 in cooldown - "openai:p3": { lastUsed: 3 }, - }, - }; - await fs.writeFile(authPath, JSON.stringify(payload)); + agentDir, + ); mockFailedThenSuccessfulAttempt("rate limit"); await runAutoPinnedOpenAiTurn({ diff --git a/src/agents/model-fallback.run-embedded.e2e.test.ts b/src/agents/model-fallback.run-embedded.e2e.test.ts index 804b5c8c63c..2dbf6fdc7dd 100644 --- a/src/agents/model-fallback.run-embedded.e2e.test.ts +++ b/src/agents/model-fallback.run-embedded.e2e.test.ts @@ -7,6 +7,7 @@ import type { AuthProfileFailureReason } from "./auth-profiles.js"; import { classifyEmbeddedAgentRunResultForModelFallback } from "./embedded-agent-runner/result-fallback-classifier.js"; import type { EmbeddedRunAttemptResult } from "./embedded-agent-runner/run/types.js"; import { runWithModelFallback } from "./model-fallback.js"; +import { ensureAuthProfileStore, saveAuthProfileStore } from "./auth-profiles/store.js"; import { buildEmbeddedRunnerAssistant, createResolvedEmbeddedRunnerModel, @@ -155,33 +156,26 @@ async function writeAuthStore( } >, ) { - await fs.writeFile( - path.join(agentDir, "auth-profiles.json"), - JSON.stringify({ + saveAuthProfileStore( + { version: 1, profiles: { "openai:p1": { type: "api_key", provider: "openai", key: "sk-openai" }, "groq:p1": { type: "api_key", provider: "groq", key: "sk-groq" }, }, - }), - ); - await fs.writeFile( - path.join(agentDir, "auth-state.json"), - JSON.stringify({ - version: 1, usageStats: usageStats ?? ({ "openai:p1": { lastUsed: 1 }, "groq:p1": { lastUsed: 2 }, } as const), - }), + }, + agentDir, ); } async function readUsageStats(agentDir: string) { - const raw = await fs.readFile(path.join(agentDir, "auth-state.json"), "utf-8"); - return JSON.parse(raw).usageStats as Record | undefined>; + return ensureAuthProfileStore(agentDir, { syncExternalCli: false }).usageStats ?? {}; } function expectFailureCount( @@ -195,9 +189,8 @@ function expectFailureCount( } async function writeMultiProfileAuthStore(agentDir: string) { - await fs.writeFile( - path.join(agentDir, "auth-profiles.json"), - JSON.stringify({ + saveAuthProfileStore( + { version: 1, profiles: { "openai:p1": { type: "api_key", provider: "openai", key: "sk-openai-1" }, @@ -205,19 +198,14 @@ async function writeMultiProfileAuthStore(agentDir: string) { "openai:p3": { type: "api_key", provider: "openai", key: "sk-openai-3" }, "groq:p1": { type: "api_key", provider: "groq", key: "sk-groq" }, }, - }), - ); - await fs.writeFile( - path.join(agentDir, "auth-state.json"), - JSON.stringify({ - version: 1, usageStats: { "openai:p1": { lastUsed: 1 }, "openai:p2": { lastUsed: 2 }, "openai:p3": { lastUsed: 3 }, "groq:p1": { lastUsed: 4 }, }, - }), + }, + agentDir, ); } diff --git a/src/plugins/install.npm-spec.e2e.test.ts b/src/plugins/install.npm-spec.e2e.test.ts index c94bfe5d6dd..9fe16fac4ec 100644 --- a/src/plugins/install.npm-spec.e2e.test.ts +++ b/src/plugins/install.npm-spec.e2e.test.ts @@ -6,8 +6,9 @@ import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolvePluginNpmProjectDir } from "./install-paths.js"; -import { installPluginFromNpmSpec } from "./install.js"; +import { installPluginFromNpmSpec, PLUGIN_INSTALL_ERROR_CODE } from "./install.js"; type PackedVersion = { archive: Buffer; @@ -51,6 +52,43 @@ async function makeTempDir(label: string): Promise { return dir; } +function configWithInstalledPackageTreeBlockPolicy(): OpenClawConfig { + return { + security: { + installPolicy: { + enabled: true, + exec: { + source: "exec", + command: process.execPath, + args: [ + "-e", + ` +let input = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { input += chunk; }); +process.stdin.on("end", () => { + const request = JSON.parse(input); + if (request.sourcePathKind === "directory") { + process.stdout.write(JSON.stringify({ + protocolVersion: 1, + decision: "block", + reason: "blocked installed package tree", + })); + return; + } + process.stdout.write(JSON.stringify({ protocolVersion: 1, decision: "allow" })); +}); +`, + ], + allowInsecurePath: true, + timeoutMs: 5000, + maxOutputBytes: 16 * 1024, + }, + }, + }, + }; +} + function pluginNpmProjectRoot(npmRoot: string, packageName: string): string { return resolvePluginNpmProjectDir({ npmDir: npmRoot, packageName }); } @@ -816,7 +854,7 @@ describe("installPluginFromNpmSpec e2e", () => { ).resolves.toBeTruthy(); }); - it("rolls back managed peer dependencies added before a failed install scan", async () => { + it("rolls back managed peer dependencies added before a failed installed package policy scan", async () => { const rootDir = await makeTempDir("npm-plugin-peer-rollback-e2e"); const npmRoot = path.join(rootDir, "managed-npm"); const blockedPlugin = `blocked-plugin-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`; @@ -854,6 +892,7 @@ describe("installPluginFromNpmSpec e2e", () => { process.env.npm_config_registry = registry; const result = await installPluginFromNpmSpec({ + config: configWithInstalledPackageTreeBlockPolicy(), spec: `${blockedPlugin}@1.0.0`, npmDir: npmRoot, logger: { info: () => {}, warn: () => {} }, @@ -861,6 +900,10 @@ describe("installPluginFromNpmSpec e2e", () => { }); expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); + expect(result.error).toContain("blocked by install policy: blocked installed package tree"); + } const projectRoot = pluginNpmProjectRoot(npmRoot, blockedPlugin); try { const rootManifest = JSON.parse( @@ -1013,6 +1056,7 @@ describe("installPluginFromNpmSpec e2e", () => { ); const result = await installPluginFromNpmSpec({ + config: configWithInstalledPackageTreeBlockPolicy(), spec: `${blockedPlugin}@1.0.0`, npmDir: npmRoot, logger: { info: () => {}, warn: () => {} }, @@ -1020,6 +1064,10 @@ describe("installPluginFromNpmSpec e2e", () => { }); expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); + expect(result.error).toContain("blocked by install policy: blocked installed package tree"); + } const rootManifest = JSON.parse( await fs.readFile(path.join(blockedProjectRoot, "package.json"), "utf8"), ) as {