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: 0 x6fb9f0,
iconCustomEmojiId: "emoji123" ,
});
const entry = getTopicEntry(-100123 , 42 );
expect(entry?.iconColor).toBe(0 x6fb9f0);
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();
}
});
});
Messung V0.5 in Prozent C=93 H=90 G=91
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland