Files
openclaw/extensions/telegram/src/topic-name-cache.test.ts
Luke 0abe64a4ff Agents: clarify local model context preflight (#66236)
Merged via squash.

Prepared head SHA: 11bfaf15f6
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Reviewed-by: @ImLukeF
2026-04-14 15:38:10 +10:00

141 lines
4.9 KiB
TypeScript

import syncFs from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearTopicNameCache,
getTopicEntry,
getTopicName,
resetTopicNameCacheForTest,
topicNameCacheSize,
updateTopicName,
} from "./topic-name-cache.js";
describe("topic-name-cache", () => {
beforeEach(() => {
vi.useRealTimers();
clearTopicNameCache();
resetTopicNameCacheForTest();
});
afterEach(() => {
vi.useRealTimers();
});
it("stores and retrieves a topic name", () => {
updateTopicName(-100123, 42, { name: "Deployments" });
expect(getTopicName(-100123, 42)).toBe("Deployments");
});
it("returns undefined for unknown topics", () => {
expect(getTopicName(-100123, 99)).toBeUndefined();
});
it("handles renames via forum_topic_edited (overwrites previous name)", () => {
updateTopicName(-100123, 42, { name: "Deployments" });
updateTopicName(-100123, 42, { name: "CI/CD" });
expect(getTopicName(-100123, 42)).toBe("CI/CD");
});
it("preserves name when patching only closed status", () => {
updateTopicName(-100123, 42, { name: "Deployments" });
updateTopicName(-100123, 42, { closed: true });
expect(getTopicName(-100123, 42)).toBe("Deployments");
expect(getTopicEntry(-100123, 42)?.closed).toBe(true);
});
it("marks topic as reopened", () => {
updateTopicName(-100123, 42, { name: "Deployments", closed: true });
updateTopicName(-100123, 42, { closed: false });
expect(getTopicEntry(-100123, 42)?.closed).toBe(false);
});
it("stores icon metadata", () => {
updateTopicName(-100123, 42, {
name: "Design",
iconColor: 0x6fb9f0,
iconCustomEmojiId: "emoji123",
});
const entry = getTopicEntry(-100123, 42);
expect(entry?.iconColor).toBe(0x6fb9f0);
expect(entry?.iconCustomEmojiId).toBe("emoji123");
});
it("does not store entries with empty name and no prior entry", () => {
updateTopicName(-100123, 42, { closed: true });
expect(getTopicName(-100123, 42)).toBeUndefined();
expect(topicNameCacheSize()).toBe(0);
});
it("updates timestamps on write", async () => {
vi.useFakeTimers();
updateTopicName(-100123, 42, { name: "A" });
const t1 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
await vi.advanceTimersByTimeAsync(10);
updateTopicName(-100123, 42, { name: "B" });
const t2 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
expect(t2).toBeGreaterThan(t1);
});
it("works with string chatId and threadId", () => {
updateTopicName("-100123", "42", { name: "StringKeys" });
expect(getTopicName("-100123", "42")).toBe("StringKeys");
});
it("evicts the oldest entry when cache exceeds 2048", () => {
for (let i = 0; i < 2049; i++) {
updateTopicName(-100000, i, { name: `Topic ${i}` });
}
expect(topicNameCacheSize()).toBe(2048);
expect(getTopicName(-100000, 0)).toBeUndefined();
expect(getTopicName(-100000, 2048)).toBe("Topic 2048");
});
it("refreshes recency on read so active topics survive eviction", async () => {
vi.useFakeTimers();
updateTopicName(-100000, 1, { name: "Active" });
await vi.advanceTimersByTimeAsync(10);
for (let i = 2; i <= 2048; i++) {
updateTopicName(-100000, i, { name: `Topic ${i}` });
}
getTopicName(-100000, 1);
updateTopicName(-100000, 9999, { name: "Newcomer" });
expect(getTopicName(-100000, 1)).toBe("Active");
expect(topicNameCacheSize()).toBe(2048);
});
it("reloads persisted entries from disk", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-topic-cache-"));
const persistedPath = path.join(tempDir, "topic-names.json");
try {
updateTopicName(-100123, 42, { name: "Deployments" }, persistedPath);
resetTopicNameCacheForTest();
expect(getTopicName(-100123, 42, persistedPath)).toBe("Deployments");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
resetTopicNameCacheForTest();
}
});
it("keeps separate in-memory stores for separate persisted paths", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-topic-cache-"));
const firstPath = path.join(tempDir, "first-topic-names.json");
const secondPath = path.join(tempDir, "second-topic-names.json");
try {
updateTopicName(-100123, 42, { name: "Deployments" }, firstPath);
updateTopicName(-200456, 84, { name: "Incidents" }, secondPath);
const readFileSpy = vi.spyOn(syncFs, "readFileSync");
expect(getTopicName(-100123, 42, firstPath)).toBe("Deployments");
expect(getTopicName(-200456, 84, secondPath)).toBe("Incidents");
expect(readFileSpy).not.toHaveBeenCalled();
} finally {
vi.restoreAllMocks();
await fs.rm(tempDir, { recursive: true, force: true });
resetTopicNameCacheForTest();
}
});
});