test: speed up telegram suites
This commit is contained in:
parent
bcde2fca5a
commit
9d2784cdb9
17 changed files with 426 additions and 471 deletions
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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
299
src/agents/skills.test.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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";
|
import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
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;
|
const ORIGINAL_TZ = process.env.TZ;
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
process.env.TZ = "UTC";
|
process.env.TZ = "UTC";
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
|
@ -135,11 +134,11 @@ const getOnHandler = (event: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
|
@ -135,11 +134,11 @@ const getOnHandler = (event: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
|
@ -135,11 +134,11 @@ const getOnHandler = (event: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -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 { 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";
|
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(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-throttler-${Math.random().toString(16).slice(2)}.json`,
|
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;
|
const ORIGINAL_TZ = process.env.TZ;
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot, getTelegramSequentialKey } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
process.env.TZ = "UTC";
|
process.env.TZ = "UTC";
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
|
@ -135,11 +134,11 @@ const getOnHandler = (event: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
|
@ -135,11 +134,11 @@ const getOnHandler = (event: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
|
@ -135,11 +134,11 @@ const getOnHandler = (event: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||||
let createTelegramBot: typeof import("./bot.js").createTelegramBot;
|
import { createTelegramBot } from "./bot.js";
|
||||||
let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe;
|
|
||||||
|
|
||||||
const { sessionStorePath } = vi.hoisted(() => ({
|
const { sessionStorePath } = vi.hoisted(() => ({
|
||||||
sessionStorePath: `/tmp/openclaw-telegram-reply-threading-${Math.random()
|
sessionStorePath: `/tmp/openclaw-telegram-reply-threading-${Math.random()
|
||||||
|
|
@ -140,11 +139,11 @@ const getOnHandler = (event: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
channels: {
|
channels: {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
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 { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
|
||||||
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
|
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
|
||||||
import {
|
import {
|
||||||
listNativeCommandSpecs,
|
listNativeCommandSpecs,
|
||||||
listNativeCommandSpecsForConfig,
|
listNativeCommandSpecsForConfig,
|
||||||
} from "../auto-reply/commands-registry.js";
|
} 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";
|
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");
|
let replyModule: typeof import("../auto-reply/reply.js");
|
||||||
const { listSkillCommandsForAgents } = vi.hoisted(() => ({
|
const { listSkillCommandsForAgents } = vi.hoisted(() => ({
|
||||||
listSkillCommandsForAgents: vi.fn(() => []),
|
listSkillCommandsForAgents: vi.fn(() => []),
|
||||||
|
|
@ -175,11 +174,11 @@ const getOnHandler = (event: string) => {
|
||||||
|
|
||||||
const ORIGINAL_TZ = process.env.TZ;
|
const ORIGINAL_TZ = process.env.TZ;
|
||||||
describe("createTelegramBot", () => {
|
describe("createTelegramBot", () => {
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
vi.resetModules();
|
|
||||||
({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js"));
|
|
||||||
({ createTelegramBot, getTelegramSequentialKey } = await import("./bot.js"));
|
|
||||||
replyModule = await import("../auto-reply/reply.js");
|
replyModule = await import("../auto-reply/reply.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
process.env.TZ = "UTC";
|
process.env.TZ = "UTC";
|
||||||
resetInboundDedupe();
|
resetInboundDedupe();
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
|
|
|
||||||
|
|
@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildTelegramThreadParams,
|
buildTelegramThreadParams,
|
||||||
buildTypingThreadParams,
|
buildTypingThreadParams,
|
||||||
|
expandTextLinks,
|
||||||
normalizeForwardedContext,
|
normalizeForwardedContext,
|
||||||
resolveTelegramForumThreadId,
|
resolveTelegramForumThreadId,
|
||||||
} from "./helpers.js";
|
} from "./helpers.js";
|
||||||
|
|
@ -120,3 +121,53 @@ describe("normalizeForwardedContext", () => {
|
||||||
expect(ctx?.date).toBe(111);
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -596,16 +596,12 @@ describe("sendStickerTelegram", () => {
|
||||||
expect(res.chatId).toBe(chatId);
|
expect(res.chatId).toBe(chatId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error when fileId is empty", async () => {
|
it("throws error when fileId is blank", async () => {
|
||||||
await expect(sendStickerTelegram("123", "", { token: "tok" })).rejects.toThrow(
|
for (const fileId of ["", " "]) {
|
||||||
/file_id is required/i,
|
await expect(sendStickerTelegram("123", fileId, { 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("includes message_thread_id for forum topic messages", async () => {
|
it("includes message_thread_id for forum topic messages", async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue