test: speed up telegram suites

This commit is contained in:
Peter Steinberger
2026-02-01 22:21:26 +00:00
parent bcde2fca5a
commit 9d2784cdb9
17 changed files with 426 additions and 471 deletions

View File

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

View File

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

View File

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

299
src/agents/skills.test.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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