mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:20:43 +00:00
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
141 lines
4.9 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|