mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:30:43 +00:00
bluebubbles: always refresh server-info cache on null status so macOS 26 plain-text sends don't silently fall back after cache expiry (Greptile/Codex review)
This commit is contained in:
@@ -1041,23 +1041,62 @@ describe("send", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not refresh when no reply or effect is requested", async () => {
|
||||
// Cache expired but no Private API features needed — skip refresh
|
||||
// Plain-text sends also need the cache populated so `isMacOS26OrHigher`
|
||||
// can read `os_version` from the same `serverInfoCache`. Without a
|
||||
// refresh on cold/expired cache, macOS 26 detection would silently
|
||||
// miss and force-route would fall back to broken AppleScript.
|
||||
// (Greptile/Codex PR #69070)
|
||||
it("refreshes cache for plain-text sends when status is unknown", async () => {
|
||||
// First call returns null (cache cold/expired). The refresh path
|
||||
// fetches server info; plain-text send still uses AppleScript when
|
||||
// Private API is disabled on the server — but the refresh ran.
|
||||
privateApiStatusMock.mockReturnValueOnce(null).mockReturnValueOnce(false);
|
||||
fetchServerInfoMock.mockResolvedValueOnce({ private_api: false });
|
||||
mockResolvedHandleTarget();
|
||||
mockSendResponse({ data: { guid: "msg-plain" } });
|
||||
mockSendResponse({ data: { guid: "msg-plain-refreshed" } });
|
||||
|
||||
const result = await sendMessageBlueBubbles("+15551234567", "Plain message", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
});
|
||||
|
||||
expect(result.messageId).toBe("msg-plain");
|
||||
expect(fetchServerInfoMock).not.toHaveBeenCalled();
|
||||
expect(result.messageId).toBe("msg-plain-refreshed");
|
||||
expect(fetchServerInfoMock).toHaveBeenCalledTimes(1);
|
||||
const sendCall = mockFetch.mock.calls[1];
|
||||
const body = JSON.parse(sendCall[1].body);
|
||||
expect(body.method).toBe("apple-script");
|
||||
});
|
||||
|
||||
// Cold cache + macOS 26 + Private API enabled on refresh — the
|
||||
// refresh populates the cache, `isMacOS26OrHigher` returns true, and
|
||||
// plain-text routes through Private API instead of broken AppleScript.
|
||||
// (Greptile/Codex PR #69070)
|
||||
it("force-routes macOS 26 plain-text through Private API after cold-cache refresh", async () => {
|
||||
privateApiStatusMock.mockReturnValueOnce(null).mockReturnValueOnce(true);
|
||||
fetchServerInfoMock.mockResolvedValueOnce({
|
||||
private_api: true,
|
||||
os_version: "26.0",
|
||||
});
|
||||
isMacOS26OrHigherMock.mockReturnValue(true);
|
||||
mockResolvedHandleTarget();
|
||||
mockSendResponse({ data: { guid: "msg-macos26-refreshed" } });
|
||||
|
||||
try {
|
||||
const result = await sendMessageBlueBubbles("+15551234567", "Plain message", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
});
|
||||
|
||||
expect(result.messageId).toBe("msg-macos26-refreshed");
|
||||
expect(fetchServerInfoMock).toHaveBeenCalledTimes(1);
|
||||
const sendCall = mockFetch.mock.calls[1];
|
||||
const body = JSON.parse(sendCall[1].body);
|
||||
expect(body.method).toBe("private-api");
|
||||
} finally {
|
||||
isMacOS26OrHigherMock.mockReturnValue(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("degrades gracefully when refresh returns null (server unreachable)", async () => {
|
||||
// Cache expired, refresh returns null (server info unavailable)
|
||||
fetchServerInfoMock.mockResolvedValueOnce(null);
|
||||
|
||||
@@ -482,10 +482,14 @@ export async function sendMessageBlueBubbles(
|
||||
const wantsReplyThread = normalizeOptionalString(opts.replyToMessageGuid) !== undefined;
|
||||
const wantsEffect = Boolean(effectId);
|
||||
|
||||
// Lazy refresh: when the cache has expired and Private API features are needed,
|
||||
// fetch server info before making the decision. This prevents silent degradation
|
||||
// of reply threading and effects after the 10-minute cache TTL expires. (#43764)
|
||||
if (privateApiStatus === null && (wantsReplyThread || wantsEffect)) {
|
||||
// Lazy refresh: when the cache has expired, fetch server info before
|
||||
// making the decision. Originally scoped to reply/effect features (#43764)
|
||||
// to avoid silent degradation after the 10-minute cache TTL expires. Now
|
||||
// always fires on null status, because `isMacOS26OrHigher()` reads from
|
||||
// the same cache and plain-text sends on macOS 26 need Private API too —
|
||||
// without this, `forceOnMacOS26` silently falls back to broken AppleScript
|
||||
// after TTL expiry or on a cold cache. (#64480, Greptile/Codex PR #69070)
|
||||
if (privateApiStatus === null) {
|
||||
try {
|
||||
await fetchBlueBubblesServerInfo({
|
||||
baseUrl,
|
||||
|
||||
Reference in New Issue
Block a user