fix(bluebubbles): cache prefixed reply context aliases

* fix: BlueBubbles reply-context fallback cache-key regression

* fix(clawsweeper): address review for clawsweeper-commit-openclaw-openclaw-76930da7ebc7 (1)

---------

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-04-30 22:44:21 -07:00
committed by GitHub
parent 1b6f2969aa
commit 173f959613
2 changed files with 72 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { BlueBubblesClient, createBlueBubblesClientFromParts } from "./client.js";
import {
_resetBlueBubblesShortIdState,
getShortIdForUuid,
resolveReplyContextFromCache,
} from "./monitor-reply-cache.js";
import {
@@ -138,6 +139,53 @@ describe("fetchBlueBubblesReplyContext", () => {
expect(requestCalls[0]?.path).toBe("/api/v1/message/msg-bare-guid");
});
it("populates the reply cache for the original prefixed reply id", async () => {
const { factory } = makeFakeClient([
jsonResponse({ data: { text: "cached prefix", handle: { address: "+15551112222" } } }),
]);
await fetchBlueBubblesReplyContext({
...baseParams,
replyToId: "p:0/msg-prefixed-cache",
chatGuid: "iMessage;-;+15551112222",
clientFactory: factory,
});
const cached = resolveReplyContextFromCache({
accountId: "default",
replyToId: "p:0/msg-prefixed-cache",
chatGuid: "iMessage;-;+15551112222",
});
expect(cached?.body).toBe("cached prefix");
expect(cached?.senderLabel).toBe("+15551112222");
});
it("does not cache non-part-index slash prefixes as aliases", async () => {
const { factory, requestCalls } = makeFakeClient([
jsonResponse({ data: { text: "cached bare only", handle: { address: "+15551112222" } } }),
]);
await fetchBlueBubblesReplyContext({
...baseParams,
replyToId: "../etc/passwd",
chatGuid: "iMessage;-;+15551112222",
clientFactory: factory,
});
expect(requestCalls[0]?.path).toBe("/api/v1/message/passwd");
expect(
resolveReplyContextFromCache({
accountId: "default",
replyToId: "passwd",
chatGuid: "iMessage;-;+15551112222",
})?.body,
).toBe("cached bare only");
expect(
resolveReplyContextFromCache({
accountId: "default",
replyToId: "../etc/passwd",
chatGuid: "iMessage;-;+15551112222",
}),
).toBeNull();
expect(getShortIdForUuid("../etc/passwd")).toBeUndefined();
});
it("fetches the BB API and returns body + normalized sender on success", async () => {
const { factory, requestCalls } = makeFakeClient([
jsonResponse({

View File

@@ -17,6 +17,8 @@ const DEFAULT_REPLY_FETCH_TIMEOUT_MS = 5_000;
// punctuation set below; 128 chars is comfortable headroom (CWE-20).
const REPLY_TO_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
const REPLY_TO_ID_MAX_LENGTH = 128;
const PART_INDEX_REPLY_TO_ID_PATTERN = /^p:\d{1,10}\/([A-Za-z0-9._:-]+)$/;
const PART_INDEX_REPLY_TO_ID_MAX_LENGTH = REPLY_TO_ID_MAX_LENGTH + "p:".length + 10 + "/".length;
export type BlueBubblesReplyFetchResult = {
body?: string;
@@ -110,6 +112,18 @@ function sanitizeReplyToId(raw: string): string | null {
return bare;
}
function normalizePartIndexReplyToIdAlias(raw: string, bareReplyToId: string): string | null {
const trimmed = raw.trim();
if (trimmed.length > PART_INDEX_REPLY_TO_ID_MAX_LENGTH) {
return null;
}
const match = PART_INDEX_REPLY_TO_ID_PATTERN.exec(trimmed);
if (!match || match[1] !== bareReplyToId) {
return null;
}
return trimmed;
}
async function runFetch(
params: FetchBlueBubblesReplyContextParams,
replyToId: string,
@@ -152,7 +166,7 @@ async function runFetch(
if (!body && !sender) {
return null;
}
rememberBlueBubblesReplyCache({
const cacheEntry = {
accountId: params.accountId,
messageId: replyToId,
chatGuid: params.chatGuid,
@@ -161,7 +175,15 @@ async function runFetch(
senderLabel: sender,
body,
timestamp: Date.now(),
});
};
rememberBlueBubblesReplyCache(cacheEntry);
const partIndexReplyToId = normalizePartIndexReplyToIdAlias(params.replyToId, replyToId);
if (partIndexReplyToId) {
rememberBlueBubblesReplyCache({
...cacheEntry,
messageId: partIndexReplyToId,
});
}
return { body, sender };
} catch {
// Best-effort: swallow network/parse errors. Caller proceeds with empty