From ffa2a47c584c372ec5822f5afb42508aaba9784d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 27 Mar 2026 04:53:55 +0000 Subject: [PATCH] test: stabilize slow contract and integration suites --- src/cli/config-cli.integration.test.ts | 2 + src/config/config-misc.test.ts | 2 +- .../isolated-agent/run.owner-auth.test.ts | 22 ++- .../contracts/catalog.contract.test.ts | 4 +- .../contracts/registry.contract.test.ts | 14 +- .../contracts/runtime.contract.test.ts | 27 +--- src/plugins/web-search-providers.test.ts | 70 +++++---- src/secrets/runtime.integration.test.ts | 139 +++++++++--------- 8 files changed, 149 insertions(+), 131 deletions(-) diff --git a/src/cli/config-cli.integration.test.ts b/src/cli/config-cli.integration.test.ts index be3aab85f6c..7462459ddfe 100644 --- a/src/cli/config-cli.integration.test.ts +++ b/src/cli/config-cli.integration.test.ts @@ -43,6 +43,8 @@ function createExecDryRunBatch(params: { markerPath: string }) { command: process.execPath, args: ["-e", script], allowInsecurePath: true, + timeoutMs: 15_000, + noOutputTimeoutMs: 15_000, }, }, { diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index f56ef4c8c5d..e151a4d240c 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -473,7 +473,7 @@ describe("config strict validation", () => { } }); - it("flags legacy config entries without auto-migrating", async () => { + it("rejects removed legacy config entries without auto-migrating", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { memorySearch: { provider: "local", fallback: "none" }, diff --git a/src/cron/isolated-agent/run.owner-auth.test.ts b/src/cron/isolated-agent/run.owner-auth.test.ts index 01aa0b3d62f..ef1c9cc8977 100644 --- a/src/cron/isolated-agent/run.owner-auth.test.ts +++ b/src/cron/isolated-agent/run.owner-auth.test.ts @@ -8,6 +8,8 @@ import { runWithModelFallbackMock, } from "./run.test-harness.js"; +const RUN_OWNER_AUTH_TIMEOUT_MS = 300_000; + const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); function makeParams() { @@ -55,11 +57,19 @@ describe("runCronIsolatedAgentTurn owner auth", () => { vi.stubEnv("OPENCLAW_TEST_FAST", previousFastTestEnv); }); - it("passes senderIsOwner=true to isolated cron agent runs", async () => { - await runCronIsolatedAgentTurn(makeParams()); + it( + "passes senderIsOwner=true to isolated cron agent runs", + { timeout: RUN_OWNER_AUTH_TIMEOUT_MS }, + async () => { + await runCronIsolatedAgentTurn(makeParams()); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const senderIsOwner = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.senderIsOwner; - expect(senderIsOwner).toBe(true); - }); + expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + const senderIsOwner = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.senderIsOwner; + expect(senderIsOwner).toBe(true); + + const toolNames = createOpenClawCodingTools({ senderIsOwner }).map((tool) => tool.name); + expect(toolNames).toContain("cron"); + expect(toolNames).toContain("gateway"); + }, + ); }); diff --git a/src/plugins/contracts/catalog.contract.test.ts b/src/plugins/contracts/catalog.contract.test.ts index fd2025447e7..292fcaccb49 100644 --- a/src/plugins/contracts/catalog.contract.test.ts +++ b/src/plugins/contracts/catalog.contract.test.ts @@ -10,6 +10,8 @@ import { } from "../provider-runtime.test-support.js"; import type { ProviderPlugin } from "../types.js"; +const PROVIDER_CATALOG_CONTRACT_TIMEOUT_MS = 300_000; + type ResolvePluginProviders = typeof import("../providers.runtime.js").resolvePluginProviders; type ResolveOwningPluginIdsForProvider = typeof import("../providers.js").resolveOwningPluginIdsForProvider; @@ -41,7 +43,7 @@ let resolveProviderBuiltInModelSuppression: typeof import("../provider-runtime.j let openaiProviders: ProviderPlugin[]; let openaiProvider: ProviderPlugin; -describe("provider catalog contract", () => { +describe("provider catalog contract", { timeout: PROVIDER_CATALOG_CONTRACT_TIMEOUT_MS }, () => { beforeAll(async () => { const openaiPlugin = await import("../../../extensions/openai/index.ts"); openaiProviders = registerProviderPlugin({ diff --git a/src/plugins/contracts/registry.contract.test.ts b/src/plugins/contracts/registry.contract.test.ts index ebffa29bc81..f39d05e4a82 100644 --- a/src/plugins/contracts/registry.contract.test.ts +++ b/src/plugins/contracts/registry.contract.test.ts @@ -10,6 +10,8 @@ import { speechProviderContractRegistry, } from "./registry.js"; +const REGISTRY_CONTRACT_TIMEOUT_MS = 300_000; + function findProviderIdsForPlugin(pluginId: string) { return ( pluginRegistrationContractRegistry.find((entry) => entry.pluginId === pluginId)?.providerIds ?? @@ -113,10 +115,14 @@ describe("plugin contract registry", () => { expect(ids).toEqual([...new Set(ids)]); }); - it("does not duplicate bundled speech provider ids", () => { - const ids = speechProviderContractRegistry.map((entry) => entry.provider.id); - expect(ids).toEqual([...new Set(ids)]); - }); + it( + "does not duplicate bundled speech provider ids", + { timeout: REGISTRY_CONTRACT_TIMEOUT_MS }, + () => { + const ids = speechProviderContractRegistry.map((entry) => entry.provider.id); + expect(ids).toEqual([...new Set(ids)]); + }, + ); it("does not duplicate bundled media provider ids", () => { const ids = mediaUnderstandingProviderContractRegistry.map((entry) => entry.provider.id); diff --git a/src/plugins/contracts/runtime.contract.test.ts b/src/plugins/contracts/runtime.contract.test.ts index 6ca7e733fd1..2bb275c01ba 100644 --- a/src/plugins/contracts/runtime.contract.test.ts +++ b/src/plugins/contracts/runtime.contract.test.ts @@ -31,6 +31,7 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { }; }); +<<<<<<< HEAD vi.mock("../../../extensions/openai/openai-codex-provider.runtime.js", () => ({ refreshOpenAICodexToken: refreshOpenAICodexTokenMock, })); @@ -118,7 +119,7 @@ function requireProviderContractProvider(providerId: string): ProviderPlugin { return provider; } -describe("provider runtime contract", () => { +describe("provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { beforeAll(async () => { providerRuntimeContractProviders.clear(); const registeredFixtures = await Promise.all( @@ -143,11 +144,9 @@ describe("provider runtime contract", () => { } } }, CONTRACT_SETUP_TIMEOUT_MS); - beforeEach(() => { refreshOpenAICodexTokenMock.mockReset(); getOAuthProvidersMock.mockClear(); - refreshOpenAICodexTokenMock.mockReset(); }, CONTRACT_SETUP_TIMEOUT_MS); describe("anthropic", () => { @@ -618,6 +617,7 @@ describe("provider runtime contract", () => { describe("openai-codex", () => { it( "owns refresh fallback for accountId extraction failures", + { timeout: CONTRACT_SETUP_TIMEOUT_MS }, async () => { const provider = requireProviderContractProvider("openai-codex"); const credential = { @@ -628,27 +628,12 @@ describe("provider runtime contract", () => { expires: Date.now() - 60_000, }; - const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString( - "base64url", + refreshOpenAICodexTokenMock.mockRejectedValueOnce( + new Error("Failed to extract accountId from token"), ); - const payload = Buffer.from(JSON.stringify({})).toString("base64url"); - const accessTokenWithoutAccountId = `${header}.${payload}.sig`; - const originalFetch = globalThis.fetch; - globalThis.fetch = vi.fn(async () => - makeResponse(200, { - access_token: accessTokenWithoutAccountId, - refresh_token: "refreshed-refresh-token", - expires_in: 3600, - }), - ) as typeof fetch; - try { - await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(credential); - } finally { - globalThis.fetch = originalFetch; - } + await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(credential); }, - CONTRACT_SETUP_TIMEOUT_MS, ); it("owns forward-compat codex models", () => { diff --git a/src/plugins/web-search-providers.test.ts b/src/plugins/web-search-providers.test.ts index ae5bf04846e..38094211652 100644 --- a/src/plugins/web-search-providers.test.ts +++ b/src/plugins/web-search-providers.test.ts @@ -1,39 +1,45 @@ import { describe, expect, it } from "vitest"; import { resolveBundledPluginWebSearchProviders } from "./web-search-providers.js"; -describe("resolveBundledPluginWebSearchProviders", () => { - it("returns bundled providers in alphabetical order", () => { - const providers = resolveBundledPluginWebSearchProviders({}); +const WEB_SEARCH_PROVIDER_TEST_TIMEOUT_MS = 300_000; - expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ - "brave:brave", - "duckduckgo:duckduckgo", - "exa:exa", - "firecrawl:firecrawl", - "google:gemini", - "xai:grok", - "moonshot:kimi", - "perplexity:perplexity", - "tavily:tavily", - ]); - expect(providers.map((provider) => provider.credentialPath)).toEqual([ - "plugins.entries.brave.config.webSearch.apiKey", - "", - "plugins.entries.exa.config.webSearch.apiKey", - "plugins.entries.firecrawl.config.webSearch.apiKey", - "plugins.entries.google.config.webSearch.apiKey", - "plugins.entries.xai.config.webSearch.apiKey", - "plugins.entries.moonshot.config.webSearch.apiKey", - "plugins.entries.perplexity.config.webSearch.apiKey", - "plugins.entries.tavily.config.webSearch.apiKey", - ]); - expect(providers.find((provider) => provider.id === "firecrawl")?.applySelectionConfig).toEqual( - expect.any(Function), - ); - expect( - providers.find((provider) => provider.id === "perplexity")?.resolveRuntimeMetadata, - ).toEqual(expect.any(Function)); - }); +describe("resolveBundledPluginWebSearchProviders", () => { + it( + "returns bundled providers in alphabetical order", + { timeout: WEB_SEARCH_PROVIDER_TEST_TIMEOUT_MS }, + () => { + const providers = resolveBundledPluginWebSearchProviders({}); + + expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ + "brave:brave", + "duckduckgo:duckduckgo", + "exa:exa", + "firecrawl:firecrawl", + "google:gemini", + "xai:grok", + "moonshot:kimi", + "perplexity:perplexity", + "tavily:tavily", + ]); + expect(providers.map((provider) => provider.credentialPath)).toEqual([ + "plugins.entries.brave.config.webSearch.apiKey", + "", + "plugins.entries.exa.config.webSearch.apiKey", + "plugins.entries.firecrawl.config.webSearch.apiKey", + "plugins.entries.google.config.webSearch.apiKey", + "plugins.entries.xai.config.webSearch.apiKey", + "plugins.entries.moonshot.config.webSearch.apiKey", + "plugins.entries.perplexity.config.webSearch.apiKey", + "plugins.entries.tavily.config.webSearch.apiKey", + ]); + expect( + providers.find((provider) => provider.id === "firecrawl")?.applySelectionConfig, + ).toEqual(expect.any(Function)); + expect( + providers.find((provider) => provider.id === "perplexity")?.resolveRuntimeMetadata, + ).toEqual(expect.any(Function)); + }, + ); it("can augment restrictive allowlists for bundled compatibility", () => { const providers = resolveBundledPluginWebSearchProviders({ diff --git a/src/secrets/runtime.integration.test.ts b/src/secrets/runtime.integration.test.ts index eae46e60408..acead31cdf8 100644 --- a/src/secrets/runtime.integration.test.ts +++ b/src/secrets/runtime.integration.test.ts @@ -27,6 +27,7 @@ const OPENAI_FILE_KEY_REF = { provider: "default", id: "/providers/openai/apiKey", } as const; +const SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS = 300_000; const allowInsecureTempSecretFile = process.platform === "win32"; function asConfig(value: unknown): OpenClawConfig { @@ -252,38 +253,55 @@ describe("secrets runtime snapshot integration", () => { }); }); - it("keeps last-known-good web runtime snapshot when reload introduces unresolved active web refs", async () => { - await withTempHome("openclaw-secrets-runtime-web-reload-lkg-", async (home) => { - const prepared = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - web: { - search: { - provider: "gemini", - gemini: { - apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" }, + it( + "keeps last-known-good web runtime snapshot when reload introduces unresolved active web refs", + async () => { + await withTempHome("openclaw-secrets-runtime-web-reload-lkg-", async (home) => { + const prepared = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + web: { + search: { + provider: "gemini", + gemini: { + apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" }, + }, }, }, }, + }), + env: { + WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-runtime-key", }, - }), - env: { - WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-runtime-key", - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); - activateSecretsRuntimeSnapshot(prepared); + activateSecretsRuntimeSnapshot(prepared); - await expect( - writeConfigFile({ - ...loadConfig(), - plugins: { - entries: { - google: { - config: { - webSearch: { + await expect( + writeConfigFile({ + ...loadConfig(), + plugins: { + entries: { + google: { + config: { + webSearch: { + apiKey: { + source: "env", + provider: "default", + id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", + }, + }, + }, + }, + }, + }, + tools: { + web: { + search: { + provider: "gemini", + gemini: { apiKey: { source: "env", provider: "default", @@ -293,49 +311,38 @@ describe("secrets runtime snapshot integration", () => { }, }, }, - }, - tools: { - web: { - search: { - provider: "gemini", - gemini: { - apiKey: { - source: "env", - provider: "default", - id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", - }, - }, - }, - }, - }, - }), - ).rejects.toThrow( - /runtime snapshot refresh failed: .*WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK/i, - ); + }), + ).rejects.toThrow( + /runtime snapshot refresh failed: .*WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK/i, + ); - const activeAfterFailure = getActiveSecretsRuntimeSnapshot(); - expect(activeAfterFailure).not.toBeNull(); - expect(loadConfig().tools?.web?.search?.gemini?.apiKey).toBe("web-search-gemini-runtime-key"); - expect(activeAfterFailure?.sourceConfig.tools?.web?.search?.gemini?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "WEB_SEARCH_GEMINI_API_KEY", - }); - expect(getActiveRuntimeWebToolsMetadata()?.search.selectedProvider).toBe("gemini"); + const activeAfterFailure = getActiveSecretsRuntimeSnapshot(); + expect(activeAfterFailure).not.toBeNull(); + expect(loadConfig().tools?.web?.search?.gemini?.apiKey).toBe( + "web-search-gemini-runtime-key", + ); + expect(activeAfterFailure?.sourceConfig.tools?.web?.search?.gemini?.apiKey).toEqual({ + source: "env", + provider: "default", + id: "WEB_SEARCH_GEMINI_API_KEY", + }); + expect(getActiveRuntimeWebToolsMetadata()?.search.selectedProvider).toBe("gemini"); - const persistedConfig = JSON.parse( - await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"), - ) as OpenClawConfig; - const persistedGoogleWebSearchConfig = persistedConfig.plugins?.entries?.google?.config as - | { webSearch?: { apiKey?: unknown } } - | undefined; - expect(persistedGoogleWebSearchConfig?.webSearch?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", + const persistedConfig = JSON.parse( + await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"), + ) as OpenClawConfig; + const persistedGoogleWebSearchConfig = persistedConfig.plugins?.entries?.google?.config as + | { webSearch?: { apiKey?: unknown } } + | undefined; + expect(persistedGoogleWebSearchConfig?.webSearch?.apiKey).toEqual({ + source: "env", + provider: "default", + id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", + }); }); - }); - }, 180_000); + }, + SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS, + ); it("recomputes config-derived agent dirs when refreshing active secrets runtime snapshots", async () => { await withTempHome("openclaw-secrets-runtime-agent-dirs-", async (home) => {