mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-13 03:12:58 +00:00
test: align e2e fixtures with current runtime stores
This commit is contained in:
@@ -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<typeof getGlobalHookRunner>,
|
||||
);
|
||||
});
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
@@ -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<string, { lastUsed?: number }>),
|
||||
},
|
||||
};
|
||||
const statePayload = {
|
||||
version: 1,
|
||||
...(opts?.order ? { order: opts.order } : {}),
|
||||
usageStats:
|
||||
opts?.usageStats ??
|
||||
({
|
||||
"openai:p1": { lastUsed: 1 },
|
||||
"openai:p2": { lastUsed: 2 },
|
||||
} as Record<string, { lastUsed?: number }>),
|
||||
};
|
||||
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<AssistantMessage> = {}) =>
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string, Record<string, unknown> | 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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user