test(perf): dedupe fixtures and reduce flaky waits

This commit is contained in:
Peter Steinberger
2026-02-22 22:05:49 +00:00
parent b534dfa3e0
commit 7b229decdd
13 changed files with 249 additions and 239 deletions

View File

@@ -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: {

View File

@@ -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 () => {

View File

@@ -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 [
{

View File

@@ -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",

View File

@@ -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,

View File

@@ -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",

View File

@@ -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: {

View File

@@ -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:

View File

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

View File

@@ -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", () => {

View File

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

View File

@@ -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 () => {

View File

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