From 8dde0acbaed960dd296f754ed1eb40e1d6b745f7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 21:24:45 +0100 Subject: [PATCH] test: trim agent test hot spots --- ...est.ts => oauth.concurrent-agents.test.ts} | 15 +-- ...ini-3-ids-preview-google-providers.test.ts | 83 -------------- src/agents/pi-bundle-mcp-runtime.test.ts | 2 +- src/agents/pi-bundle-mcp-test-harness.ts | 2 +- src/agents/skills.compact-skill-paths.test.ts | 108 +++++++++--------- src/agents/skills/workspace.ts | 9 +- 6 files changed, 73 insertions(+), 146 deletions(-) rename src/agents/auth-profiles/{oauth.concurrent-20-agents.test.ts => oauth.concurrent-agents.test.ts} (92%) delete mode 100644 src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts diff --git a/src/agents/auth-profiles/oauth.concurrent-20-agents.test.ts b/src/agents/auth-profiles/oauth.concurrent-agents.test.ts similarity index 92% rename from src/agents/auth-profiles/oauth.concurrent-20-agents.test.ts rename to src/agents/auth-profiles/oauth.concurrent-agents.test.ts index 5045ea97073..22b49dca6a8 100644 --- a/src/agents/auth-profiles/oauth.concurrent-20-agents.test.ts +++ b/src/agents/auth-profiles/oauth.concurrent-agents.test.ts @@ -133,16 +133,17 @@ describe("resolveApiKeyForProfile cross-agent refresh coordination (#26322)", () } }); - it("refreshes exactly once when 20 agents share one OAuth profile and all race on expiry", async () => { + it("refreshes exactly once when agents share one OAuth profile and race on expiry", async () => { + const agentCount = 6; const profileId = "openai-codex:default"; const provider = "openai-codex"; const accountId = "acct-shared"; const freshExpiry = Date.now() + 60 * 60 * 1000; - // Seed 20 sub-agents + main with the SAME stale OAuth credential. Main is + // Seed sub-agents + main with the SAME stale OAuth credential. Main is // also expired so it cannot short-circuit via adoptNewerMainOAuthCredential. const subAgents = await Promise.all( - Array.from({ length: 20 }, async (_, i) => { + Array.from({ length: agentCount }, async (_, i) => { const dir = path.join(tempRoot, "agents", `sub-${i}`, "agent"); await fs.mkdir(dir, { recursive: true }); saveAuthProfileStore(createExpiredOauthStore({ profileId, provider, accountId }), dir); @@ -166,10 +167,10 @@ describe("resolveApiKeyForProfile cross-agent refresh coordination (#26322)", () } as never; }); - // Fire all 20 agents concurrently. With the old per-agentDir lock this - // would produce ~20 concurrent refresh calls and 19 refresh_token_reused + // Fire all agents concurrently. With the old per-agentDir lock this + // would produce N concurrent refresh calls and N-1 refresh_token_reused // 401s. With the new global per-profile lock, only the first refresh is - // performed; the remaining 19 adopt the resulting fresh credentials. + // performed; the remaining agents adopt the resulting fresh credentials. const results = await Promise.all( subAgents.map((agentDir) => resolveApiKeyForProfileInTest({ @@ -181,7 +182,7 @@ describe("resolveApiKeyForProfile cross-agent refresh coordination (#26322)", () ); expect(callCount).toBe(1); - expect(results).toHaveLength(20); + expect(results).toHaveLength(agentCount); for (const result of results) { expect(result).not.toBeNull(); expect(result?.apiKey).toBe("cross-agent-refreshed-access"); diff --git a/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts b/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts deleted file mode 100644 index 50cc8e2b1a4..00000000000 --- a/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, expect, it } from "vitest"; -import type { ModelDefinitionConfig } from "../config/types.models.js"; -import { normalizeProviders } from "./models-config.providers.normalize.js"; -import type { ProviderConfig } from "./models-config.providers.secrets.js"; - -function createGoogleModel(id: string): ModelDefinitionConfig { - return { - id, - name: id, - api: "google-generative-ai", - reasoning: id.includes("pro"), - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_048_576, - maxTokens: 65_536, - }; -} - -function buildGoogleProvider( - modelIds: string[], - overrides: Partial = {}, -): ProviderConfig { - return { - baseUrl: "https://generativelanguage.googleapis.com", - apiKey: "GEMINI_KEY", // pragma: allowlist secret - api: "google-generative-ai", - models: modelIds.map((id) => createGoogleModel(id)), - ...overrides, - } satisfies ProviderConfig; -} - -function normalizeForTest(providers: Record) { - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-models-normalize-")); - return normalizeProviders({ providers, agentDir }) ?? {}; -} - -function normalizedModelIds(provider: ProviderConfig | undefined): string[] { - return provider?.models?.map((model) => model.id) ?? []; -} - -describe("models-config", () => { - it("normalizes gemini 3 ids to preview for google providers", () => { - const normalized = normalizeForTest({ - google: buildGoogleProvider(["gemini-3-pro", "gemini-3-flash"]), - }); - - expect(normalizedModelIds(normalized.google)).toEqual([ - "gemini-3-pro-preview", - "gemini-3-flash-preview", - ]); - }); - - it("normalizes the deprecated google flash preview id to the working preview id", () => { - const normalized = normalizeForTest({ - google: buildGoogleProvider(["gemini-3.1-flash-preview"]), - }); - - expect(normalizedModelIds(normalized.google)).toEqual(["gemini-3-flash-preview"]); - }); - - it("normalizes custom Google Generative AI providers by api instead of provider name", () => { - const normalized = normalizeForTest({ - "google-paid": buildGoogleProvider(["gemini-3-pro"]), - }); - - expect(normalizedModelIds(normalized["google-paid"])).toEqual(["gemini-3-pro-preview"]); - expect(normalized["google-paid"]?.baseUrl).toBe( - "https://generativelanguage.googleapis.com/v1beta", - ); - }); - - it("keeps built-in google normalization when api is only defined on models", () => { - const normalized = normalizeForTest({ - google: buildGoogleProvider(["gemini-3-flash"], { api: undefined }), - }); - - expect(normalizedModelIds(normalized.google)).toEqual(["gemini-3-flash-preview"]); - expect(normalized.google?.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta"); - }); -}); diff --git a/src/agents/pi-bundle-mcp-runtime.test.ts b/src/agents/pi-bundle-mcp-runtime.test.ts index 2645cd667df..bb2ceece485 100644 --- a/src/agents/pi-bundle-mcp-runtime.test.ts +++ b/src/agents/pi-bundle-mcp-runtime.test.ts @@ -266,7 +266,7 @@ describe("session MCP runtime", () => { const serverScriptPath = path.join(pluginRoot, "servers", "bundle-probe.mjs"); await writeBundleProbeMcpServer(serverScriptPath, { startupCounterPath, - startupDelayMs: 100, + startupDelayMs: 10, pidPath, exitMarkerPath, }); diff --git a/src/agents/pi-bundle-mcp-test-harness.ts b/src/agents/pi-bundle-mcp-test-harness.ts index 2d4ad3a7d97..d3d3bc2b636 100644 --- a/src/agents/pi-bundle-mcp-test-harness.ts +++ b/src/agents/pi-bundle-mcp-test-harness.ts @@ -38,7 +38,7 @@ export async function waitForFileText(filePath: string, timeoutMs = 5_000): Prom if (content != null) { return content; } - await new Promise((resolve) => setTimeout(resolve, 25)); + await new Promise((resolve) => setTimeout(resolve, 5)); } throw new Error(`Timed out waiting for ${filePath}`); } diff --git a/src/agents/skills.compact-skill-paths.test.ts b/src/agents/skills.compact-skill-paths.test.ts index bd0a2fabb9e..764af374594 100644 --- a/src/agents/skills.compact-skill-paths.test.ts +++ b/src/agents/skills.compact-skill-paths.test.ts @@ -1,67 +1,69 @@ -import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { buildWorkspaceSkillsPrompt } from "./skills.js"; -import { writeSkill } from "./skills.test-helpers.js"; - -async function withTempWorkspace(run: (workspaceDir: string) => Promise) { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-compact-")); - try { - await run(workspaceDir); - } finally { - await fs.rm(workspaceDir, { recursive: true, force: true }); - } -} +import { createCanonicalFixtureSkill } from "./skills.test-helpers.js"; describe("compactSkillPaths", () => { - it("replaces home directory prefix with ~ in skill locations", async () => { - await withTempWorkspace(async (workspaceDir) => { - const skillDir = path.join(workspaceDir, "skills", "test-skill"); + it("replaces home directory prefix with ~ in skill locations", () => { + const home = os.homedir(); + const skillDir = path.join(home, ".openclaw-test-skills", "test-skill"); - await writeSkill({ - dir: skillDir, - name: "test-skill", - description: "A test skill for path compaction", - }); - - const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { - bundledSkillsDir: path.join(workspaceDir, ".bundled-empty"), - managedSkillsDir: path.join(workspaceDir, ".managed-empty"), - }); - - const home = os.homedir(); - // The prompt should NOT contain the absolute home directory path - // when the skill is under the home directory (which tmpdir usually is on macOS) - if (workspaceDir.startsWith(home)) { - expect(prompt).not.toContain(home + path.sep); - expect(prompt).toContain("~/"); - } - - // The skill name and description should still be present - expect(prompt).toContain("test-skill"); - expect(prompt).toContain("A test skill for path compaction"); + const prompt = buildWorkspaceSkillsPrompt(home, { + entries: [ + { + skill: createCanonicalFixtureSkill({ + name: "test-skill", + description: "A test skill for path compaction", + filePath: path.join(skillDir, "SKILL.md"), + baseDir: skillDir, + source: "test", + }), + frontmatter: {}, + metadata: undefined, + invocation: { disableModelInvocation: false, userInvocable: true }, + exposure: { + includeInRuntimeRegistry: true, + includeInAvailableSkillsPrompt: true, + userInvocable: true, + }, + }, + ], }); + + expect(prompt).not.toContain(home + path.sep); + expect(prompt).toContain("~/"); + expect(prompt).toContain("test-skill"); + expect(prompt).toContain("A test skill for path compaction"); }); - it("preserves paths outside home directory", async () => { - // Skills outside ~ should keep their absolute paths - await withTempWorkspace(async (workspaceDir) => { - const skillDir = path.join(workspaceDir, "skills", "ext-skill"); + it("preserves paths outside home directory", () => { + const outsideHome = path.join(path.parse(os.homedir()).root, "openclaw-external-skills"); + const skillDir = path.join(outsideHome, "skills", "ext-skill"); - await writeSkill({ - dir: skillDir, - name: "ext-skill", - description: "External skill", - }); - - const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { - bundledSkillsDir: path.join(workspaceDir, ".bundled-empty"), - managedSkillsDir: path.join(workspaceDir, ".managed-empty"), - }); - - // Should still contain a valid location tag - expect(prompt).toMatch(/[^<]+SKILL\.md<\/location>/); + const prompt = buildWorkspaceSkillsPrompt(outsideHome, { + entries: [ + { + skill: createCanonicalFixtureSkill({ + name: "ext-skill", + description: "External skill", + filePath: path.join(skillDir, "SKILL.md"), + baseDir: skillDir, + source: "test", + }), + frontmatter: {}, + metadata: undefined, + invocation: { disableModelInvocation: false, userInvocable: true }, + exposure: { + includeInRuntimeRegistry: true, + includeInAvailableSkillsPrompt: true, + userInvocable: true, + }, + }, + ], }); + + expect(prompt).toMatch(/[^<]+SKILL\.md<\/location>/); + expect(prompt).toContain(path.join(skillDir, "SKILL.md")); }); }); diff --git a/src/agents/skills/workspace.ts b/src/agents/skills/workspace.ts index a90480a144f..cb488daa407 100644 --- a/src/agents/skills/workspace.ts +++ b/src/agents/skills/workspace.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { resolveOsHomeDir } from "../../infra/home-dir.js"; import { isPathInside } from "../../infra/path-guards.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; @@ -40,6 +41,10 @@ const skillsLogger = createSubsystemLogger("skills"); * Saves ~5–6 tokens per skill path × N skills ≈ 400–600 tokens total. */ function resolveUserHomeDir(): string | undefined { + return resolveOsHomeDir(process.env, os.homedir); +} + +function resolveNativeUserHomeDir(): string | undefined { try { return path.resolve(os.homedir()); } catch { @@ -48,7 +53,9 @@ function resolveUserHomeDir(): string | undefined { } function resolveCompactHomePrefixes(): string[] { - const homes = [resolveHomeDir(), resolveUserHomeDir()].filter((home): home is string => !!home); + const homes = [resolveHomeDir(), resolveUserHomeDir(), resolveNativeUserHomeDir()].filter( + (home): home is string => !!home, + ); const resolvedHomes = homes.map((home) => path.resolve(home)); const realHomes = resolvedHomes .map((home) => tryRealpath(home))