fix(msteams): bound team id cache expiry

This commit is contained in:
Peter Steinberger
2026-05-30 11:47:00 -04:00
parent 5eb71927b7
commit 0e2694ff47
2 changed files with 54 additions and 6 deletions
@@ -77,6 +77,35 @@ describe("resolveTeamGroupId", () => {
expect(fetchGraphJson).toHaveBeenCalledTimes(1);
});
it("does not cache team ids when the expiry would exceed a valid Date", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(8_640_000_000_000_000));
try {
vi.mocked(fetchGraphJson).mockResolvedValue({ id: "group-guid-boundary" } as never);
await resolveTeamGroupId("tok", "team-boundary");
await resolveTeamGroupId("tok", "team-boundary");
expect(fetchGraphJson).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it("evicts cached team ids when the current clock is invalid", async () => {
vi.mocked(fetchGraphJson).mockResolvedValue({ id: "group-guid-invalid-clock" } as never);
await resolveTeamGroupId("tok", "team-invalid-clock");
const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
try {
await resolveTeamGroupId("tok", "team-invalid-clock");
} finally {
dateNow.mockRestore();
}
expect(fetchGraphJson).toHaveBeenCalledTimes(2);
});
it("falls back to conversationTeamId when Graph returns no id", async () => {
vi.mocked(fetchGraphJson).mockResolvedValueOnce({} as never);
+25 -6
View File
@@ -1,3 +1,7 @@
import {
asDateTimestampMs,
resolveExpiresAtMsFromDurationMs,
} from "openclaw/plugin-sdk/number-runtime";
import { fetchGraphJson, type GraphResponse } from "./graph.js";
export type GraphThreadMessage = {
@@ -14,6 +18,13 @@ export type GraphThreadMessage = {
const teamGroupIdCache = new Map<string, { groupId: string; expiresAt: number }>();
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
function resolveTeamGroupIdCacheExpiresAt(nowRaw = Date.now()): number | undefined {
const now = asDateTimestampMs(nowRaw);
return now === undefined
? undefined
: resolveExpiresAtMsFromDurationMs(CACHE_TTL_MS, { nowMs: now });
}
/**
* Strip HTML tags from Teams message content, preserving @mention display names.
* Teams wraps mentions in <at>Name</at> tags.
@@ -44,8 +55,13 @@ export async function resolveTeamGroupId(
conversationTeamId: string,
): Promise<string> {
const cached = teamGroupIdCache.get(conversationTeamId);
if (cached && cached.expiresAt > Date.now()) {
return cached.groupId;
if (cached) {
const now = asDateTimestampMs(Date.now());
const expiresAt = asDateTimestampMs(cached.expiresAt);
if (now !== undefined && expiresAt !== undefined && expiresAt > now) {
return cached.groupId;
}
teamGroupIdCache.delete(conversationTeamId);
}
// The team ID in channelData is typically the group ID itself for standard teams.
@@ -59,10 +75,13 @@ export async function resolveTeamGroupId(
// Only cache when the Graph lookup succeeds — caching a fallback raw ID
// can cause silent failures for the entire TTL if the ID is not a valid
// Graph team GUID (e.g. Bot Framework conversation key).
teamGroupIdCache.set(conversationTeamId, {
groupId,
expiresAt: Date.now() + CACHE_TTL_MS,
});
const expiresAt = resolveTeamGroupIdCacheExpiresAt();
if (expiresAt !== undefined) {
teamGroupIdCache.set(conversationTeamId, {
groupId,
expiresAt,
});
}
return groupId;
} catch {