From bf690507bfed62f768d60f11fece8f57f931cef2 Mon Sep 17 00:00:00 2001 From: Shadow Date: Thu, 12 Feb 2026 16:31:05 -0600 Subject: [PATCH] fix: cover discord thread starter cache TTL (#5274) (thanks @webvijayi) --- CHANGELOG.md | 1 + src/discord/monitor/threading.test.ts | 48 +++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c984ab53..863f49f9f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - Agents: prevent file descriptor leaks in child process cleanup. (#13565) Thanks @KyleChen26. - Agents: prevent double compaction caused by cache TTL bypassing guard. (#13514) Thanks @taw0002. - Agents: use last API call's cache tokens for context display instead of accumulated sum. (#13805) Thanks @akari-musubi. +- Discord: add TTL/LRU eviction to the thread starter cache to prevent unbounded growth. (#5274) Thanks @webvijayi. - Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale. - Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik. diff --git a/src/discord/monitor/threading.test.ts b/src/discord/monitor/threading.test.ts index b4a1c42f6c..6c36c1bc9c 100644 --- a/src/discord/monitor/threading.test.ts +++ b/src/discord/monitor/threading.test.ts @@ -1,10 +1,12 @@ -import type { Client } from "@buape/carbon"; -import { describe, expect, it } from "vitest"; +import { ChannelType, type Client } from "@buape/carbon"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { buildAgentSessionKey } from "../../routing/resolve-route.js"; import { + __resetDiscordThreadStarterCacheForTest, resolveDiscordAutoThreadContext, resolveDiscordAutoThreadReplyPlan, resolveDiscordReplyDeliveryPlan, + resolveDiscordThreadStarter, } from "./threading.js"; describe("resolveDiscordAutoThreadContext", () => { @@ -142,3 +144,45 @@ describe("resolveDiscordAutoThreadReplyPlan", () => { expect(plan.autoThreadContext).toBeNull(); }); }); + +describe("resolveDiscordThreadStarter cache", () => { + afterEach(() => { + vi.useRealTimers(); + __resetDiscordThreadStarterCacheForTest(); + }); + + it("expires cached entries after TTL", async () => { + vi.useFakeTimers(); + const baseTime = new Date("2026-02-12T00:00:00Z").getTime(); + vi.setSystemTime(baseTime); + + const restGet = vi.fn(async () => ({ + content: "starter", + author: { username: "starter", id: "user-1" }, + timestamp: "2026-02-12T00:00:00Z", + })); + const client = { rest: { get: restGet } } as unknown as Client; + + const params = { + channel: { id: "thread-1" }, + client, + parentId: "parent-1", + parentType: ChannelType.GuildText, + resolveTimestampMs: () => baseTime, + }; + + const first = await resolveDiscordThreadStarter(params); + expect(first?.text).toBe("starter"); + expect(restGet).toHaveBeenCalledTimes(1); + + vi.setSystemTime(baseTime + 60_000); + const second = await resolveDiscordThreadStarter(params); + expect(second).toEqual(first); + expect(restGet).toHaveBeenCalledTimes(1); + + vi.setSystemTime(baseTime + 60_000 + 5 * 60_000 + 1); + const third = await resolveDiscordThreadStarter(params); + expect(third).toEqual(first); + expect(restGet).toHaveBeenCalledTimes(2); + }); +});