diff --git a/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts b/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts index 2d1e591ccc8..d9ab9810a32 100644 --- a/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts +++ b/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts @@ -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: { diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts index 6c9038164af..974bd181726 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts @@ -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 () => { diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index b28b43360a9..671d35e56c9 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -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 [ { diff --git a/src/agents/skills-install.download.test.ts b/src/agents/skills-install.download.test.ts index c28c88118f9..912b6ccb92e 100644 --- a/src/agents/skills-install.download.test.ts +++ b/src/agents/skills-install.download.test.ts @@ -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", diff --git a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts index 5bd9921486c..ac28d9c3b5d 100644 --- a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts +++ b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts @@ -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 { + 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, diff --git a/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts b/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts index 7cf3f5fa493..9ad7efbe5db 100644 --- a/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts +++ b/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts @@ -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 { } } +let fixtureRoot = ""; +let fixtureCount = 0; + +async function createCaseDir(prefix: string): Promise { + 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", diff --git a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts index aef59ed891c..93600471690 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts @@ -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(fn: (home: string) => Promise): Promise { - 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: { diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.test.ts index e772dabe3eb..d8b3f8d4f12 100644 --- a/src/commands/models/list.status.test.ts +++ b/src/commands/models/list.status.test.ts @@ -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(); + return { + ...actual, + loadProviderUsageSummary: mocks.loadProviderUsageSummary, + }; +}); + import { modelsStatusCommand } from "./list.status-command.js"; const defaultResolveEnvApiKeyImpl: diff --git a/src/config/sessions/store.pruning.test.ts b/src/config/sessions/store.pruning.test.ts index 677a01fb4cf..2efd200441c 100644 --- a/src/config/sessions/store.pruning.test.ts +++ b/src/config/sessions/store.pruning.test.ts @@ -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); } diff --git a/src/gateway/server.config-patch.test.ts b/src/gateway/server.config-patch.test.ts index e08174527cb..a4b487b834f 100644 --- a/src/gateway/server.config-patch.test.ts +++ b/src/gateway/server.config-patch.test.ts @@ -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", () => { diff --git a/src/logging/diagnostic.test.ts b/src/logging/diagnostic.test.ts index 1648b244b64..37eecaf0b12 100644 --- a/src/logging/diagnostic.test.ts +++ b/src/logging/diagnostic.test.ts @@ -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); }); diff --git a/src/media-understanding/apply.test.ts b/src/media-understanding/apply.test.ts index 3ce3b888b88..c798cfb2876 100644 --- a/src/media-understanding/apply.test.ts +++ b/src/media-understanding/apply.test.ts @@ -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 () => { diff --git a/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts b/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts index c43c3f1746c..88e9f29590e 100644 --- a/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts +++ b/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts @@ -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, + ) => Promise; + expect(handler).toBeDefined(); - createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS }); - const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as ( - ctx: Record, - ) => Promise; - 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, );