From bb15b7c53c28ede5b8ea4c614b578c8667bb94ca Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 01:48:26 +0100 Subject: [PATCH] perf(runtime): trim config, media, and secrets tests --- .../config.nix-integration-u3-u5-u9.test.ts | 140 +----------------- .../config.telegram-audio-preflight.test.ts | 55 +++++++ src/config/normalize-paths.test.ts | 10 ++ .../runner.auto-audio.test.ts | 22 ++- .../runtime-auth.integration.test-helpers.ts | 18 +++ src/secrets/runtime.auth.integration.test.ts | 80 +++++++++- 6 files changed, 181 insertions(+), 144 deletions(-) diff --git a/src/config/config.nix-integration-u3-u5-u9.test.ts b/src/config/config.nix-integration-u3-u5-u9.test.ts index 68c04210968..2778f19a91f 100644 --- a/src/config/config.nix-integration-u3-u5-u9.test.ts +++ b/src/config/config.nix-integration-u3-u5-u9.test.ts @@ -1,15 +1,13 @@ -import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { - createConfigIO, DEFAULT_GATEWAY_PORT, resolveConfigPathCandidate, resolveGatewayPort, resolveIsNixMode, resolveStateDir, } from "./config.js"; -import { withTempHome, withTempHomeConfig } from "./test-helpers.js"; +import { withTempHome } from "./test-helpers.js"; vi.unmock("../version.js"); @@ -18,23 +16,6 @@ function envWith(overrides: Record): NodeJS.ProcessE return { ...overrides }; } -function loadConfigForHome(home: string) { - return createConfigIO({ - env: envWith({ OPENCLAW_HOME: home }), - homedir: () => home, - }).loadConfig(); -} - -async function withLoadedConfigForHome( - config: unknown, - run: (cfg: ReturnType) => Promise | void, -) { - await withTempHomeConfig(config, async ({ home }) => { - const cfg = loadConfigForHome(home); - await run(cfg); - }); -} - describe("Nix integration (U3, U5, U9)", () => { afterEach(() => { vi.restoreAllMocks(); @@ -126,82 +107,6 @@ describe("Nix integration (U3, U5, U9)", () => { }); }); - describe("U5b: tilde expansion for config paths", () => { - it("expands ~ in common path-ish config fields", async () => { - await withTempHome(async (home) => { - const configDir = path.join(home, ".openclaw"); - await fs.mkdir(configDir, { recursive: true }); - const pluginDir = path.join(home, "plugins", "demo-plugin"); - await fs.mkdir(pluginDir, { recursive: true }); - await fs.writeFile( - path.join(pluginDir, "index.js"), - 'export default { id: "demo-plugin", register() {} };', - "utf-8", - ); - await fs.writeFile( - path.join(pluginDir, "openclaw.plugin.json"), - JSON.stringify( - { - id: "demo-plugin", - configSchema: { type: "object", additionalProperties: false, properties: {} }, - }, - null, - 2, - ), - "utf-8", - ); - await fs.writeFile( - path.join(configDir, "openclaw.json"), - JSON.stringify( - { - plugins: { - load: { - paths: ["~/plugins/demo-plugin"], - }, - }, - agents: { - defaults: { workspace: "~/ws-default" }, - list: [ - { - id: "main", - workspace: "~/ws-agent", - agentDir: "~/.openclaw/agents/main", - sandbox: { workspaceRoot: "~/sandbox-root" }, - }, - ], - }, - channels: { - whatsapp: { - accounts: { - personal: { - authDir: "~/.openclaw/credentials/wa-personal", - }, - }, - }, - }, - }, - null, - 2, - ), - "utf-8", - ); - - const cfg = loadConfigForHome(home); - - expect(cfg.plugins?.load?.paths?.[0]).toBe(path.join(home, "plugins", "demo-plugin")); - expect(cfg.agents?.defaults?.workspace).toBe(path.join(home, "ws-default")); - expect(cfg.agents?.list?.[0]?.workspace).toBe(path.join(home, "ws-agent")); - expect(cfg.agents?.list?.[0]?.agentDir).toBe( - path.join(home, ".openclaw", "agents", "main"), - ); - expect(cfg.agents?.list?.[0]?.sandbox?.workspaceRoot).toBe(path.join(home, "sandbox-root")); - expect(cfg.channels?.whatsapp?.accounts?.personal?.authDir).toBe( - path.join(home, ".openclaw", "credentials", "wa-personal"), - ); - }); - }); - }); - describe("U6: gateway port resolution", () => { it("uses default when env and config are unset", () => { expect(resolveGatewayPort({}, envWith({ OPENCLAW_GATEWAY_PORT: undefined }))).toBe( @@ -227,47 +132,4 @@ describe("Nix integration (U3, U5, U9)", () => { ).toBe(19003); }); }); - - describe("U9: telegram.tokenFile schema validation", () => { - it("accepts config with only botToken", async () => { - await withLoadedConfigForHome( - { - channels: { telegram: { botToken: "123:ABC" } }, - }, - async (cfg) => { - expect(cfg.channels?.telegram?.botToken).toBe("123:ABC"); - expect(cfg.channels?.telegram?.tokenFile).toBeUndefined(); - }, - ); - }); - - it("accepts config with only tokenFile", async () => { - await withLoadedConfigForHome( - { - channels: { telegram: { tokenFile: "/run/agenix/telegram-token" } }, - }, - async (cfg) => { - expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token"); - expect(cfg.channels?.telegram?.botToken).toBeUndefined(); - }, - ); - }); - - it("accepts config with both botToken and tokenFile", async () => { - await withLoadedConfigForHome( - { - channels: { - telegram: { - botToken: "fallback:token", - tokenFile: "/run/agenix/telegram-token", - }, - }, - }, - async (cfg) => { - expect(cfg.channels?.telegram?.botToken).toBe("fallback:token"); - expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token"); - }, - ); - }); - }); }); diff --git a/src/config/config.telegram-audio-preflight.test.ts b/src/config/config.telegram-audio-preflight.test.ts index 42c10e23c7f..c27cfcaf54d 100644 --- a/src/config/config.telegram-audio-preflight.test.ts +++ b/src/config/config.telegram-audio-preflight.test.ts @@ -46,4 +46,59 @@ describe("telegram disableAudioPreflight schema", () => { expect(res.success).toBe(false); }); + + it("accepts telegram botToken without tokenFile", () => { + const res = OpenClawSchema.safeParse({ + channels: { + telegram: { + botToken: "123:ABC", + }, + }, + }); + + expect(res.success).toBe(true); + if (!res.success) { + return; + } + + expect(res.data.channels?.telegram?.botToken).toBe("123:ABC"); + expect(res.data.channels?.telegram?.tokenFile).toBeUndefined(); + }); + + it("accepts telegram tokenFile without botToken", () => { + const res = OpenClawSchema.safeParse({ + channels: { + telegram: { + tokenFile: "/run/agenix/telegram-token", + }, + }, + }); + + expect(res.success).toBe(true); + if (!res.success) { + return; + } + + expect(res.data.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token"); + expect(res.data.channels?.telegram?.botToken).toBeUndefined(); + }); + + it("accepts telegram botToken and tokenFile together", () => { + const res = OpenClawSchema.safeParse({ + channels: { + telegram: { + botToken: "fallback:token", + tokenFile: "/run/agenix/telegram-token", + }, + }, + }); + + expect(res.success).toBe(true); + if (!res.success) { + return; + } + + expect(res.data.channels?.telegram?.botToken).toBe("fallback:token"); + expect(res.data.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token"); + }); }); diff --git a/src/config/normalize-paths.test.ts b/src/config/normalize-paths.test.ts index e0e0f0a0147..ba5c3f5bd72 100644 --- a/src/config/normalize-paths.test.ts +++ b/src/config/normalize-paths.test.ts @@ -22,6 +22,13 @@ describe("normalizeConfigPaths", () => { }, }, }, + whatsapp: { + accounts: { + personal: { + authDir: "~/.openclaw/credentials/wa-personal", + }, + }, + }, imessage: { accounts: { personal: { dbPath: "~/Library/Messages/chat.db" } }, }, @@ -50,6 +57,9 @@ describe("normalizeConfigPaths", () => { expect(cfg.channels?.telegram?.accounts?.personal?.tokenFile).toBe( path.join(home, ".openclaw", "telegram.token"), ); + expect(cfg.channels?.whatsapp?.accounts?.personal?.authDir).toBe( + path.join(home, ".openclaw", "credentials", "wa-personal"), + ); expect(cfg.channels?.imessage?.accounts?.personal?.dbPath).toBe( path.join(home, "Library", "Messages", "chat.db"), ); diff --git a/src/media-understanding/runner.auto-audio.test.ts b/src/media-understanding/runner.auto-audio.test.ts index c738ce741c8..925329f9904 100644 --- a/src/media-understanding/runner.auto-audio.test.ts +++ b/src/media-understanding/runner.auto-audio.test.ts @@ -1,13 +1,33 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withEnvAsync } from "../test-utils/env.js"; import { runCapability } from "./runner.js"; import { withAudioFixture } from "./runner.test-utils.js"; import type { AudioTranscriptionRequest, MediaUnderstandingProvider } from "./types.js"; +const modelAuthMocks = vi.hoisted(() => ({ + hasAvailableAuthForProvider: vi.fn(() => true), + resolveApiKeyForProvider: vi.fn(async () => ({ + apiKey: "test-key", + source: "test", + mode: "api-key", + })), + requireApiKey: vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? "test-key"), +})); + +vi.mock("../agents/model-auth.js", () => ({ + hasAvailableAuthForProvider: modelAuthMocks.hasAvailableAuthForProvider, + resolveApiKeyForProvider: modelAuthMocks.resolveApiKeyForProvider, + requireApiKey: modelAuthMocks.requireApiKey, +})); + +vi.mock("../plugins/capability-provider-runtime.js", () => ({ + resolvePluginCapabilityProviders: () => [], +})); + function createProviderRegistry( providers: Record, ): Map { diff --git a/src/secrets/runtime-auth.integration.test-helpers.ts b/src/secrets/runtime-auth.integration.test-helpers.ts index 1be41367614..a0a075451e1 100644 --- a/src/secrets/runtime-auth.integration.test-helpers.ts +++ b/src/secrets/runtime-auth.integration.test-helpers.ts @@ -7,6 +7,20 @@ import { clearConfigCache, clearRuntimeConfigSnapshot, loadConfig } from "../con import { captureEnv } from "../test-utils/env.js"; import { clearSecretsRuntimeSnapshot } from "./runtime.js"; +const secretsRuntimePluginMocks = vi.hoisted(() => ({ + resolveExternalAuthProfilesWithPluginsMock: vi.fn(() => []), + resolvePluginWebSearchProvidersMock: vi.fn(() => []), +})); + +vi.mock("../plugins/web-search-providers.runtime.js", () => ({ + resolvePluginWebSearchProviders: secretsRuntimePluginMocks.resolvePluginWebSearchProvidersMock, +})); + +vi.mock("../plugins/provider-runtime.js", () => ({ + resolveExternalAuthProfilesWithPlugins: + secretsRuntimePluginMocks.resolveExternalAuthProfilesWithPluginsMock, +})); + export const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", @@ -109,6 +123,10 @@ export function expectResolvedOpenAIRuntime(agentDir: string) { } export function beginSecretsRuntimeIsolationForTest(): SecretsRuntimeEnvSnapshot { + secretsRuntimePluginMocks.resolveExternalAuthProfilesWithPluginsMock.mockReset(); + secretsRuntimePluginMocks.resolveExternalAuthProfilesWithPluginsMock.mockReturnValue([]); + secretsRuntimePluginMocks.resolvePluginWebSearchProvidersMock.mockReset(); + secretsRuntimePluginMocks.resolvePluginWebSearchProvidersMock.mockReturnValue([]); const envSnapshot = captureEnv([ "OPENCLAW_BUNDLED_PLUGINS_DIR", "OPENCLAW_DISABLE_BUNDLED_PLUGINS", diff --git a/src/secrets/runtime.auth.integration.test.ts b/src/secrets/runtime.auth.integration.test.ts index 5243a63ba84..e17defc6e89 100644 --- a/src/secrets/runtime.auth.integration.test.ts +++ b/src/secrets/runtime.auth.integration.test.ts @@ -1,7 +1,8 @@ +import { readFileSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ensureAuthProfileStore } from "../agents/auth-profiles.js"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; import { withTempHome } from "../config/home-env.test-harness.js"; import { asConfig, @@ -11,10 +12,71 @@ import { OPENAI_ENV_KEY_REF, type SecretsRuntimeEnvSnapshot, } from "./runtime-auth.integration.test-helpers.js"; -import { activateSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } from "./runtime.js"; +import { + activateSecretsRuntimeSnapshot, + getActiveSecretsRuntimeSnapshot, + prepareSecretsRuntimeSnapshot, +} from "./runtime.js"; vi.unmock("../version.js"); +vi.mock("./runtime-prepare.runtime.js", () => ({ + createResolverContext: ({ + sourceConfig, + env, + }: { + sourceConfig: unknown; + env: NodeJS.ProcessEnv; + }) => ({ + sourceConfig, + env, + cache: {}, + warnings: [], + warningKeys: new Set(), + assignments: [], + }), + collectConfigAssignments: () => {}, + collectAuthStoreAssignments: ({ + store, + context, + }: { + store: AuthProfileStore; + context: { env: NodeJS.ProcessEnv }; + }) => { + for (const profile of Object.values(store.profiles)) { + if ( + profile?.type === "api_key" && + profile.keyRef?.source === "env" && + typeof profile.keyRef.id === "string" + ) { + const key = context.env[profile.keyRef.id]; + if (typeof key === "string" && key.length > 0) { + profile.key = key; + } + } + } + }, + resolveSecretRefValues: async () => new Map(), + applyResolvedAssignments: () => {}, + resolveRuntimeWebTools: async () => ({ + search: { providerSource: "none", diagnostics: [] }, + fetch: { providerSource: "none", diagnostics: [] }, + diagnostics: [], + }), +})); + +function loadAuthStoreFromTestFile(agentDir?: string): AuthProfileStore { + if (!agentDir) { + return { version: 1, profiles: {} }; + } + try { + const raw = readFileSync(path.join(agentDir, "auth-profiles.json"), "utf8"); + return JSON.parse(raw) as AuthProfileStore; + } catch { + return { version: 1, profiles: {} }; + } +} + describe("secrets runtime snapshot auth integration", () => { let envSnapshot: SecretsRuntimeEnvSnapshot; @@ -75,11 +137,16 @@ describe("secrets runtime snapshot auth integration", () => { OPENAI_API_KEY: "sk-main-runtime", ANTHROPIC_API_KEY: "sk-ops-runtime", }, + loadAuthStore: loadAuthStoreFromTestFile, loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, }); activateSecretsRuntimeSnapshot(prepared); - expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toBeUndefined(); + expect( + getActiveSecretsRuntimeSnapshot()?.authStores.find( + (entry) => entry.agentDir === opsAgentDir, + ), + ).toBeUndefined(); const refreshed = await prepareSecretsRuntimeSnapshot({ config: asConfig({ @@ -91,11 +158,16 @@ describe("secrets runtime snapshot auth integration", () => { OPENAI_API_KEY: "sk-main-runtime", ANTHROPIC_API_KEY: "sk-ops-runtime", }, + loadAuthStore: loadAuthStoreFromTestFile, loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, }); activateSecretsRuntimeSnapshot(refreshed); - expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toMatchObject({ + expect( + getActiveSecretsRuntimeSnapshot()?.authStores.find( + (entry) => entry.agentDir === opsAgentDir, + )?.store.profiles["anthropic:ops"], + ).toMatchObject({ type: "api_key", key: "sk-ops-runtime", keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" },