perf(test): optimize heavy suites and stabilize lock timing

This commit is contained in:
Peter Steinberger
2026-02-13 13:28:23 +00:00
parent 8307f9738b
commit 8899f9e94a
14 changed files with 476 additions and 702 deletions
+123 -269
View File
@@ -1,7 +1,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
import { loadConfig } from "./config.js";
import { withTempHome } from "./test-helpers.js";
describe("config identity defaults", () => {
@@ -15,139 +16,77 @@ describe("config identity defaults", () => {
process.env.HOME = previousHome;
});
it("does not derive mentionPatterns when identity is set", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
},
},
],
},
messages: {},
},
null,
2,
),
"utf-8",
);
const writeAndLoadConfig = async (home: string, config: Record<string, unknown>) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(config, null, 2),
"utf-8",
);
return loadConfig();
};
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
it("does not derive mention defaults and only sets ackReactionScope when identity is present", async () => {
await withTempHome(async (home) => {
const cfg = await writeAndLoadConfig(home, {
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
},
},
],
},
messages: {},
});
expect(cfg.messages?.responsePrefix).toBeUndefined();
expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined();
});
});
it("defaults ackReactionScope without setting ackReaction", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
},
},
],
},
messages: {},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
expect(cfg.messages?.ackReaction).toBeUndefined();
expect(cfg.messages?.ackReactionScope).toBe("group-mentions");
});
});
it("keeps ackReaction unset when identity is missing", async () => {
it("keeps ackReaction unset and does not synthesize agent/session defaults when identity is missing", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
messages: {},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
const cfg = await writeAndLoadConfig(home, { messages: {} });
expect(cfg.messages?.ackReaction).toBeUndefined();
expect(cfg.messages?.ackReactionScope).toBe("group-mentions");
expect(cfg.messages?.responsePrefix).toBeUndefined();
expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined();
expect(cfg.agents?.list).toBeUndefined();
expect(cfg.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT);
expect(cfg.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT);
expect(cfg.session).toBeUndefined();
});
});
it("does not override explicit values", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha Sloth",
theme: "space lobster",
emoji: "🦞",
},
groupChat: { mentionPatterns: ["@openclaw"] },
},
],
const cfg = await writeAndLoadConfig(home, {
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha Sloth",
theme: "space lobster",
emoji: "🦞",
},
groupChat: { mentionPatterns: ["@openclaw"] },
},
messages: {
responsePrefix: "✅",
},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
],
},
messages: {
responsePrefix: "✅",
},
});
expect(cfg.messages?.responsePrefix).toBe("✅");
expect(cfg.agents?.list?.[0]?.groupChat?.mentionPatterns).toEqual(["@openclaw"]);
@@ -156,37 +95,23 @@ describe("config identity defaults", () => {
it("supports provider textChunkLimit config", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
messages: {
messagePrefix: "[openclaw]",
responsePrefix: "🦞",
},
channels: {
whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 },
telegram: { enabled: true, textChunkLimit: 3333 },
discord: {
enabled: true,
textChunkLimit: 1999,
maxLinesPerMessage: 17,
},
signal: { enabled: true, textChunkLimit: 2222 },
imessage: { enabled: true, textChunkLimit: 1111 },
},
const cfg = await writeAndLoadConfig(home, {
messages: {
messagePrefix: "[openclaw]",
responsePrefix: "🦞",
},
channels: {
whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 },
telegram: { enabled: true, textChunkLimit: 3333 },
discord: {
enabled: true,
textChunkLimit: 1999,
maxLinesPerMessage: 17,
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
signal: { enabled: true, textChunkLimit: 2222 },
imessage: { enabled: true, textChunkLimit: 1111 },
},
});
expect(cfg.channels?.whatsapp?.textChunkLimit).toBe(4444);
expect(cfg.channels?.telegram?.textChunkLimit).toBe(3333);
@@ -202,48 +127,34 @@ describe("config identity defaults", () => {
it("accepts blank model provider apiKey values", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
models: {
mode: "merge",
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
apiKey: "",
api: "anthropic-messages",
models: [
{
id: "MiniMax-M2.1",
name: "MiniMax M2.1",
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 200000,
maxTokens: 8192,
},
],
const cfg = await writeAndLoadConfig(home, {
models: {
mode: "merge",
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
apiKey: "",
api: "anthropic-messages",
models: [
{
id: "MiniMax-M2.1",
name: "MiniMax M2.1",
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 200000,
maxTokens: 8192,
},
},
],
},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
},
});
expect(cfg.models?.providers?.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic");
});
@@ -251,100 +162,43 @@ describe("config identity defaults", () => {
it("respects empty responsePrefix to disable identity defaults", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
},
},
],
const cfg = await writeAndLoadConfig(home, {
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
},
},
messages: { responsePrefix: "" },
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
],
},
messages: { responsePrefix: "" },
});
expect(cfg.messages?.responsePrefix).toBe("");
});
});
it("does not synthesize agent list/session when absent", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
messages: {},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
expect(cfg.messages?.responsePrefix).toBeUndefined();
expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined();
expect(cfg.agents?.list).toBeUndefined();
expect(cfg.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT);
expect(cfg.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT);
expect(cfg.session).toBeUndefined();
});
});
it("does not derive responsePrefix from identity emoji", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify(
{
agents: {
list: [
{
id: "main",
identity: {
name: "OpenClaw",
theme: "space lobster",
emoji: "🦞",
},
},
],
const cfg = await writeAndLoadConfig(home, {
agents: {
list: [
{
id: "main",
identity: {
name: "OpenClaw",
theme: "space lobster",
emoji: "🦞",
},
},
messages: {},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
],
},
messages: {},
});
expect(cfg.messages?.responsePrefix).toBeUndefined();
});
+14 -29
View File
@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { validateConfigObjectWithPlugins } from "./config.js";
import { withTempHome } from "./test-helpers.js";
async function writePluginFixture(params: {
@@ -30,13 +31,15 @@ async function writePluginFixture(params: {
}
describe("config plugin validation", () => {
const validateInHome = (home: string, raw: unknown) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
return validateConfigObjectWithPlugins(raw);
};
it("rejects missing plugin load paths", async () => {
await withTempHome(async (home) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
vi.resetModules();
const { validateConfigObjectWithPlugins } = await import("./config.js");
const missingPath = path.join(home, "missing-plugin");
const res = validateConfigObjectWithPlugins({
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [missingPath] } },
});
@@ -53,10 +56,7 @@ describe("config plugin validation", () => {
it("rejects missing plugin ids in entries", async () => {
await withTempHome(async (home) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
vi.resetModules();
const { validateConfigObjectWithPlugins } = await import("./config.js");
const res = validateConfigObjectWithPlugins({
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } },
});
@@ -72,10 +72,7 @@ describe("config plugin validation", () => {
it("rejects missing plugin ids in allow/deny/slots", async () => {
await withTempHome(async (home) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
vi.resetModules();
const { validateConfigObjectWithPlugins } = await import("./config.js");
const res = validateConfigObjectWithPlugins({
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: false,
@@ -99,7 +96,6 @@ describe("config plugin validation", () => {
it("surfaces plugin config diagnostics", async () => {
await withTempHome(async (home) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
const pluginDir = path.join(home, "bad-plugin");
await writePluginFixture({
dir: pluginDir,
@@ -114,9 +110,7 @@ describe("config plugin validation", () => {
},
});
vi.resetModules();
const { validateConfigObjectWithPlugins } = await import("./config.js");
const res = validateConfigObjectWithPlugins({
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: true,
@@ -138,10 +132,7 @@ describe("config plugin validation", () => {
it("accepts known plugin ids", async () => {
await withTempHome(async (home) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
vi.resetModules();
const { validateConfigObjectWithPlugins } = await import("./config.js");
const res = validateConfigObjectWithPlugins({
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, entries: { discord: { enabled: true } } },
});
@@ -151,7 +142,6 @@ describe("config plugin validation", () => {
it("accepts plugin heartbeat targets", async () => {
await withTempHome(async (home) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
const pluginDir = path.join(home, "bluebubbles-plugin");
await writePluginFixture({
dir: pluginDir,
@@ -160,9 +150,7 @@ describe("config plugin validation", () => {
schema: { type: "object" },
});
vi.resetModules();
const { validateConfigObjectWithPlugins } = await import("./config.js");
const res = validateConfigObjectWithPlugins({
const res = validateInHome(home, {
agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [pluginDir] } },
});
@@ -172,10 +160,7 @@ describe("config plugin validation", () => {
it("rejects unknown heartbeat targets", async () => {
await withTempHome(async (home) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
vi.resetModules();
const { validateConfigObjectWithPlugins } = await import("./config.js");
const res = validateConfigObjectWithPlugins({
const res = validateInHome(home, {
agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
});
expect(res.ok).toBe(false);
+33 -13
View File
@@ -52,7 +52,7 @@ describe("session store lock (Promise chain mutex)", () => {
const entry = store[key] as Record<string, unknown>;
// Simulate async work so that without proper serialization
// multiple readers would see the same stale value.
await sleep(Math.random() * 20);
await sleep(Math.random() * 3);
entry.counter = (entry.counter as number) + 1;
entry.tag = `writer-${i}`;
}),
@@ -74,7 +74,7 @@ describe("session store lock (Promise chain mutex)", () => {
storePath,
sessionKey: key,
update: async () => {
await sleep(30);
await sleep(9);
return { modelOverride: "model-a" };
},
}),
@@ -82,7 +82,7 @@ describe("session store lock (Promise chain mutex)", () => {
storePath,
sessionKey: key,
update: async () => {
await sleep(10);
await sleep(3);
return { thinkingLevel: "high" as const };
},
}),
@@ -90,7 +90,7 @@ describe("session store lock (Promise chain mutex)", () => {
storePath,
sessionKey: key,
update: async () => {
await sleep(20);
await sleep(6);
return { systemPromptOverride: "custom" };
},
}),
@@ -168,22 +168,32 @@ describe("session store lock (Promise chain mutex)", () => {
const opA = updateSessionStore(pathA, async (store) => {
order.push("a-start");
await sleep(50);
await sleep(12);
store.a = { ...store.a, modelOverride: "done-a" } as unknown as SessionEntry;
order.push("a-end");
});
const opB = updateSessionStore(pathB, async (store) => {
order.push("b-start");
await sleep(10);
await sleep(3);
store.b = { ...store.b, modelOverride: "done-b" } as unknown as SessionEntry;
order.push("b-end");
});
await Promise.all([opA, opB]);
// B should finish before A because they run in parallel and B sleeps less.
expect(order.indexOf("b-end")).toBeLessThan(order.indexOf("a-end"));
// Parallel behavior: both ops start before either one finishes.
const aStart = order.indexOf("a-start");
const bStart = order.indexOf("b-start");
const aEnd = order.indexOf("a-end");
const bEnd = order.indexOf("b-end");
const firstEnd = Math.min(aEnd, bEnd);
expect(aStart).toBeGreaterThanOrEqual(0);
expect(bStart).toBeGreaterThanOrEqual(0);
expect(aEnd).toBeGreaterThanOrEqual(0);
expect(bEnd).toBeGreaterThanOrEqual(0);
expect(aStart).toBeLessThan(firstEnd);
expect(bStart).toBeLessThan(firstEnd);
expect(loadSessionStore(pathA).a?.modelOverride).toBe("done-a");
expect(loadSessionStore(pathB).b?.modelOverride).toBe("done-b");
@@ -256,7 +266,7 @@ describe("session store lock (Promise chain mutex)", () => {
const lockHolder = withSessionStoreLockForTest(
storePath,
async () => {
await sleep(80);
await sleep(40);
},
{ timeoutMs: 2_000 },
);
@@ -270,7 +280,7 @@ describe("session store lock (Promise chain mutex)", () => {
await expect(timedOut).rejects.toThrow("timeout waiting for session store lock");
await lockHolder;
await sleep(30);
await sleep(8);
expect(timedOutRan).toBe(false);
});
@@ -281,12 +291,22 @@ describe("session store lock (Promise chain mutex)", () => {
});
const write = updateSessionStore(storePath, async (store) => {
await sleep(60);
await sleep(18);
store[key] = { ...store[key], modelOverride: "v" } as unknown as SessionEntry;
});
await sleep(10);
await expect(fs.access(`${storePath}.lock`)).resolves.toBeUndefined();
const lockPath = `${storePath}.lock`;
let lockSeen = false;
for (let i = 0; i < 20; i += 1) {
try {
await fs.access(lockPath);
lockSeen = true;
break;
} catch {
await sleep(2);
}
}
expect(lockSeen).toBe(true);
await write;
const files = await fs.readdir(dir);