From 9d2784cdb956b9e3b3b030248341342b8e0d73d8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 1 Feb 2026 22:21:26 +0000 Subject: [PATCH] test: speed up telegram suites --- .../skills.applyskillenvoverrides.test.ts | 102 ------ ...pty-prompt-skills-dirs-are-missing.test.ts | 120 ------- ...skills.buildworkspaceskillcommands.test.ts | 106 ------- src/agents/skills.test.ts | 299 ++++++++++++++++++ ...patterns-match-without-botusername.test.ts | 15 +- ...topic-skill-filters-system-prompts.test.ts | 15 +- ...-all-group-messages-grouppolicy-is.test.ts | 15 +- ...e-callback-query-updates-by-update.test.ts | 15 +- ...gram-bot.installs-grammy-throttler.test.ts | 16 +- ...lowfrom-entries-case-insensitively.test.ts | 15 +- ...-case-insensitively-grouppolicy-is.test.ts | 15 +- ...-dms-by-telegram-accountid-binding.test.ts | 15 +- ...ies-without-native-reply-threading.test.ts | 15 +- src/telegram/bot.test.ts | 15 +- .../bot/helpers.expand-text-links.test.ts | 52 --- src/telegram/bot/helpers.test.ts | 51 +++ ...send.returns-undefined-empty-input.test.ts | 16 +- 17 files changed, 426 insertions(+), 471 deletions(-) delete mode 100644 src/agents/skills.applyskillenvoverrides.test.ts delete mode 100644 src/agents/skills.build-workspace-skills-prompt.returns-empty-prompt-skills-dirs-are-missing.test.ts delete mode 100644 src/agents/skills.buildworkspaceskillcommands.test.ts create mode 100644 src/agents/skills.test.ts delete mode 100644 src/telegram/bot/helpers.expand-text-links.test.ts diff --git a/src/agents/skills.applyskillenvoverrides.test.ts b/src/agents/skills.applyskillenvoverrides.test.ts deleted file mode 100644 index f1350432c..000000000 --- a/src/agents/skills.applyskillenvoverrides.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { - applySkillEnvOverrides, - applySkillEnvOverridesFromSnapshot, - buildWorkspaceSkillSnapshot, - loadWorkspaceSkillEntries, -} from "./skills.js"; - -async function writeSkill(params: { - dir: string; - name: string; - description: string; - metadata?: string; - body?: string; -}) { - const { dir, name, description, metadata, body } = params; - await fs.mkdir(dir, { recursive: true }); - await fs.writeFile( - path.join(dir, "SKILL.md"), - `--- -name: ${name} -description: ${description}${metadata ? `\nmetadata: ${metadata}` : ""} ---- - -${body ?? `# ${name}\n`} -`, - "utf-8", - ); -} - -describe("applySkillEnvOverrides", () => { - it("sets and restores env vars", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - const skillDir = path.join(workspaceDir, "skills", "env-skill"); - await writeSkill({ - dir: skillDir, - name: "env-skill", - description: "Needs env", - metadata: '{"openclaw":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', - }); - - const entries = loadWorkspaceSkillEntries(workspaceDir, { - managedSkillsDir: path.join(workspaceDir, ".managed"), - }); - - const originalEnv = process.env.ENV_KEY; - delete process.env.ENV_KEY; - - const restore = applySkillEnvOverrides({ - skills: entries, - config: { skills: { entries: { "env-skill": { apiKey: "injected" } } } }, - }); - - try { - expect(process.env.ENV_KEY).toBe("injected"); - } finally { - restore(); - if (originalEnv === undefined) { - expect(process.env.ENV_KEY).toBeUndefined(); - } else { - expect(process.env.ENV_KEY).toBe(originalEnv); - } - } - }); - it("applies env overrides from snapshots", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - const skillDir = path.join(workspaceDir, "skills", "env-skill"); - await writeSkill({ - dir: skillDir, - name: "env-skill", - description: "Needs env", - metadata: '{"openclaw":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', - }); - - const snapshot = buildWorkspaceSkillSnapshot(workspaceDir, { - managedSkillsDir: path.join(workspaceDir, ".managed"), - config: { skills: { entries: { "env-skill": { apiKey: "snap-key" } } } }, - }); - - const originalEnv = process.env.ENV_KEY; - delete process.env.ENV_KEY; - - const restore = applySkillEnvOverridesFromSnapshot({ - snapshot, - config: { skills: { entries: { "env-skill": { apiKey: "snap-key" } } } }, - }); - - try { - expect(process.env.ENV_KEY).toBe("snap-key"); - } finally { - restore(); - if (originalEnv === undefined) { - expect(process.env.ENV_KEY).toBeUndefined(); - } else { - expect(process.env.ENV_KEY).toBe(originalEnv); - } - } - }); -}); diff --git a/src/agents/skills.build-workspace-skills-prompt.returns-empty-prompt-skills-dirs-are-missing.test.ts b/src/agents/skills.build-workspace-skills-prompt.returns-empty-prompt-skills-dirs-are-missing.test.ts deleted file mode 100644 index be6eb7cd6..000000000 --- a/src/agents/skills.build-workspace-skills-prompt.returns-empty-prompt-skills-dirs-are-missing.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { buildWorkspaceSkillsPrompt } from "./skills.js"; - -async function writeSkill(params: { - dir: string; - name: string; - description: string; - metadata?: string; - body?: string; -}) { - const { dir, name, description, metadata, body } = params; - await fs.mkdir(dir, { recursive: true }); - await fs.writeFile( - path.join(dir, "SKILL.md"), - `--- -name: ${name} -description: ${description}${metadata ? `\nmetadata: ${metadata}` : ""} ---- - -${body ?? `# ${name}\n`} -`, - "utf-8", - ); -} - -describe("buildWorkspaceSkillsPrompt", () => { - it("returns empty prompt when skills dirs are missing", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - - const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { - managedSkillsDir: path.join(workspaceDir, ".managed"), - bundledSkillsDir: path.join(workspaceDir, ".bundled"), - }); - - expect(prompt).toBe(""); - }); - it("loads bundled skills when present", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - const bundledDir = path.join(workspaceDir, ".bundled"); - const bundledSkillDir = path.join(bundledDir, "peekaboo"); - - await writeSkill({ - dir: bundledSkillDir, - name: "peekaboo", - description: "Capture UI", - body: "# Peekaboo\n", - }); - - const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { - managedSkillsDir: path.join(workspaceDir, ".managed"), - bundledSkillsDir: bundledDir, - }); - expect(prompt).toContain("peekaboo"); - expect(prompt).toContain("Capture UI"); - expect(prompt).toContain(path.join(bundledSkillDir, "SKILL.md")); - }); - it("loads extra skill folders from config (lowest precedence)", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - const extraDir = path.join(workspaceDir, ".extra"); - const bundledDir = path.join(workspaceDir, ".bundled"); - const managedDir = path.join(workspaceDir, ".managed"); - - await writeSkill({ - dir: path.join(extraDir, "demo-skill"), - name: "demo-skill", - description: "Extra version", - body: "# Extra\n", - }); - await writeSkill({ - dir: path.join(bundledDir, "demo-skill"), - name: "demo-skill", - description: "Bundled version", - body: "# Bundled\n", - }); - await writeSkill({ - dir: path.join(managedDir, "demo-skill"), - name: "demo-skill", - description: "Managed version", - body: "# Managed\n", - }); - await writeSkill({ - dir: path.join(workspaceDir, "skills", "demo-skill"), - name: "demo-skill", - description: "Workspace version", - body: "# Workspace\n", - }); - - const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { - bundledSkillsDir: bundledDir, - managedSkillsDir: managedDir, - config: { skills: { load: { extraDirs: [extraDir] } } }, - }); - - expect(prompt).toContain("Workspace version"); - expect(prompt).not.toContain("Managed version"); - expect(prompt).not.toContain("Bundled version"); - expect(prompt).not.toContain("Extra version"); - }); - it("loads skills from workspace skills/", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - const skillDir = path.join(workspaceDir, "skills", "demo-skill"); - - await writeSkill({ - dir: skillDir, - name: "demo-skill", - description: "Does demo things", - body: "# Demo Skill\n", - }); - - const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { - managedSkillsDir: path.join(workspaceDir, ".managed"), - }); - expect(prompt).toContain("demo-skill"); - expect(prompt).toContain("Does demo things"); - expect(prompt).toContain(path.join(skillDir, "SKILL.md")); - }); -}); diff --git a/src/agents/skills.buildworkspaceskillcommands.test.ts b/src/agents/skills.buildworkspaceskillcommands.test.ts deleted file mode 100644 index 648be6592..000000000 --- a/src/agents/skills.buildworkspaceskillcommands.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { buildWorkspaceSkillCommandSpecs } from "./skills.js"; - -async function writeSkill(params: { - dir: string; - name: string; - description: string; - frontmatterExtra?: string; -}) { - const { dir, name, description, frontmatterExtra } = params; - await fs.mkdir(dir, { recursive: true }); - await fs.writeFile( - path.join(dir, "SKILL.md"), - `--- -name: ${name} -description: ${description} -${frontmatterExtra ?? ""} ---- - -# ${name} -`, - "utf-8", - ); -} - -describe("buildWorkspaceSkillCommandSpecs", () => { - it("sanitizes and de-duplicates command names", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - await writeSkill({ - dir: path.join(workspaceDir, "skills", "hello-world"), - name: "hello-world", - description: "Hello world skill", - }); - await writeSkill({ - dir: path.join(workspaceDir, "skills", "hello_world"), - name: "hello_world", - description: "Hello underscore skill", - }); - await writeSkill({ - dir: path.join(workspaceDir, "skills", "help"), - name: "help", - description: "Help skill", - }); - await writeSkill({ - dir: path.join(workspaceDir, "skills", "hidden"), - name: "hidden-skill", - description: "Hidden skill", - frontmatterExtra: "user-invocable: false", - }); - - const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, { - managedSkillsDir: path.join(workspaceDir, ".managed"), - bundledSkillsDir: path.join(workspaceDir, ".bundled"), - reservedNames: new Set(["help"]), - }); - - const names = commands.map((entry) => entry.name).toSorted(); - expect(names).toEqual(["hello_world", "hello_world_2", "help_2"]); - expect(commands.find((entry) => entry.skillName === "hidden-skill")).toBeUndefined(); - }); - - it("truncates descriptions longer than 100 characters for Discord compatibility", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - const longDescription = - "This is a very long description that exceeds Discord's 100 character limit for slash command descriptions and should be truncated"; - await writeSkill({ - dir: path.join(workspaceDir, "skills", "long-desc"), - name: "long-desc", - description: longDescription, - }); - await writeSkill({ - dir: path.join(workspaceDir, "skills", "short-desc"), - name: "short-desc", - description: "Short description", - }); - - const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, { - managedSkillsDir: path.join(workspaceDir, ".managed"), - bundledSkillsDir: path.join(workspaceDir, ".bundled"), - }); - - const longCmd = commands.find((entry) => entry.skillName === "long-desc"); - const shortCmd = commands.find((entry) => entry.skillName === "short-desc"); - - expect(longCmd?.description.length).toBeLessThanOrEqual(100); - expect(longCmd?.description.endsWith("…")).toBe(true); - expect(shortCmd?.description).toBe("Short description"); - }); - - it("includes tool-dispatch metadata from frontmatter", async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); - await writeSkill({ - dir: path.join(workspaceDir, "skills", "tool-dispatch"), - name: "tool-dispatch", - description: "Dispatch to a tool", - frontmatterExtra: "command-dispatch: tool\ncommand-tool: sessions_send", - }); - - const commands = buildWorkspaceSkillCommandSpecs(workspaceDir); - const cmd = commands.find((entry) => entry.skillName === "tool-dispatch"); - expect(cmd?.dispatch).toEqual({ kind: "tool", toolName: "sessions_send", argMode: "raw" }); - }); -}); diff --git a/src/agents/skills.test.ts b/src/agents/skills.test.ts new file mode 100644 index 000000000..a174da332 --- /dev/null +++ b/src/agents/skills.test.ts @@ -0,0 +1,299 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + applySkillEnvOverrides, + applySkillEnvOverridesFromSnapshot, + buildWorkspaceSkillCommandSpecs, + buildWorkspaceSkillsPrompt, + buildWorkspaceSkillSnapshot, + loadWorkspaceSkillEntries, +} from "./skills.js"; + +type SkillFixture = { + dir: string; + name: string; + description: string; + metadata?: string; + body?: string; + frontmatterExtra?: string; +}; + +const tempDirs: string[] = []; + +const makeWorkspace = async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-")); + tempDirs.push(workspaceDir); + return workspaceDir; +}; + +const writeSkill = async (params: SkillFixture) => { + const { dir, name, description, metadata, body, frontmatterExtra } = params; + await fs.mkdir(dir, { recursive: true }); + const frontmatter = [ + `name: ${name}`, + `description: ${description}`, + metadata ? `metadata: ${metadata}` : "", + frontmatterExtra ?? "", + ] + .filter((line) => line.trim().length > 0) + .join("\n"); + await fs.writeFile( + path.join(dir, "SKILL.md"), + `---\n${frontmatter}\n---\n\n${body ?? `# ${name}\n`}`, + "utf-8", + ); +}; + +afterEach(async () => { + await Promise.all( + tempDirs.splice(0, tempDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })), + ); +}); + +describe("buildWorkspaceSkillCommandSpecs", () => { + it("sanitizes and de-duplicates command names", async () => { + const workspaceDir = await makeWorkspace(); + await writeSkill({ + dir: path.join(workspaceDir, "skills", "hello-world"), + name: "hello-world", + description: "Hello world skill", + }); + await writeSkill({ + dir: path.join(workspaceDir, "skills", "hello_world"), + name: "hello_world", + description: "Hello underscore skill", + }); + await writeSkill({ + dir: path.join(workspaceDir, "skills", "help"), + name: "help", + description: "Help skill", + }); + await writeSkill({ + dir: path.join(workspaceDir, "skills", "hidden"), + name: "hidden-skill", + description: "Hidden skill", + frontmatterExtra: "user-invocable: false", + }); + + const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + bundledSkillsDir: path.join(workspaceDir, ".bundled"), + reservedNames: new Set(["help"]), + }); + + const names = commands.map((entry) => entry.name).toSorted(); + expect(names).toEqual(["hello_world", "hello_world_2", "help_2"]); + expect(commands.find((entry) => entry.skillName === "hidden-skill")).toBeUndefined(); + }); + + it("truncates descriptions longer than 100 characters for Discord compatibility", async () => { + const workspaceDir = await makeWorkspace(); + const longDescription = + "This is a very long description that exceeds Discord's 100 character limit for slash command descriptions and should be truncated"; + await writeSkill({ + dir: path.join(workspaceDir, "skills", "long-desc"), + name: "long-desc", + description: longDescription, + }); + await writeSkill({ + dir: path.join(workspaceDir, "skills", "short-desc"), + name: "short-desc", + description: "Short description", + }); + + const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + bundledSkillsDir: path.join(workspaceDir, ".bundled"), + }); + + const longCmd = commands.find((entry) => entry.skillName === "long-desc"); + const shortCmd = commands.find((entry) => entry.skillName === "short-desc"); + + expect(longCmd?.description.length).toBeLessThanOrEqual(100); + expect(longCmd?.description.endsWith("…")).toBe(true); + expect(shortCmd?.description).toBe("Short description"); + }); + + it("includes tool-dispatch metadata from frontmatter", async () => { + const workspaceDir = await makeWorkspace(); + await writeSkill({ + dir: path.join(workspaceDir, "skills", "tool-dispatch"), + name: "tool-dispatch", + description: "Dispatch to a tool", + frontmatterExtra: "command-dispatch: tool\ncommand-tool: sessions_send", + }); + + const commands = buildWorkspaceSkillCommandSpecs(workspaceDir); + const cmd = commands.find((entry) => entry.skillName === "tool-dispatch"); + expect(cmd?.dispatch).toEqual({ kind: "tool", toolName: "sessions_send", argMode: "raw" }); + }); +}); + +describe("buildWorkspaceSkillsPrompt", () => { + it("returns empty prompt when skills dirs are missing", async () => { + const workspaceDir = await makeWorkspace(); + + const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + bundledSkillsDir: path.join(workspaceDir, ".bundled"), + }); + + expect(prompt).toBe(""); + }); + + it("loads bundled skills when present", async () => { + const workspaceDir = await makeWorkspace(); + const bundledDir = path.join(workspaceDir, ".bundled"); + const bundledSkillDir = path.join(bundledDir, "peekaboo"); + + await writeSkill({ + dir: bundledSkillDir, + name: "peekaboo", + description: "Capture UI", + body: "# Peekaboo\n", + }); + + const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + bundledSkillsDir: bundledDir, + }); + expect(prompt).toContain("peekaboo"); + expect(prompt).toContain("Capture UI"); + expect(prompt).toContain(path.join(bundledSkillDir, "SKILL.md")); + }); + + it("loads extra skill folders from config (lowest precedence)", async () => { + const workspaceDir = await makeWorkspace(); + const extraDir = path.join(workspaceDir, ".extra"); + const bundledDir = path.join(workspaceDir, ".bundled"); + const managedDir = path.join(workspaceDir, ".managed"); + + await writeSkill({ + dir: path.join(extraDir, "demo-skill"), + name: "demo-skill", + description: "Extra version", + body: "# Extra\n", + }); + await writeSkill({ + dir: path.join(bundledDir, "demo-skill"), + name: "demo-skill", + description: "Bundled version", + body: "# Bundled\n", + }); + await writeSkill({ + dir: path.join(managedDir, "demo-skill"), + name: "demo-skill", + description: "Managed version", + body: "# Managed\n", + }); + await writeSkill({ + dir: path.join(workspaceDir, "skills", "demo-skill"), + name: "demo-skill", + description: "Workspace version", + body: "# Workspace\n", + }); + + const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { + bundledSkillsDir: bundledDir, + managedSkillsDir: managedDir, + config: { skills: { load: { extraDirs: [extraDir] } } }, + }); + + expect(prompt).toContain("Workspace version"); + expect(prompt).not.toContain("Managed version"); + expect(prompt).not.toContain("Bundled version"); + expect(prompt).not.toContain("Extra version"); + }); + + it("loads skills from workspace skills/", async () => { + const workspaceDir = await makeWorkspace(); + const skillDir = path.join(workspaceDir, "skills", "demo-skill"); + + await writeSkill({ + dir: skillDir, + name: "demo-skill", + description: "Does demo things", + body: "# Demo Skill\n", + }); + + const prompt = buildWorkspaceSkillsPrompt(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + }); + expect(prompt).toContain("demo-skill"); + expect(prompt).toContain("Does demo things"); + expect(prompt).toContain(path.join(skillDir, "SKILL.md")); + }); +}); + +describe("applySkillEnvOverrides", () => { + it("sets and restores env vars", async () => { + const workspaceDir = await makeWorkspace(); + const skillDir = path.join(workspaceDir, "skills", "env-skill"); + await writeSkill({ + dir: skillDir, + name: "env-skill", + description: "Needs env", + metadata: '{"openclaw":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', + }); + + const entries = loadWorkspaceSkillEntries(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + }); + + const originalEnv = process.env.ENV_KEY; + delete process.env.ENV_KEY; + + const restore = applySkillEnvOverrides({ + skills: entries, + config: { skills: { entries: { "env-skill": { apiKey: "injected" } } } }, + }); + + try { + expect(process.env.ENV_KEY).toBe("injected"); + } finally { + restore(); + if (originalEnv === undefined) { + expect(process.env.ENV_KEY).toBeUndefined(); + } else { + expect(process.env.ENV_KEY).toBe(originalEnv); + } + } + }); + + it("applies env overrides from snapshots", async () => { + const workspaceDir = await makeWorkspace(); + const skillDir = path.join(workspaceDir, "skills", "env-skill"); + await writeSkill({ + dir: skillDir, + name: "env-skill", + description: "Needs env", + metadata: '{"openclaw":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', + }); + + const snapshot = buildWorkspaceSkillSnapshot(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + config: { skills: { entries: { "env-skill": { apiKey: "snap-key" } } } }, + }); + + const originalEnv = process.env.ENV_KEY; + delete process.env.ENV_KEY; + + const restore = applySkillEnvOverridesFromSnapshot({ + snapshot, + config: { skills: { entries: { "env-skill": { apiKey: "snap-key" } } } }, + }); + + try { + expect(process.env.ENV_KEY).toBe("snap-key"); + } finally { + restore(); + if (originalEnv === undefined) { + expect(process.env.ENV_KEY).toBeUndefined(); + } else { + expect(process.env.ENV_KEY).toBe(originalEnv); + } + } + }); +}); diff --git a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts index 62fa9eeca..46f1ba98f 100644 --- a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts +++ b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts @@ -1,8 +1,7 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, @@ -137,11 +136,11 @@ const getOnHandler = (event: string) => { const ORIGINAL_TZ = process.env.TZ; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { process.env.TZ = "UTC"; resetInboundDedupe(); loadConfig.mockReturnValue({ diff --git a/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts b/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts index 06a924e84..0e1a68cb5 100644 --- a/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts +++ b/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts @@ -1,7 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, @@ -135,11 +134,11 @@ const getOnHandler = (event: string) => { }; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts b/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts index b52e93406..0436c03ce 100644 --- a/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts +++ b/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts @@ -1,7 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, @@ -135,11 +134,11 @@ const getOnHandler = (event: string) => { }; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts b/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts index 4c0828c44..55b851dda 100644 --- a/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts +++ b/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts @@ -1,7 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, @@ -135,11 +134,11 @@ const getOnHandler = (event: string) => { }; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts index 3f08a45f6..292c257fa 100644 --- a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts +++ b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts @@ -1,11 +1,9 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot, getTelegramSequentialKey } from "./bot.js"; import { resolveTelegramFetch } from "./fetch.js"; -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let getTelegramSequentialKey: typeof import("./bot.js").getTelegramSequentialKey; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; - const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-throttler-${Math.random().toString(16).slice(2)}.json`, })); @@ -141,11 +139,11 @@ const getOnHandler = (event: string) => { const ORIGINAL_TZ = process.env.TZ; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot, getTelegramSequentialKey } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { process.env.TZ = "UTC"; resetInboundDedupe(); loadConfig.mockReturnValue({ diff --git a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts index dea2babb4..c5449baf2 100644 --- a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts +++ b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts @@ -1,7 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, @@ -135,11 +134,11 @@ const getOnHandler = (event: string) => { }; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts b/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts index d99126eed..312fe4d07 100644 --- a/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts +++ b/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts @@ -1,7 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, @@ -135,11 +134,11 @@ const getOnHandler = (event: string) => { }; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts b/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts index b7e87debf..6fad17e73 100644 --- a/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts +++ b/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts @@ -1,7 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, @@ -135,11 +134,11 @@ const getOnHandler = (event: string) => { }; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts b/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts index d8a5c91c4..f36161d4b 100644 --- a/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts +++ b/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts @@ -1,10 +1,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot } from "./bot.js"; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-reply-threading-${Math.random() @@ -140,11 +139,11 @@ const getOnHandler = (event: string) => { }; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index a11803125..bc96c5b60 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -1,18 +1,17 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js"; import { listNativeCommandSpecs, listNativeCommandSpecsForConfig, } from "../auto-reply/commands-registry.js"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { createTelegramBot, getTelegramSequentialKey } from "./bot.js"; import { resolveTelegramFetch } from "./fetch.js"; -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let getTelegramSequentialKey: typeof import("./bot.js").getTelegramSequentialKey; -let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; let replyModule: typeof import("../auto-reply/reply.js"); const { listSkillCommandsForAgents } = vi.hoisted(() => ({ listSkillCommandsForAgents: vi.fn(() => []), @@ -175,11 +174,11 @@ const getOnHandler = (event: string) => { const ORIGINAL_TZ = process.env.TZ; describe("createTelegramBot", () => { - beforeEach(async () => { - vi.resetModules(); - ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); - ({ createTelegramBot, getTelegramSequentialKey } = await import("./bot.js")); + beforeAll(async () => { replyModule = await import("../auto-reply/reply.js"); + }); + + beforeEach(() => { process.env.TZ = "UTC"; resetInboundDedupe(); loadConfig.mockReturnValue({ diff --git a/src/telegram/bot/helpers.expand-text-links.test.ts b/src/telegram/bot/helpers.expand-text-links.test.ts deleted file mode 100644 index 7035a670a..000000000 --- a/src/telegram/bot/helpers.expand-text-links.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { expandTextLinks } from "./helpers.js"; - -describe("expandTextLinks", () => { - it("returns text unchanged when no entities are provided", () => { - expect(expandTextLinks("Hello world")).toBe("Hello world"); - expect(expandTextLinks("Hello world", null)).toBe("Hello world"); - expect(expandTextLinks("Hello world", [])).toBe("Hello world"); - }); - - it("returns text unchanged when there are no text_link entities", () => { - const entities = [ - { type: "mention", offset: 0, length: 5 }, - { type: "bold", offset: 6, length: 5 }, - ]; - expect(expandTextLinks("@user hello", entities)).toBe("@user hello"); - }); - - it("expands a single text_link entity", () => { - const text = "Check this link for details"; - const entities = [{ type: "text_link", offset: 11, length: 4, url: "https://example.com" }]; - expect(expandTextLinks(text, entities)).toBe( - "Check this [link](https://example.com) for details", - ); - }); - - it("expands multiple text_link entities", () => { - const text = "Visit Google or GitHub for more"; - const entities = [ - { type: "text_link", offset: 6, length: 6, url: "https://google.com" }, - { type: "text_link", offset: 16, length: 6, url: "https://github.com" }, - ]; - expect(expandTextLinks(text, entities)).toBe( - "Visit [Google](https://google.com) or [GitHub](https://github.com) for more", - ); - }); - - it("handles adjacent text_link entities", () => { - const text = "AB"; - const entities = [ - { type: "text_link", offset: 0, length: 1, url: "https://a.example" }, - { type: "text_link", offset: 1, length: 1, url: "https://b.example" }, - ]; - expect(expandTextLinks(text, entities)).toBe("[A](https://a.example)[B](https://b.example)"); - }); - - it("preserves offsets from the original string", () => { - const text = " Hello world"; - const entities = [{ type: "text_link", offset: 1, length: 5, url: "https://example.com" }]; - expect(expandTextLinks(text, entities)).toBe(" [Hello](https://example.com) world"); - }); -}); diff --git a/src/telegram/bot/helpers.test.ts b/src/telegram/bot/helpers.test.ts index 1f0a58132..96a41c219 100644 --- a/src/telegram/bot/helpers.test.ts +++ b/src/telegram/bot/helpers.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { buildTelegramThreadParams, buildTypingThreadParams, + expandTextLinks, normalizeForwardedContext, resolveTelegramForumThreadId, } from "./helpers.js"; @@ -120,3 +121,53 @@ describe("normalizeForwardedContext", () => { expect(ctx?.date).toBe(111); }); }); + +describe("expandTextLinks", () => { + it("returns text unchanged when no entities are provided", () => { + expect(expandTextLinks("Hello world")).toBe("Hello world"); + expect(expandTextLinks("Hello world", null)).toBe("Hello world"); + expect(expandTextLinks("Hello world", [])).toBe("Hello world"); + }); + + it("returns text unchanged when there are no text_link entities", () => { + const entities = [ + { type: "mention", offset: 0, length: 5 }, + { type: "bold", offset: 6, length: 5 }, + ]; + expect(expandTextLinks("@user hello", entities)).toBe("@user hello"); + }); + + it("expands a single text_link entity", () => { + const text = "Check this link for details"; + const entities = [{ type: "text_link", offset: 11, length: 4, url: "https://example.com" }]; + expect(expandTextLinks(text, entities)).toBe( + "Check this [link](https://example.com) for details", + ); + }); + + it("expands multiple text_link entities", () => { + const text = "Visit Google or GitHub for more"; + const entities = [ + { type: "text_link", offset: 6, length: 6, url: "https://google.com" }, + { type: "text_link", offset: 16, length: 6, url: "https://github.com" }, + ]; + expect(expandTextLinks(text, entities)).toBe( + "Visit [Google](https://google.com) or [GitHub](https://github.com) for more", + ); + }); + + it("handles adjacent text_link entities", () => { + const text = "AB"; + const entities = [ + { type: "text_link", offset: 0, length: 1, url: "https://a.example" }, + { type: "text_link", offset: 1, length: 1, url: "https://b.example" }, + ]; + expect(expandTextLinks(text, entities)).toBe("[A](https://a.example)[B](https://b.example)"); + }); + + it("preserves offsets from the original string", () => { + const text = " Hello world"; + const entities = [{ type: "text_link", offset: 1, length: 5, url: "https://example.com" }]; + expect(expandTextLinks(text, entities)).toBe(" [Hello](https://example.com) world"); + }); +}); diff --git a/src/telegram/send.returns-undefined-empty-input.test.ts b/src/telegram/send.returns-undefined-empty-input.test.ts index b6b497789..000708dca 100644 --- a/src/telegram/send.returns-undefined-empty-input.test.ts +++ b/src/telegram/send.returns-undefined-empty-input.test.ts @@ -596,16 +596,12 @@ describe("sendStickerTelegram", () => { expect(res.chatId).toBe(chatId); }); - it("throws error when fileId is empty", async () => { - await expect(sendStickerTelegram("123", "", { token: "tok" })).rejects.toThrow( - /file_id is required/i, - ); - }); - - it("throws error when fileId is whitespace only", async () => { - await expect(sendStickerTelegram("123", " ", { token: "tok" })).rejects.toThrow( - /file_id is required/i, - ); + it("throws error when fileId is blank", async () => { + for (const fileId of ["", " "]) { + await expect(sendStickerTelegram("123", fileId, { token: "tok" })).rejects.toThrow( + /file_id is required/i, + ); + } }); it("includes message_thread_id for forum topic messages", async () => {