test: trim agent test hot spots

This commit is contained in:
Peter Steinberger
2026-04-17 21:24:45 +01:00
parent ff55cd5c16
commit 8dde0acbae
6 changed files with 73 additions and 146 deletions

View File

@@ -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");

View File

@@ -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> = {},
): 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<string, ProviderConfig>) {
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");
});
});

View File

@@ -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,
});

View File

@@ -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}`);
}

View File

@@ -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<void>) {
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(/<location>[^<]+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(/<location>[^<]+SKILL\.md<\/location>/);
expect(prompt).toContain(path.join(skillDir, "SKILL.md"));
});
});

View File

@@ -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 ~56 tokens per skill path × N skills ≈ 400600 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))