



























@@ -1,140 +1,145 @@
1-import syncFs from "node:fs";
2-import fs from "node:fs/promises";
3-import os from "node:os";
4-import path from "node:path";
51import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
62import {
73clearTopicNameCache,
84getTopicEntry,
95getTopicName,
106resetTopicNameCacheForTest,
7+setTelegramTopicNameStoreFactoryForTest,
118topicNameCacheSize,
129updateTopicName,
1310} from "./topic-name-cache.js";
141112+type TopicEntry = NonNullable<Awaited<ReturnType<typeof getTopicEntry>>>;
13+14+function installMemoryStores() {
15+const stores = new Map<string, Map<string, TopicEntry>>();
16+setTelegramTopicNameStoreFactoryForTest((namespace) => {
17+const entries = stores.get(namespace) ?? new Map<string, TopicEntry>();
18+stores.set(namespace, entries);
19+return {
20+async register(key, value) {
21+entries.set(key, value);
22+},
23+async entries() {
24+return Array.from(entries, ([key, value]) => ({ key, value }));
25+},
26+async delete(key) {
27+return entries.delete(key);
28+},
29+async clear() {
30+entries.clear();
31+},
32+};
33+});
34+return stores;
35+}
36+1537describe("topic-name-cache", () => {
16-beforeEach(() => {
38+beforeEach(async () => {
1739vi.useRealTimers();
18-clearTopicNameCache();
40+installMemoryStores();
41+await clearTopicNameCache();
1942resetTopicNameCacheForTest();
2043});
21442245afterEach(() => {
2346vi.useRealTimers();
47+setTelegramTopicNameStoreFactoryForTest(undefined);
2448});
254926-it("stores and retrieves a topic name", () => {
27-updateTopicName(-100123, 42, { name: "Deployments" });
28-expect(getTopicName(-100123, 42)).toBe("Deployments");
50+it("stores and retrieves a topic name", async () => {
51+await updateTopicName(-100123, 42, { name: "Deployments" });
52+await expect(getTopicName(-100123, 42)).resolves.toBe("Deployments");
2953});
305431-it("returns undefined for unknown topics", () => {
32-expect(getTopicName(-100123, 99)).toBeUndefined();
55+it("returns undefined for unknown topics", async () => {
56+await expect(getTopicName(-100123, 99)).resolves.toBeUndefined();
3357});
345835-it("handles renames via forum_topic_edited (overwrites previous name)", () => {
36-updateTopicName(-100123, 42, { name: "Deployments" });
37-updateTopicName(-100123, 42, { name: "CI/CD" });
38-expect(getTopicName(-100123, 42)).toBe("CI/CD");
59+it("handles renames via forum_topic_edited", async () => {
60+await updateTopicName(-100123, 42, { name: "Deployments" });
61+await updateTopicName(-100123, 42, { name: "CI/CD" });
62+await expect(getTopicName(-100123, 42)).resolves.toBe("CI/CD");
3963});
406441-it("preserves name when patching only closed status", () => {
42-updateTopicName(-100123, 42, { name: "Deployments" });
43-updateTopicName(-100123, 42, { closed: true });
44-expect(getTopicName(-100123, 42)).toBe("Deployments");
45-expect(getTopicEntry(-100123, 42)?.closed).toBe(true);
65+it("preserves name when patching only closed status", async () => {
66+await updateTopicName(-100123, 42, { name: "Deployments" });
67+await updateTopicName(-100123, 42, { closed: true });
68+await expect(getTopicName(-100123, 42)).resolves.toBe("Deployments");
69+expect((await getTopicEntry(-100123, 42))?.closed).toBe(true);
4670});
477148-it("marks topic as reopened", () => {
49-updateTopicName(-100123, 42, { name: "Deployments", closed: true });
50-updateTopicName(-100123, 42, { closed: false });
51-expect(getTopicEntry(-100123, 42)?.closed).toBe(false);
72+it("marks topic as reopened", async () => {
73+await updateTopicName(-100123, 42, { name: "Deployments", closed: true });
74+await updateTopicName(-100123, 42, { closed: false });
75+expect((await getTopicEntry(-100123, 42))?.closed).toBe(false);
5276});
537754-it("stores icon metadata", () => {
55-updateTopicName(-100123, 42, {
78+it("stores icon metadata", async () => {
79+await updateTopicName(-100123, 42, {
5680name: "Design",
5781iconColor: 0x6fb9f0,
5882iconCustomEmojiId: "emoji123",
5983});
60-const entry = getTopicEntry(-100123, 42);
84+const entry = await getTopicEntry(-100123, 42);
6185expect(entry?.iconColor).toBe(0x6fb9f0);
6286expect(entry?.iconCustomEmojiId).toBe("emoji123");
6387});
648865-it("does not store entries with empty name and no prior entry", () => {
66-updateTopicName(-100123, 42, { closed: true });
67-expect(getTopicName(-100123, 42)).toBeUndefined();
89+it("does not store entries with empty name and no prior entry", async () => {
90+await updateTopicName(-100123, 42, { closed: true });
91+await expect(getTopicName(-100123, 42)).resolves.toBeUndefined();
6892expect(topicNameCacheSize()).toBe(0);
6993});
70947195it("updates timestamps on write", async () => {
7296vi.useFakeTimers();
73-updateTopicName(-100123, 42, { name: "A" });
74-const t1 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
97+await updateTopicName(-100123, 42, { name: "A" });
98+const t1 = (await getTopicEntry(-100123, 42))?.updatedAt ?? 0;
7599await vi.advanceTimersByTimeAsync(10);
76-updateTopicName(-100123, 42, { name: "B" });
77-const t2 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
100+await updateTopicName(-100123, 42, { name: "B" });
101+const t2 = (await getTopicEntry(-100123, 42))?.updatedAt ?? 0;
78102expect(t2).toBeGreaterThan(t1);
79103});
8010481-it("works with string chatId and threadId", () => {
82-updateTopicName("-100123", "42", { name: "StringKeys" });
83-expect(getTopicName("-100123", "42")).toBe("StringKeys");
105+it("works with string chatId and threadId", async () => {
106+await updateTopicName("-100123", "42", { name: "StringKeys" });
107+await expect(getTopicName("-100123", "42")).resolves.toBe("StringKeys");
84108});
8510986-it("evicts the oldest entry when cache exceeds 2048", () => {
110+it("evicts the oldest entry when cache exceeds 2048", async () => {
87111for (let i = 0; i < 2049; i++) {
88-updateTopicName(-100000, i, { name: `Topic ${i}` });
112+await updateTopicName(-100000, i, { name: `Topic ${i}` });
89113}
90114expect(topicNameCacheSize()).toBe(2048);
91-expect(getTopicName(-100000, 0)).toBeUndefined();
92-expect(getTopicName(-100000, 2048)).toBe("Topic 2048");
115+await expect(getTopicName(-100000, 0)).resolves.toBeUndefined();
116+await expect(getTopicName(-100000, 2048)).resolves.toBe("Topic 2048");
93117});
9411895119it("refreshes recency on read so active topics survive eviction", async () => {
96120vi.useFakeTimers();
97-updateTopicName(-100000, 1, { name: "Active" });
121+await updateTopicName(-100000, 1, { name: "Active" });
98122await vi.advanceTimersByTimeAsync(10);
99123for (let i = 2; i <= 2048; i++) {
100-updateTopicName(-100000, i, { name: `Topic ${i}` });
124+await updateTopicName(-100000, i, { name: `Topic ${i}` });
101125}
102-getTopicName(-100000, 1);
103-updateTopicName(-100000, 9999, { name: "Newcomer" });
104-expect(getTopicName(-100000, 1)).toBe("Active");
126+await getTopicName(-100000, 1);
127+await updateTopicName(-100000, 9999, { name: "Newcomer" });
128+await expect(getTopicName(-100000, 1)).resolves.toBe("Active");
105129expect(topicNameCacheSize()).toBe(2048);
106130});
107131108-it("reloads persisted entries from disk", async () => {
109-const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-topic-cache-"));
110-const persistedPath = path.join(tempDir, "topic-names.json");
111-try {
112-updateTopicName(-100123, 42, { name: "Deployments" }, persistedPath);
113-resetTopicNameCacheForTest();
114-expect(getTopicName(-100123, 42, persistedPath)).toBe("Deployments");
115-} finally {
116-await fs.rm(tempDir, { recursive: true, force: true });
117-resetTopicNameCacheForTest();
118-}
132+it("reloads persisted entries from plugin state", async () => {
133+await updateTopicName(-100123, 42, { name: "Deployments" }, "first");
134+resetTopicNameCacheForTest();
135+await expect(getTopicName(-100123, 42, "first")).resolves.toBe("Deployments");
119136});
120137121-it("keeps separate in-memory stores for separate persisted paths", async () => {
122-const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-topic-cache-"));
123-const firstPath = path.join(tempDir, "first-topic-names.json");
124-const secondPath = path.join(tempDir, "second-topic-names.json");
125-try {
126-updateTopicName(-100123, 42, { name: "Deployments" }, firstPath);
127-updateTopicName(-200456, 84, { name: "Incidents" }, secondPath);
128-129-const readFileSpy = vi.spyOn(syncFs, "readFileSync");
138+it("keeps separate stores for separate scopes", async () => {
139+await updateTopicName(-100123, 42, { name: "Deployments" }, "first");
140+await updateTopicName(-200456, 84, { name: "Incidents" }, "second");
130141131-expect(getTopicName(-100123, 42, firstPath)).toBe("Deployments");
132-expect(getTopicName(-200456, 84, secondPath)).toBe("Incidents");
133-expect(readFileSpy).not.toHaveBeenCalled();
134-} finally {
135-vi.restoreAllMocks();
136-await fs.rm(tempDir, { recursive: true, force: true });
137-resetTopicNameCacheForTest();
138-}
142+await expect(getTopicName(-100123, 42, "first")).resolves.toBe("Deployments");
143+await expect(getTopicName(-200456, 84, "second")).resolves.toBe("Incidents");
139144});
140145});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。