mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
test(perf): dedupe fixtures and reduce flaky waits
This commit is contained in:
@@ -2,16 +2,15 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||
import { installModelsConfigTestHooks, withModelsTempHome } from "./models-config.e2e-harness.js";
|
||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||
|
||||
describe("models-config", () => {
|
||||
installModelsConfigTestHooks();
|
||||
|
||||
it("normalizes gemini 3 ids to preview for google providers", async () => {
|
||||
await withModelsTempHome(async () => {
|
||||
const { ensureOpenClawModelsJson } = await import("./models-config.js");
|
||||
const { resolveOpenClawAgentDir } = await import("./agent-paths.js");
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
|
||||
@@ -317,23 +317,12 @@ async function runTurnWithCooldownSeed(params: {
|
||||
|
||||
describe("runEmbeddedPiAgent auth profile rotation", () => {
|
||||
it("rotates for auto-pinned profiles across retryable stream failures", async () => {
|
||||
const cases = [
|
||||
{
|
||||
errorMessage: "rate limit",
|
||||
sessionKey: "agent:test:auto",
|
||||
runId: "run:auto",
|
||||
},
|
||||
{
|
||||
errorMessage: "request ended without sending any chunks",
|
||||
sessionKey: "agent:test:empty-chunk-stream",
|
||||
runId: "run:empty-chunk-stream",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
const { usageStats } = await runAutoPinnedRotationCase(testCase);
|
||||
expect(typeof usageStats["openai:p2"]?.lastUsed).toBe("number");
|
||||
}
|
||||
const { usageStats } = await runAutoPinnedRotationCase({
|
||||
errorMessage: "rate limit",
|
||||
sessionKey: "agent:test:auto",
|
||||
runId: "run:auto",
|
||||
});
|
||||
expect(typeof usageStats["openai:p2"]?.lastUsed).toBe("number");
|
||||
});
|
||||
|
||||
it("rotates on timeout without cooling down the timed-out profile", async () => {
|
||||
|
||||
@@ -233,7 +233,7 @@ const runDefaultEmbeddedTurn = async (sessionFile: string, prompt: string, sessi
|
||||
});
|
||||
};
|
||||
|
||||
describe.concurrent("runEmbeddedPiAgent", () => {
|
||||
describe("runEmbeddedPiAgent", () => {
|
||||
it("handles prompt error paths without dropping user state", async () => {
|
||||
for (const testCase of [
|
||||
{
|
||||
|
||||
@@ -166,10 +166,11 @@ afterAll(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
runCommandWithTimeoutMock.mockClear();
|
||||
scanDirectoryWithSummaryMock.mockClear();
|
||||
fetchWithSsrFGuardMock.mockClear();
|
||||
beforeEach(async () => {
|
||||
runCommandWithTimeoutMock.mockReset();
|
||||
runCommandWithTimeoutMock.mockResolvedValue(runCommandResult());
|
||||
scanDirectoryWithSummaryMock.mockReset();
|
||||
fetchWithSsrFGuardMock.mockReset();
|
||||
scanDirectoryWithSummaryMock.mockResolvedValue({
|
||||
scannedFiles: 0,
|
||||
critical: 0,
|
||||
@@ -242,10 +243,7 @@ describe("installSkill download extraction safety", () => {
|
||||
});
|
||||
|
||||
it("rejects targetDir escapes outside the per-skill tools root", async () => {
|
||||
for (const testCase of [
|
||||
{ name: "targetdir-escape", targetDir: path.join(workspaceDir, "outside") },
|
||||
{ name: "relative-traversal", targetDir: "../outside" },
|
||||
]) {
|
||||
for (const testCase of [{ name: "relative-traversal", targetDir: "../outside" }]) {
|
||||
mockArchiveResponse(new Uint8Array(SAFE_ZIP_BUFFER));
|
||||
await writeDownloadSkill({
|
||||
workspaceDir,
|
||||
@@ -288,16 +286,6 @@ describe("installSkill download extraction safety", () => {
|
||||
describe("installSkill download extraction safety (tar.bz2)", () => {
|
||||
it("handles tar.bz2 extraction safety edge-cases", async () => {
|
||||
for (const testCase of [
|
||||
{
|
||||
label: "rejects traversal before extraction",
|
||||
name: "tbz2-slip",
|
||||
url: "https://example.invalid/evil.tbz2",
|
||||
listOutput: "../outside.txt\n",
|
||||
verboseListOutput: "-rw-r--r-- 0 0 0 0 Jan 1 00:00 ../outside.txt\n",
|
||||
extract: "reject" as const,
|
||||
expectedOk: false,
|
||||
expectedExtract: false,
|
||||
},
|
||||
{
|
||||
label: "rejects archives containing symlinks",
|
||||
name: "tbz2-symlink",
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { writeSkill } from "./skills.e2e-test-helpers.js";
|
||||
import { buildWorkspaceSkillsPrompt } from "./skills.js";
|
||||
|
||||
let fixtureRoot = "";
|
||||
let fixtureCount = 0;
|
||||
|
||||
async function createCaseDir(prefix: string): Promise<string> {
|
||||
const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skills-prompt-suite-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("buildWorkspaceSkillsPrompt", () => {
|
||||
it("prefers workspace skills over managed skills", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const workspaceDir = await createCaseDir("workspace");
|
||||
const managedDir = path.join(workspaceDir, ".managed");
|
||||
const bundledDir = path.join(workspaceDir, ".bundled");
|
||||
const managedSkillDir = path.join(managedDir, "demo-skill");
|
||||
@@ -45,7 +62,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
expect(prompt).not.toContain(path.join(bundledSkillDir, "SKILL.md"));
|
||||
});
|
||||
it("gates by bins, config, and always", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const workspaceDir = await createCaseDir("workspace");
|
||||
const skillsDir = path.join(workspaceDir, "skills");
|
||||
const binDir = path.join(workspaceDir, "bin");
|
||||
|
||||
@@ -80,9 +97,12 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
metadata: '{"openclaw":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}',
|
||||
});
|
||||
|
||||
const defaultPrompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||
});
|
||||
const managedSkillsDir = path.join(workspaceDir, ".managed");
|
||||
const defaultPrompt = withEnv({ PATH: "" }, () =>
|
||||
buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
managedSkillsDir,
|
||||
}),
|
||||
);
|
||||
expect(defaultPrompt).toContain("always-skill");
|
||||
expect(defaultPrompt).toContain("config-skill");
|
||||
expect(defaultPrompt).not.toContain("bin-skill");
|
||||
@@ -94,23 +114,23 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
await fs.writeFile(fakebinPath, "#!/bin/sh\nexit 0\n", "utf-8");
|
||||
await fs.chmod(fakebinPath, 0o755);
|
||||
|
||||
withEnv({ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` }, () => {
|
||||
const gatedPrompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||
const gatedPrompt = withEnv({ PATH: binDir }, () =>
|
||||
buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
managedSkillsDir,
|
||||
config: {
|
||||
browser: { enabled: false },
|
||||
skills: { entries: { "env-skill": { apiKey: "ok" } } },
|
||||
},
|
||||
});
|
||||
expect(gatedPrompt).toContain("bin-skill");
|
||||
expect(gatedPrompt).toContain("anybin-skill");
|
||||
expect(gatedPrompt).toContain("env-skill");
|
||||
expect(gatedPrompt).toContain("always-skill");
|
||||
expect(gatedPrompt).not.toContain("config-skill");
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(gatedPrompt).toContain("bin-skill");
|
||||
expect(gatedPrompt).toContain("anybin-skill");
|
||||
expect(gatedPrompt).toContain("env-skill");
|
||||
expect(gatedPrompt).toContain("always-skill");
|
||||
expect(gatedPrompt).not.toContain("config-skill");
|
||||
});
|
||||
it("uses skillKey for config lookups", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const workspaceDir = await createCaseDir("workspace");
|
||||
const skillDir = path.join(workspaceDir, "skills", "alias-skill");
|
||||
await writeSkill({
|
||||
dir: skillDir,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { writeSkill } from "./skills.e2e-test-helpers.js";
|
||||
import { buildWorkspaceSkillsPrompt, syncSkillsToWorkspace } from "./skills.js";
|
||||
@@ -15,10 +15,27 @@ async function pathExists(filePath: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
let fixtureRoot = "";
|
||||
let fixtureCount = 0;
|
||||
|
||||
async function createCaseDir(prefix: string): Promise<string> {
|
||||
const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skills-sync-suite-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("buildWorkspaceSkillsPrompt", () => {
|
||||
it("syncs merged skills into a target workspace", async () => {
|
||||
const sourceWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const targetWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const sourceWorkspace = await createCaseDir("source");
|
||||
const targetWorkspace = await createCaseDir("target");
|
||||
const extraDir = path.join(sourceWorkspace, ".extra");
|
||||
const bundledDir = path.join(sourceWorkspace, ".bundled");
|
||||
const managedDir = path.join(sourceWorkspace, ".managed");
|
||||
@@ -64,9 +81,9 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
expect(prompt).toContain(path.join(targetWorkspace, "skills", "demo-skill", "SKILL.md"));
|
||||
});
|
||||
it("keeps synced skills confined under target workspace when frontmatter name uses traversal", async () => {
|
||||
const sourceWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const targetWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const escapeId = `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
||||
const sourceWorkspace = await createCaseDir("source");
|
||||
const targetWorkspace = await createCaseDir("target");
|
||||
const escapeId = fixtureCount;
|
||||
const traversalName = `../../../skill-sync-escape-${escapeId}`;
|
||||
const escapedDest = path.resolve(targetWorkspace, "skills", traversalName);
|
||||
|
||||
@@ -94,9 +111,9 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
expect(await pathExists(escapedDest)).toBe(false);
|
||||
});
|
||||
it("keeps synced skills confined under target workspace when frontmatter name is absolute", async () => {
|
||||
const sourceWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const targetWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const escapeId = `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
||||
const sourceWorkspace = await createCaseDir("source");
|
||||
const targetWorkspace = await createCaseDir("target");
|
||||
const escapeId = fixtureCount;
|
||||
const absoluteDest = path.join(os.tmpdir(), `skill-sync-abs-escape-${escapeId}`);
|
||||
|
||||
await fs.rm(absoluteDest, { recursive: true, force: true });
|
||||
@@ -121,7 +138,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
expect(await pathExists(absoluteDest)).toBe(false);
|
||||
});
|
||||
it("filters skills based on env/config gates", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const workspaceDir = await createCaseDir("workspace");
|
||||
const skillDir = path.join(workspaceDir, "skills", "nano-banana-pro");
|
||||
await writeSkill({
|
||||
dir: skillDir,
|
||||
@@ -149,7 +166,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
});
|
||||
});
|
||||
it("applies skill filters, including empty lists", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
|
||||
const workspaceDir = await createCaseDir("workspace");
|
||||
await writeSkill({
|
||||
dir: path.join(workspaceDir, "skills", "alpha"),
|
||||
name: "alpha",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, vi } from "vitest";
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
import { afterAll, afterEach, beforeAll, expect, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
|
||||
@@ -105,17 +105,91 @@ vi.mock("../web/session.js", () => webSessionMocks);
|
||||
|
||||
export const MAIN_SESSION_KEY = "agent:main:main";
|
||||
|
||||
type TempHomeEnvSnapshot = {
|
||||
home: string | undefined;
|
||||
userProfile: string | undefined;
|
||||
homeDrive: string | undefined;
|
||||
homePath: string | undefined;
|
||||
openclawHome: string | undefined;
|
||||
stateDir: string | undefined;
|
||||
};
|
||||
|
||||
let suiteTempHomeRoot = "";
|
||||
let suiteTempHomeId = 0;
|
||||
|
||||
function snapshotTempHomeEnv(): TempHomeEnvSnapshot {
|
||||
return {
|
||||
home: process.env.HOME,
|
||||
userProfile: process.env.USERPROFILE,
|
||||
homeDrive: process.env.HOMEDRIVE,
|
||||
homePath: process.env.HOMEPATH,
|
||||
openclawHome: process.env.OPENCLAW_HOME,
|
||||
stateDir: process.env.OPENCLAW_STATE_DIR,
|
||||
};
|
||||
}
|
||||
|
||||
function restoreTempHomeEnv(snapshot: TempHomeEnvSnapshot): void {
|
||||
const restoreKey = (key: string, value: string | undefined) => {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
return;
|
||||
}
|
||||
process.env[key] = value;
|
||||
};
|
||||
|
||||
restoreKey("HOME", snapshot.home);
|
||||
restoreKey("USERPROFILE", snapshot.userProfile);
|
||||
restoreKey("HOMEDRIVE", snapshot.homeDrive);
|
||||
restoreKey("HOMEPATH", snapshot.homePath);
|
||||
restoreKey("OPENCLAW_HOME", snapshot.openclawHome);
|
||||
restoreKey("OPENCLAW_STATE_DIR", snapshot.stateDir);
|
||||
}
|
||||
|
||||
function setTempHomeEnv(home: string): void {
|
||||
process.env.HOME = home;
|
||||
process.env.USERPROFILE = home;
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
process.env.OPENCLAW_STATE_DIR = join(home, ".openclaw");
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
return;
|
||||
}
|
||||
const match = home.match(/^([A-Za-z]:)(.*)$/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
process.env.HOMEDRIVE = match[1];
|
||||
process.env.HOMEPATH = match[2] || "\\";
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
suiteTempHomeRoot = await fs.mkdtemp(join(os.tmpdir(), "openclaw-triggers-suite-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!suiteTempHomeRoot) {
|
||||
return;
|
||||
}
|
||||
await fs.rm(suiteTempHomeRoot, { recursive: true, force: true }).catch(() => undefined);
|
||||
suiteTempHomeRoot = "";
|
||||
suiteTempHomeId = 0;
|
||||
});
|
||||
|
||||
export async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
return withTempHomeBase(
|
||||
async (home) => {
|
||||
// Avoid cross-test leakage if a test doesn't touch these mocks.
|
||||
piEmbeddedMocks.runEmbeddedPiAgent.mockClear();
|
||||
piEmbeddedMocks.abortEmbeddedPiRun.mockClear();
|
||||
piEmbeddedMocks.compactEmbeddedPiSession.mockClear();
|
||||
return await fn(home);
|
||||
},
|
||||
{ prefix: "openclaw-triggers-" },
|
||||
);
|
||||
const home = join(suiteTempHomeRoot, `case-${++suiteTempHomeId}`);
|
||||
const snapshot = snapshotTempHomeEnv();
|
||||
await fs.mkdir(join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true });
|
||||
setTempHomeEnv(home);
|
||||
|
||||
try {
|
||||
// Avoid cross-test leakage if a test doesn't touch these mocks.
|
||||
piEmbeddedMocks.runEmbeddedPiAgent.mockClear();
|
||||
piEmbeddedMocks.abortEmbeddedPiRun.mockClear();
|
||||
piEmbeddedMocks.compactEmbeddedPiSession.mockClear();
|
||||
return await fn(home);
|
||||
} finally {
|
||||
restoreTempHomeEnv(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
export function makeCfg(home: string): OpenClawConfig {
|
||||
@@ -126,6 +200,8 @@ export function makeCfg(home: string): OpenClawConfig {
|
||||
workspace: join(home, "openclaw"),
|
||||
// Test harness: avoid 1s coalescer idle sleeps that dominate trigger suites.
|
||||
blockStreamingCoalesce: { idleMs: 1 },
|
||||
// Trigger tests assert routing/authorization behavior, not delivery pacing.
|
||||
humanDelay: { mode: "off" },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
|
||||
@@ -73,6 +73,7 @@ const mocks = vi.hoisted(() => {
|
||||
models: { providers: {} },
|
||||
env: { shellEnv: { enabled: true } },
|
||||
}),
|
||||
loadProviderUsageSummary: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -116,6 +117,14 @@ vi.mock("../../config/config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../infra/provider-usage.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../infra/provider-usage.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadProviderUsageSummary: mocks.loadProviderUsageSummary,
|
||||
};
|
||||
});
|
||||
|
||||
import { modelsStatusCommand } from "./list.status-command.js";
|
||||
|
||||
const defaultResolveEnvApiKeyImpl:
|
||||
|
||||
@@ -105,7 +105,8 @@ describe("rotateSessionFile", () => {
|
||||
let now = Date.now();
|
||||
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => (now += 5));
|
||||
try {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// 4 rotations are enough to verify pruning to <=3 backups.
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await fs.writeFile(storePath, `data-${i}-${"x".repeat(100)}`, "utf-8");
|
||||
await rotateSessionFile(storePath, 50);
|
||||
}
|
||||
|
||||
@@ -29,37 +29,6 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
describe("gateway config methods", () => {
|
||||
type AgentConfigEntry = {
|
||||
id: string;
|
||||
default?: boolean;
|
||||
workspace?: string;
|
||||
};
|
||||
|
||||
const seedAgentsConfig = async (list: AgentConfigEntry[]) => {
|
||||
const setRes = await rpcReq<{ ok?: boolean }>(ws, "config.set", {
|
||||
raw: JSON.stringify({
|
||||
agents: {
|
||||
list,
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(setRes.ok).toBe(true);
|
||||
};
|
||||
|
||||
const readConfigHash = async () => {
|
||||
const snapshotRes = await rpcReq<{ hash?: string }>(ws, "config.get", {});
|
||||
expect(snapshotRes.ok).toBe(true);
|
||||
expect(typeof snapshotRes.payload?.hash).toBe("string");
|
||||
return snapshotRes.payload?.hash ?? "";
|
||||
};
|
||||
|
||||
it("returns a config snapshot", async () => {
|
||||
const res = await rpcReq<{ hash?: string; raw?: string }>(ws, "config.get", {});
|
||||
expect(res.ok).toBe(true);
|
||||
const payload = res.payload ?? {};
|
||||
expect(typeof payload.raw === "string" || typeof payload.hash === "string").toBe(true);
|
||||
});
|
||||
|
||||
it("rejects config.patch when raw is not an object", async () => {
|
||||
const res = await rpcReq<{ ok?: boolean }>(ws, "config.patch", {
|
||||
raw: "[]",
|
||||
@@ -67,75 +36,6 @@ describe("gateway config methods", () => {
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.error?.message ?? "").toContain("raw must be an object");
|
||||
});
|
||||
|
||||
it("merges agents.list entries by id instead of replacing the full array", async () => {
|
||||
await seedAgentsConfig([
|
||||
{ id: "primary", default: true, workspace: "/tmp/primary" },
|
||||
{ id: "secondary", workspace: "/tmp/secondary" },
|
||||
]);
|
||||
const baseHash = await readConfigHash();
|
||||
|
||||
const patchRes = await rpcReq<{
|
||||
config?: {
|
||||
agents?: {
|
||||
list?: Array<{
|
||||
id?: string;
|
||||
workspace?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}>(ws, "config.patch", {
|
||||
baseHash,
|
||||
raw: JSON.stringify({
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "primary",
|
||||
workspace: "/tmp/primary-updated",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(patchRes.ok).toBe(true);
|
||||
|
||||
const list = patchRes.payload?.config?.agents?.list ?? [];
|
||||
expect(list).toHaveLength(2);
|
||||
const primary = list.find((entry) => entry.id === "primary");
|
||||
const secondary = list.find((entry) => entry.id === "secondary");
|
||||
expect(primary?.workspace).toBe("/tmp/primary-updated");
|
||||
expect(secondary?.workspace).toBe("/tmp/secondary");
|
||||
});
|
||||
|
||||
it("rejects mixed-id agents.list patches without mutating persisted config", async () => {
|
||||
await seedAgentsConfig([
|
||||
{ id: "primary", default: true, workspace: "/tmp/primary" },
|
||||
{ id: "secondary", workspace: "/tmp/secondary" },
|
||||
]);
|
||||
const beforeHash = await readConfigHash();
|
||||
|
||||
const patchRes = await rpcReq<{ ok?: boolean }>(ws, "config.patch", {
|
||||
baseHash: beforeHash,
|
||||
raw: JSON.stringify({
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "primary",
|
||||
workspace: "/tmp/primary-updated",
|
||||
},
|
||||
{
|
||||
workspace: "/tmp/orphan-no-id",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(patchRes.ok).toBe(false);
|
||||
expect(patchRes.error?.message ?? "").toContain("invalid config");
|
||||
|
||||
const afterHash = await readConfigHash();
|
||||
expect(afterHash).toBe(beforeHash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gateway server sessions", () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
diagnosticSessionStates,
|
||||
getDiagnosticSessionStateCountForTest,
|
||||
getDiagnosticSessionState,
|
||||
pruneDiagnosticSessionStates,
|
||||
resetDiagnosticSessionStateForTest,
|
||||
} from "./diagnostic-session-state.js";
|
||||
|
||||
@@ -28,9 +30,16 @@ describe("diagnostic session state pruning", () => {
|
||||
});
|
||||
|
||||
it("caps tracked session states to a bounded max", () => {
|
||||
const now = Date.now();
|
||||
for (let i = 0; i < 2001; i += 1) {
|
||||
getDiagnosticSessionState({ sessionId: `session-${i}` });
|
||||
diagnosticSessionStates.set(`session-${i}`, {
|
||||
sessionId: `session-${i}`,
|
||||
lastActivity: now + i,
|
||||
state: "idle",
|
||||
queueDepth: 1,
|
||||
});
|
||||
}
|
||||
pruneDiagnosticSessionStates(now + 2002, true);
|
||||
|
||||
expect(getDiagnosticSessionStateCountForTest()).toBe(2000);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveApiKeyForProvider } from "../agents/model-auth.js";
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
@@ -32,13 +32,16 @@ vi.mock("../process/exec.js", () => ({
|
||||
let applyMediaUnderstanding: typeof import("./apply.js").applyMediaUnderstanding;
|
||||
|
||||
const TEMP_MEDIA_PREFIX = "openclaw-media-";
|
||||
const tempMediaDirs: string[] = [];
|
||||
let suiteTempMediaRootDir = "";
|
||||
let tempMediaDirCounter = 0;
|
||||
|
||||
async function createTempMediaDir() {
|
||||
const baseDir = resolvePreferredOpenClawTmpDir();
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
const dir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX));
|
||||
tempMediaDirs.push(dir);
|
||||
if (!suiteTempMediaRootDir) {
|
||||
throw new Error("suite temp media root not initialized");
|
||||
}
|
||||
const dir = path.join(suiteTempMediaRootDir, `case-${String(tempMediaDirCounter)}`);
|
||||
tempMediaDirCounter += 1;
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -162,6 +165,9 @@ describe("applyMediaUnderstanding", () => {
|
||||
const mockedFetchRemoteMedia = vi.mocked(fetchRemoteMedia);
|
||||
|
||||
beforeAll(async () => {
|
||||
const baseDir = resolvePreferredOpenClawTmpDir();
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
suiteTempMediaRootDir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX));
|
||||
({ applyMediaUnderstanding } = await import("./apply.js"));
|
||||
});
|
||||
|
||||
@@ -175,12 +181,12 @@ describe("applyMediaUnderstanding", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempMediaDirs.splice(0).map(async (dir) => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}),
|
||||
);
|
||||
afterAll(async () => {
|
||||
if (!suiteTempMediaRootDir) {
|
||||
return;
|
||||
}
|
||||
await fs.rm(suiteTempMediaRootDir, { recursive: true, force: true });
|
||||
suiteTempMediaRootDir = "";
|
||||
});
|
||||
|
||||
it("sets Transcript and replaces Body when audio transcription succeeds", async () => {
|
||||
|
||||
@@ -225,7 +225,6 @@ describe("telegram media groups", () => {
|
||||
const runtimeError = vi.fn();
|
||||
const { handler, replySpy } = await createBotHandlerWithOptions({ runtimeError });
|
||||
const fetchSpy = mockTelegramPngDownload();
|
||||
|
||||
const first = handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
@@ -277,7 +276,6 @@ describe("telegram media groups", () => {
|
||||
async () => {
|
||||
const { handler, replySpy } = await createBotHandler();
|
||||
const fetchSpy = mockTelegramPngDownload();
|
||||
|
||||
const first = handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
@@ -334,6 +332,7 @@ describe("telegram forwarded bursts", () => {
|
||||
const runtimeError = vi.fn();
|
||||
const { handler, replySpy } = await createBotHandlerWithOptions({ runtimeError });
|
||||
const fetchSpy = mockTelegramPngDownload();
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
await handler({
|
||||
@@ -362,12 +361,8 @@ describe("telegram forwarded bursts", () => {
|
||||
getFile: async () => ({ file_path: "photos/fwd1.jpg" }),
|
||||
});
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
{ timeout: FORWARD_BURST_TEST_TIMEOUT_MS, interval: 10 },
|
||||
);
|
||||
await vi.runAllTimersAsync();
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(runtimeError).not.toHaveBeenCalled();
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
@@ -375,6 +370,7 @@ describe("telegram forwarded bursts", () => {
|
||||
expect(payload.MediaPaths).toHaveLength(1);
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
},
|
||||
FORWARD_BURST_TEST_TIMEOUT_MS,
|
||||
@@ -589,49 +585,49 @@ describe("telegram text fragments", () => {
|
||||
async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS });
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS });
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(handler).toBeDefined();
|
||||
const part1 = "A".repeat(4050);
|
||||
const part2 = "B".repeat(50);
|
||||
|
||||
const part1 = "A".repeat(4050);
|
||||
const part2 = "B".repeat(50);
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
message_id: 10,
|
||||
date: 1736380800,
|
||||
text: part1,
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
message_id: 10,
|
||||
date: 1736380800,
|
||||
text: part1,
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
message_id: 11,
|
||||
date: 1736380801,
|
||||
text: part2,
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
message_id: 11,
|
||||
date: 1736380801,
|
||||
text: part2,
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(TEXT_FRAGMENT_FLUSH_MS * 2);
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
{ timeout: TEXT_FRAGMENT_FLUSH_MS * 2, interval: 10 },
|
||||
);
|
||||
|
||||
const payload = replySpy.mock.calls[0][0] as { RawBody?: string; Body?: string };
|
||||
expect(payload.RawBody).toContain(part1.slice(0, 32));
|
||||
expect(payload.RawBody).toContain(part2.slice(0, 32));
|
||||
const payload = replySpy.mock.calls[0][0] as { RawBody?: string; Body?: string };
|
||||
expect(payload.RawBody).toContain(part1.slice(0, 32));
|
||||
expect(payload.RawBody).toContain(part2.slice(0, 32));
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
},
|
||||
TEXT_FRAGMENT_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user