mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
test: trim agent test hot spots
This commit is contained in:
@@ -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");
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user