mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-19 11:11:38 +00:00
177 lines
5.9 KiB
TypeScript
177 lines
5.9 KiB
TypeScript
// Telegram tests cover topic name cache plugin behavior.
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
getTopicName,
|
|
resetTopicNameCacheForTest,
|
|
setTelegramTopicNameStoreFactoryForTest,
|
|
updateTopicName,
|
|
} from "./topic-name-cache.js";
|
|
|
|
type TopicEntry = {
|
|
name: string;
|
|
iconColor?: number;
|
|
iconCustomEmojiId?: string;
|
|
closed?: boolean;
|
|
updatedAt: number;
|
|
};
|
|
|
|
function topicKey(chatId: number | string, threadId: number | string): string {
|
|
return `${chatId}:${threadId}`;
|
|
}
|
|
|
|
function getStoredTopicEntry(
|
|
stores: Map<string, Map<string, TopicEntry>>,
|
|
chatId: number | string,
|
|
threadId: number | string,
|
|
): TopicEntry | undefined {
|
|
const key = topicKey(chatId, threadId);
|
|
for (const entries of stores.values()) {
|
|
const entry = entries.get(key);
|
|
if (entry) {
|
|
return entry;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function topicStoreSize(stores: Map<string, Map<string, TopicEntry>>): number {
|
|
return Array.from(stores.values(), (entries) => entries.size).reduce(
|
|
(total, size) => total + size,
|
|
0,
|
|
);
|
|
}
|
|
|
|
function installMemoryStores() {
|
|
const stores = new Map<string, Map<string, TopicEntry>>();
|
|
setTelegramTopicNameStoreFactoryForTest((namespace) => {
|
|
const entries = stores.get(namespace) ?? new Map<string, TopicEntry>();
|
|
stores.set(namespace, entries);
|
|
return {
|
|
async register(key, value) {
|
|
entries.set(key, value);
|
|
},
|
|
async entries() {
|
|
return Array.from(entries, ([key, value]) => ({ key, value }));
|
|
},
|
|
async delete(key) {
|
|
return entries.delete(key);
|
|
},
|
|
async clear() {
|
|
entries.clear();
|
|
},
|
|
};
|
|
});
|
|
return stores;
|
|
}
|
|
|
|
describe("topic-name-cache", () => {
|
|
let stores: Map<string, Map<string, TopicEntry>>;
|
|
|
|
beforeEach(async () => {
|
|
vi.useRealTimers();
|
|
stores = installMemoryStores();
|
|
resetTopicNameCacheForTest();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
setTelegramTopicNameStoreFactoryForTest(undefined);
|
|
});
|
|
|
|
it("stores and retrieves a topic name", async () => {
|
|
await updateTopicName(-100123, 42, { name: "Deployments" });
|
|
await expect(getTopicName(-100123, 42)).resolves.toBe("Deployments");
|
|
});
|
|
|
|
it("returns undefined for unknown topics", async () => {
|
|
await expect(getTopicName(-100123, 99)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("handles renames via forum_topic_edited", async () => {
|
|
await updateTopicName(-100123, 42, { name: "Deployments" });
|
|
await updateTopicName(-100123, 42, { name: "CI/CD" });
|
|
await expect(getTopicName(-100123, 42)).resolves.toBe("CI/CD");
|
|
});
|
|
|
|
it("preserves name when patching only closed status", async () => {
|
|
await updateTopicName(-100123, 42, { name: "Deployments" });
|
|
await updateTopicName(-100123, 42, { closed: true });
|
|
await expect(getTopicName(-100123, 42)).resolves.toBe("Deployments");
|
|
expect(getStoredTopicEntry(stores, -100123, 42)?.closed).toBe(true);
|
|
});
|
|
|
|
it("marks topic as reopened", async () => {
|
|
await updateTopicName(-100123, 42, { name: "Deployments", closed: true });
|
|
await updateTopicName(-100123, 42, { closed: false });
|
|
expect(getStoredTopicEntry(stores, -100123, 42)?.closed).toBe(false);
|
|
});
|
|
|
|
it("stores icon metadata", async () => {
|
|
await updateTopicName(-100123, 42, {
|
|
name: "Design",
|
|
iconColor: 0x6fb9f0,
|
|
iconCustomEmojiId: "emoji123",
|
|
});
|
|
const entry = getStoredTopicEntry(stores, -100123, 42);
|
|
expect(entry?.iconColor).toBe(0x6fb9f0);
|
|
expect(entry?.iconCustomEmojiId).toBe("emoji123");
|
|
});
|
|
|
|
it("does not store entries with empty name and no prior entry", async () => {
|
|
await updateTopicName(-100123, 42, { closed: true });
|
|
await expect(getTopicName(-100123, 42)).resolves.toBeUndefined();
|
|
expect(topicStoreSize(stores)).toBe(0);
|
|
});
|
|
|
|
it("updates timestamps on write", async () => {
|
|
vi.useFakeTimers();
|
|
await updateTopicName(-100123, 42, { name: "A" });
|
|
const t1 = getStoredTopicEntry(stores, -100123, 42)?.updatedAt ?? 0;
|
|
await vi.advanceTimersByTimeAsync(10);
|
|
await updateTopicName(-100123, 42, { name: "B" });
|
|
const t2 = getStoredTopicEntry(stores, -100123, 42)?.updatedAt ?? 0;
|
|
expect(t2).toBeGreaterThan(t1);
|
|
});
|
|
|
|
it("works with string chatId and threadId", async () => {
|
|
await updateTopicName("-100123", "42", { name: "StringKeys" });
|
|
await expect(getTopicName("-100123", "42")).resolves.toBe("StringKeys");
|
|
});
|
|
|
|
it("evicts the oldest entry when cache exceeds 2048", async () => {
|
|
for (let i = 0; i < 2049; i++) {
|
|
await updateTopicName(-100000, i, { name: `Topic ${i}` });
|
|
}
|
|
expect(topicStoreSize(stores)).toBe(2048);
|
|
await expect(getTopicName(-100000, 0)).resolves.toBeUndefined();
|
|
await expect(getTopicName(-100000, 2048)).resolves.toBe("Topic 2048");
|
|
});
|
|
|
|
it("refreshes recency on read so active topics survive eviction", async () => {
|
|
vi.useFakeTimers();
|
|
await updateTopicName(-100000, 1, { name: "Active" });
|
|
await vi.advanceTimersByTimeAsync(10);
|
|
for (let i = 2; i <= 2048; i++) {
|
|
await updateTopicName(-100000, i, { name: `Topic ${i}` });
|
|
}
|
|
await getTopicName(-100000, 1);
|
|
await updateTopicName(-100000, 9999, { name: "Newcomer" });
|
|
await expect(getTopicName(-100000, 1)).resolves.toBe("Active");
|
|
expect(topicStoreSize(stores)).toBe(2048);
|
|
});
|
|
|
|
it("reloads persisted entries from plugin state", async () => {
|
|
await updateTopicName(-100123, 42, { name: "Deployments" }, "first");
|
|
resetTopicNameCacheForTest();
|
|
await expect(getTopicName(-100123, 42, "first")).resolves.toBe("Deployments");
|
|
});
|
|
|
|
it("keeps separate stores for separate scopes", async () => {
|
|
await updateTopicName(-100123, 42, { name: "Deployments" }, "first");
|
|
await updateTopicName(-200456, 84, { name: "Incidents" }, "second");
|
|
|
|
await expect(getTopicName(-100123, 42, "first")).resolves.toBe("Deployments");
|
|
await expect(getTopicName(-200456, 84, "second")).resolves.toBe("Incidents");
|
|
});
|
|
});
|