From e7e4fbcab943e2a579ca8f54ac67a70d551194ac Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 17:39:58 +0000 Subject: [PATCH] test: dedupe secrets and guardrail fixtures --- src/infra/state-migrations.test.ts | 107 ++++----- .../channel-import-guardrails.test.ts | 217 +++++++----------- src/secrets/runtime.integration.test.ts | 208 +++++++---------- 3 files changed, 220 insertions(+), 312 deletions(-) diff --git a/src/infra/state-migrations.test.ts b/src/infra/state-migrations.test.ts index f8437f85529..5462463a9a4 100644 --- a/src/infra/state-migrations.test.ts +++ b/src/infra/state-migrations.test.ts @@ -35,41 +35,55 @@ function createEnv(stateDir: string): NodeJS.ProcessEnv { }; } +async function createLegacyStateFixture(params?: { includePreKey?: boolean }) { + const root = await createTempDir(); + const stateDir = path.join(root, ".openclaw"); + const env = createEnv(stateDir); + const cfg = createConfig(); + + await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "agent"), { recursive: true }); + await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true }); + + await fs.writeFile( + path.join(stateDir, "sessions", "sessions.json"), + `${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`, + "utf8", + ); + await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8"); + await fs.writeFile( + path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"), + `${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`, + "utf8", + ); + await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8"); + await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8"); + if (params?.includePreKey) { + await fs.writeFile( + path.join(stateDir, "credentials", "pre-key-1.json"), + '{"preKey":true}\n', + "utf8", + ); + } + await fs.writeFile(path.join(stateDir, "credentials", "oauth.json"), '{"oauth":true}\n', "utf8"); + await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8"); + + return { + root, + stateDir, + env, + cfg, + }; +} + afterEach(async () => { await tempDirs.cleanup(); }); describe("state migrations", () => { it("detects legacy sessions, agent files, whatsapp auth, and telegram allowFrom copies", async () => { - const root = await createTempDir(); - const stateDir = path.join(root, ".openclaw"); - const env = createEnv(stateDir); - const cfg = createConfig(); - - await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true }); - await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true }); - await fs.mkdir(path.join(stateDir, "agent"), { recursive: true }); - await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true }); - - await fs.writeFile( - path.join(stateDir, "sessions", "sessions.json"), - `${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`, - "utf8", - ); - await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8"); - await fs.writeFile( - path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"), - `${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`, - "utf8", - ); - await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8"); - await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8"); - await fs.writeFile( - path.join(stateDir, "credentials", "oauth.json"), - '{"oauth":true}\n', - "utf8", - ); - await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8"); + const { root, stateDir, env, cfg } = await createLegacyStateFixture(); const detected = await detectLegacyStateMigrations({ cfg, @@ -99,40 +113,7 @@ describe("state migrations", () => { }); it("runs legacy state migrations and canonicalizes the merged session store", async () => { - const root = await createTempDir(); - const stateDir = path.join(root, ".openclaw"); - const env = createEnv(stateDir); - const cfg = createConfig(); - - await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true }); - await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true }); - await fs.mkdir(path.join(stateDir, "agent"), { recursive: true }); - await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true }); - - await fs.writeFile( - path.join(stateDir, "sessions", "sessions.json"), - `${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`, - "utf8", - ); - await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8"); - await fs.writeFile( - path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"), - `${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`, - "utf8", - ); - await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8"); - await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8"); - await fs.writeFile( - path.join(stateDir, "credentials", "pre-key-1.json"), - '{"preKey":true}\n', - "utf8", - ); - await fs.writeFile( - path.join(stateDir, "credentials", "oauth.json"), - '{"oauth":true}\n', - "utf8", - ); - await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8"); + const { root, stateDir, env, cfg } = await createLegacyStateFixture({ includePreKey: true }); const detected = await detectLegacyStateMigrations({ cfg, diff --git a/src/plugin-sdk/channel-import-guardrails.test.ts b/src/plugin-sdk/channel-import-guardrails.test.ts index 339b13a2e14..1f7259db4f5 100644 --- a/src/plugin-sdk/channel-import-guardrails.test.ts +++ b/src/plugin-sdk/channel-import-guardrails.test.ts @@ -164,6 +164,12 @@ let extensionSourceFilesCache: string[] | null = null; let coreSourceFilesCache: string[] | null = null; const extensionFilesCache = new Map(); +type SourceFileCollectorOptions = { + rootDir: string; + shouldSkipPath?: (normalizedFullPath: string) => boolean; + shouldSkipEntry?: (params: { entryName: string; normalizedFullPath: string }) => boolean; +}; + function readSource(path: string): string { const fullPath = resolve(REPO_ROOT, path); const cached = sourceTextCache.get(fullPath); @@ -179,6 +185,51 @@ function normalizePath(path: string): string { return path.replaceAll("\\", "/"); } +function collectSourceFiles( + cached: string[] | undefined | null, + options: SourceFileCollectorOptions, +): string[] { + if (cached) { + return cached; + } + const files: string[] = []; + const stack = [options.rootDir]; + while (stack.length > 0) { + const current = stack.pop(); + if (!current) { + continue; + } + for (const entry of readdirSync(current, { withFileTypes: true })) { + const fullPath = resolve(current, entry.name); + const normalizedFullPath = normalizePath(fullPath); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") { + continue; + } + if (options.shouldSkipPath?.(normalizedFullPath)) { + continue; + } + stack.push(fullPath); + continue; + } + if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) { + continue; + } + if (entry.name.endsWith(".d.ts")) { + continue; + } + if ( + options.shouldSkipPath?.(normalizedFullPath) || + options.shouldSkipEntry?.({ entryName: entry.name, normalizedFullPath }) + ) { + continue; + } + files.push(fullPath); + } + } + return files; +} + function readSetupBarrelImportBlock(path: string): string { const lines = readSource(path).split("\n"); const targetLineIndex = lines.findIndex((line) => @@ -195,145 +246,55 @@ function readSetupBarrelImportBlock(path: string): string { } function collectExtensionSourceFiles(): string[] { - if (extensionSourceFilesCache) { - return extensionSourceFilesCache; - } const extensionsDir = normalizePath(resolve(ROOT_DIR, "..", "extensions")); const sharedExtensionsDir = normalizePath(resolve(extensionsDir, "shared")); - const files: string[] = []; - const stack = [resolve(ROOT_DIR, "..", "extensions")]; - while (stack.length > 0) { - const current = stack.pop(); - if (!current) { - continue; - } - for (const entry of readdirSync(current, { withFileTypes: true })) { - const fullPath = resolve(current, entry.name); - const normalizedFullPath = normalizePath(fullPath); - if (entry.isDirectory()) { - if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") { - continue; - } - stack.push(fullPath); - continue; - } - if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) { - continue; - } - if (entry.name.endsWith(".d.ts") || normalizedFullPath.includes(sharedExtensionsDir)) { - continue; - } - if (normalizedFullPath.includes(`${extensionsDir}/shared/`)) { - continue; - } - if ( - normalizedFullPath.includes(".test.") || - normalizedFullPath.includes(".test-") || - normalizedFullPath.includes(".fixture.") || - normalizedFullPath.includes(".snap") || - normalizedFullPath.includes("test-support") || - entry.name === "api.ts" || - entry.name === "runtime-api.ts" - ) { - continue; - } - files.push(fullPath); - } - } - extensionSourceFilesCache = files; - return files; + extensionSourceFilesCache = collectSourceFiles(extensionSourceFilesCache, { + rootDir: resolve(ROOT_DIR, "..", "extensions"), + shouldSkipPath: (normalizedFullPath) => + normalizedFullPath.includes(sharedExtensionsDir) || + normalizedFullPath.includes(`${extensionsDir}/shared/`), + shouldSkipEntry: ({ entryName, normalizedFullPath }) => + normalizedFullPath.includes(".test.") || + normalizedFullPath.includes(".test-") || + normalizedFullPath.includes(".fixture.") || + normalizedFullPath.includes(".snap") || + normalizedFullPath.includes("test-support") || + entryName === "api.ts" || + entryName === "runtime-api.ts", + }); + return extensionSourceFilesCache; } function collectCoreSourceFiles(): string[] { - if (coreSourceFilesCache) { - return coreSourceFilesCache; - } const srcDir = resolve(ROOT_DIR, "..", "src"); const normalizedPluginSdkDir = normalizePath(resolve(ROOT_DIR, "plugin-sdk")); - const files: string[] = []; - const stack = [srcDir]; - while (stack.length > 0) { - const current = stack.pop(); - if (!current) { - continue; - } - for (const entry of readdirSync(current, { withFileTypes: true })) { - const fullPath = resolve(current, entry.name); - const normalizedFullPath = normalizePath(fullPath); - if (entry.isDirectory()) { - if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") { - continue; - } - stack.push(fullPath); - continue; - } - if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) { - continue; - } - if (entry.name.endsWith(".d.ts")) { - continue; - } - if ( - normalizedFullPath.includes(".test.") || - normalizedFullPath.includes(".mock-harness.") || - normalizedFullPath.includes(".spec.") || - normalizedFullPath.includes(".fixture.") || - normalizedFullPath.includes(".snap") || - // src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated - // plugin-sdk guardrails instead of the generic "core should not touch extensions" rule. - normalizedFullPath.includes(`${normalizedPluginSdkDir}/`) - ) { - continue; - } - files.push(fullPath); - } - } - coreSourceFilesCache = files; - return files; + coreSourceFilesCache = collectSourceFiles(coreSourceFilesCache, { + rootDir: srcDir, + shouldSkipEntry: ({ normalizedFullPath }) => + normalizedFullPath.includes(".test.") || + normalizedFullPath.includes(".mock-harness.") || + normalizedFullPath.includes(".spec.") || + normalizedFullPath.includes(".fixture.") || + normalizedFullPath.includes(".snap") || + // src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated + // plugin-sdk guardrails instead of the generic "core should not touch extensions" rule. + normalizedFullPath.includes(`${normalizedPluginSdkDir}/`), + }); + return coreSourceFilesCache; } function collectExtensionFiles(extensionId: string): string[] { const cached = extensionFilesCache.get(extensionId); - if (cached) { - return cached; - } - const extensionDir = resolve(ROOT_DIR, "..", "extensions", extensionId); - const files: string[] = []; - const stack = [extensionDir]; - while (stack.length > 0) { - const current = stack.pop(); - if (!current) { - continue; - } - for (const entry of readdirSync(current, { withFileTypes: true })) { - const fullPath = resolve(current, entry.name); - const normalizedFullPath = normalizePath(fullPath); - if (entry.isDirectory()) { - if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") { - continue; - } - stack.push(fullPath); - continue; - } - if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) { - continue; - } - if (entry.name.endsWith(".d.ts")) { - continue; - } - if ( - normalizedFullPath.includes(".test.") || - normalizedFullPath.includes(".test-") || - normalizedFullPath.includes(".spec.") || - normalizedFullPath.includes(".fixture.") || - normalizedFullPath.includes(".snap") || - entry.name === "runtime-api.ts" - ) { - continue; - } - files.push(fullPath); - } - } + const files = collectSourceFiles(cached, { + rootDir: resolve(ROOT_DIR, "..", "extensions", extensionId), + shouldSkipEntry: ({ entryName, normalizedFullPath }) => + normalizedFullPath.includes(".test.") || + normalizedFullPath.includes(".test-") || + normalizedFullPath.includes(".spec.") || + normalizedFullPath.includes(".fixture.") || + normalizedFullPath.includes(".snap") || + entryName === "runtime-api.ts", + }); extensionFilesCache.set(extensionId, files); return files; } diff --git a/src/secrets/runtime.integration.test.ts b/src/secrets/runtime.integration.test.ts index d52483944ef..eae46e60408 100644 --- a/src/secrets/runtime.integration.test.ts +++ b/src/secrets/runtime.integration.test.ts @@ -22,6 +22,11 @@ import { vi.unmock("../version.js"); const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const; +const OPENAI_FILE_KEY_REF = { + source: "file", + provider: "default", + id: "/providers/openai/apiKey", +} as const; const allowInsecureTempSecretFile = process.platform === "win32"; function asConfig(value: unknown): OpenClawConfig { @@ -35,6 +40,77 @@ function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): Auth }; } +async function createOpenAIFileRuntimeFixture(home: string) { + const configDir = path.join(home, ".openclaw"); + const secretFile = path.join(configDir, "secrets.json"); + const agentDir = path.join(configDir, "agents", "main", "agent"); + const authStorePath = path.join(agentDir, "auth-profiles.json"); + + await fs.mkdir(agentDir, { recursive: true }); + await fs.chmod(configDir, 0o700).catch(() => {}); + await fs.writeFile( + secretFile, + `${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + await fs.writeFile( + authStorePath, + `${JSON.stringify( + { + version: 1, + profiles: { + "openai:default": { + type: "api_key", + provider: "openai", + keyRef: OPENAI_FILE_KEY_REF, + }, + }, + }, + null, + 2, + )}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + + return { + configDir, + secretFile, + agentDir, + }; +} + +function createOpenAIFileRuntimeConfig(secretFile: string): OpenClawConfig { + return asConfig({ + secrets: { + providers: { + default: { + source: "file", + path: secretFile, + mode: "json", + ...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}), + }, + }, + }, + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + apiKey: OPENAI_FILE_KEY_REF, + models: [], + }, + }, + }, + }); +} + +function expectResolvedOpenAIRuntime(agentDir: string) { + expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); + expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ + type: "api_key", + key: "sk-file-runtime", + }); +} + describe("secrets runtime snapshot integration", () => { let envSnapshot: ReturnType; @@ -106,68 +182,16 @@ describe("secrets runtime snapshot integration", () => { return; } await withTempHome("openclaw-secrets-runtime-write-", async (home) => { - const configDir = path.join(home, ".openclaw"); - const secretFile = path.join(configDir, "secrets.json"); - const agentDir = path.join(configDir, "agents", "main", "agent"); - const authStorePath = path.join(agentDir, "auth-profiles.json"); - await fs.mkdir(agentDir, { recursive: true }); - await fs.chmod(configDir, 0o700).catch(() => {}); - await fs.writeFile( - secretFile, - `${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, - { encoding: "utf8", mode: 0o600 }, - ); - await fs.writeFile( - authStorePath, - `${JSON.stringify( - { - version: 1, - profiles: { - "openai:default": { - type: "api_key", - provider: "openai", - keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, - }, - }, - }, - null, - 2, - )}\n`, - { encoding: "utf8", mode: 0o600 }, - ); + const { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home); const prepared = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - secrets: { - providers: { - default: { - source: "file", - path: secretFile, - mode: "json", - ...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}), - }, - }, - }, - models: { - providers: { - openai: { - baseUrl: "https://api.openai.com/v1", - apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, - models: [], - }, - }, - }, - }), + config: createOpenAIFileRuntimeConfig(secretFile), agentDirs: [agentDir], }); activateSecretsRuntimeSnapshot(prepared); - expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); - expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ - type: "api_key", - key: "sk-file-runtime", - }); + expectResolvedOpenAIRuntime(agentDir); await writeConfigFile({ ...loadConfig(), @@ -175,11 +199,7 @@ describe("secrets runtime snapshot integration", () => { }); expect(loadConfig().gateway?.auth).toEqual({ mode: "token" }); - expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); - expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ - type: "api_key", - key: "sk-file-runtime", - }); + expectResolvedOpenAIRuntime(agentDir); }); }); @@ -188,35 +208,7 @@ describe("secrets runtime snapshot integration", () => { return; } await withTempHome("openclaw-secrets-runtime-refresh-fail-", async (home) => { - const configDir = path.join(home, ".openclaw"); - const secretFile = path.join(configDir, "secrets.json"); - const agentDir = path.join(configDir, "agents", "main", "agent"); - const authStorePath = path.join(agentDir, "auth-profiles.json"); - await fs.mkdir(agentDir, { recursive: true }); - await fs.chmod(configDir, 0o700).catch(() => {}); - await fs.writeFile( - secretFile, - `${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, - { encoding: "utf8", mode: 0o600 }, - ); - await fs.writeFile( - authStorePath, - `${JSON.stringify( - { - version: 1, - profiles: { - "openai:default": { - type: "api_key", - provider: "openai", - keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, - }, - }, - }, - null, - 2, - )}\n`, - { encoding: "utf8", mode: 0o600 }, - ); + const { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home); let loadAuthStoreCalls = 0; const loadAuthStore = () => { @@ -228,33 +220,13 @@ describe("secrets runtime snapshot integration", () => { "openai:default": { type: "api_key", provider: "openai", - keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, + keyRef: OPENAI_FILE_KEY_REF, }, }); }; const prepared = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - secrets: { - providers: { - default: { - source: "file", - path: secretFile, - mode: "json", - ...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}), - }, - }, - }, - models: { - providers: { - openai: { - baseUrl: "https://api.openai.com/v1", - apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, - models: [], - }, - }, - }, - }), + config: createOpenAIFileRuntimeConfig(secretFile), agentDirs: [agentDir], loadAuthStore, }); @@ -273,16 +245,10 @@ describe("secrets runtime snapshot integration", () => { const activeAfterFailure = getActiveSecretsRuntimeSnapshot(); expect(activeAfterFailure).not.toBeNull(); expect(loadConfig().gateway?.auth).toBeUndefined(); - expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); - expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual({ - source: "file", - provider: "default", - id: "/providers/openai/apiKey", - }); - expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ - type: "api_key", - key: "sk-file-runtime", - }); + expectResolvedOpenAIRuntime(agentDir); + expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual( + OPENAI_FILE_KEY_REF, + ); }); });