Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
406
extensions/imessage/src/monitor-reply-cache.test.ts
Normal file
406
extensions/imessage/src/monitor-reply-cache.test.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
// Imessage tests cover monitor reply cache plugin behavior.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
resetIMessageShortIdState,
|
||||
findLatestIMessageEntryForChat,
|
||||
isKnownFromMeIMessageMessageId,
|
||||
rememberIMessageReplyCache,
|
||||
resolveIMessageMessageId,
|
||||
} from "./monitor-reply-cache.js";
|
||||
import { installIMessageStateRuntimeForTest } from "./test-support/runtime.js";
|
||||
|
||||
beforeEach(() => {
|
||||
installIMessageStateRuntimeForTest();
|
||||
resetIMessageShortIdState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("imessage short message id resolution", () => {
|
||||
it("resolves a short id to a cached message guid", () => {
|
||||
const entry = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "full-guid",
|
||||
chatGuid: "iMessage;+;chat0000",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
expect(entry.shortId).toBe("1");
|
||||
expect(
|
||||
resolveIMessageMessageId("1", {
|
||||
requireKnownShortId: true,
|
||||
chatContext: { chatGuid: "iMessage;+;chat0000" },
|
||||
}),
|
||||
).toBe("full-guid");
|
||||
});
|
||||
|
||||
it("resolves a known short id even without caller-supplied chat scope", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "full-guid",
|
||||
chatGuid: "iMessage;+;chat0000",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// The cached entry already carries chat info; cross-chat checks only
|
||||
// matter when the caller separately provides a (potentially conflicting)
|
||||
// chat scope. A plain known short id from the cache must resolve.
|
||||
expect(resolveIMessageMessageId("1", { requireKnownShortId: true })).toBe("full-guid");
|
||||
});
|
||||
|
||||
it("requires chat scope when a privileged short id is unknown", () => {
|
||||
expect(() => resolveIMessageMessageId("9999", { requireKnownShortId: true })).toThrow(
|
||||
"requires a chat scope",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects short ids from another chat", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "full-guid",
|
||||
chatGuid: "iMessage;+;chat0000",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
resolveIMessageMessageId("1", {
|
||||
requireKnownShortId: true,
|
||||
chatContext: { chatGuid: "iMessage;+;other" },
|
||||
}),
|
||||
).toThrow("belongs to a different chat");
|
||||
});
|
||||
|
||||
it("guards full guid reuse across chats when cached", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "full-guid",
|
||||
chatId: 42,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
expect(() => resolveIMessageMessageId("full-guid", { chatContext: { chatId: 99 } })).toThrow(
|
||||
"belongs to a different chat",
|
||||
);
|
||||
});
|
||||
|
||||
it("recognizes only cached outbound message ids as own messages", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "outbound-guid",
|
||||
chatGuid: "any;-;+12069106512",
|
||||
chatIdentifier: "+12069106512",
|
||||
chatId: 3,
|
||||
timestamp: Date.now(),
|
||||
isFromMe: true,
|
||||
});
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "inbound-guid",
|
||||
chatGuid: "any;-;+12069106512",
|
||||
chatIdentifier: "+12069106512",
|
||||
chatId: 3,
|
||||
timestamp: Date.now(),
|
||||
isFromMe: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
isKnownFromMeIMessageMessageId("outbound-guid", {
|
||||
accountId: "default",
|
||||
chatGuid: "any;-;+12069106512",
|
||||
chatIdentifier: "+12069106512",
|
||||
chatId: 3,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isKnownFromMeIMessageMessageId("inbound-guid", {
|
||||
accountId: "default",
|
||||
chatGuid: "any;-;+12069106512",
|
||||
chatIdentifier: "+12069106512",
|
||||
chatId: 3,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isKnownFromMeIMessageMessageId("outbound-guid", {
|
||||
accountId: "default",
|
||||
chatGuid: "any;-;+12069106514",
|
||||
chatIdentifier: "+12069106514",
|
||||
chatId: 4,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requireFromMe (edit / unsend authorization)", () => {
|
||||
it("rejects a short id resolution when the cached entry came from inbound", () => {
|
||||
// The default inbound recorder sets isFromMe:false (or omits it), so
|
||||
// resolving with requireFromMe must reject — agents cannot edit/unsend
|
||||
// messages that other participants sent.
|
||||
const entry = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "inbound-guid",
|
||||
chatGuid: "iMessage;+;chatA",
|
||||
timestamp: Date.now(),
|
||||
isFromMe: false,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
resolveIMessageMessageId(entry.shortId, {
|
||||
requireKnownShortId: true,
|
||||
chatContext: { chatGuid: "iMessage;+;chatA" },
|
||||
requireFromMe: true,
|
||||
}),
|
||||
).toThrow("not one this agent sent");
|
||||
});
|
||||
|
||||
it("allows a short id resolution when the cached entry was sent by the gateway", () => {
|
||||
const entry = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "outbound-guid",
|
||||
chatGuid: "iMessage;+;chatA",
|
||||
timestamp: Date.now(),
|
||||
isFromMe: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveIMessageMessageId(entry.shortId, {
|
||||
requireKnownShortId: true,
|
||||
chatContext: { chatGuid: "iMessage;+;chatA" },
|
||||
requireFromMe: true,
|
||||
}),
|
||||
).toBe("outbound-guid");
|
||||
});
|
||||
|
||||
it("rejects an uncached full guid under requireFromMe (agent cannot edit/unsend unknown messages)", () => {
|
||||
expect(() =>
|
||||
resolveIMessageMessageId("never-seen-guid", {
|
||||
chatContext: { chatGuid: "iMessage;+;chatA" },
|
||||
requireFromMe: true,
|
||||
}),
|
||||
).toThrow("not one this agent sent");
|
||||
});
|
||||
|
||||
it("rejects when the cached entry has no isFromMe field (older persisted entry, treated as not-from-me)", () => {
|
||||
// Persisted entries written before this option existed do not carry
|
||||
// isFromMe. Treat undefined as the safe default (false) — that pre-
|
||||
// existing-on-disk caller is the inbound recorder, the only writer that
|
||||
// existed before.
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "legacy-guid",
|
||||
chatGuid: "iMessage;+;chatA",
|
||||
timestamp: Date.now(),
|
||||
// isFromMe deliberately omitted
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
resolveIMessageMessageId("legacy-guid", {
|
||||
chatContext: { chatGuid: "iMessage;+;chatA" },
|
||||
requireFromMe: true,
|
||||
}),
|
||||
).toThrow("not one this agent sent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("findLatestIMessageEntryForChat", () => {
|
||||
it("returns the latest entry for the matching chat scope", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "older",
|
||||
chatGuid: "any;-;+12069106512",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: Date.now() - 1000,
|
||||
});
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "newest",
|
||||
chatGuid: "any;-;+12069106512",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = findLatestIMessageEntryForChat({
|
||||
accountId: "default",
|
||||
chatIdentifier: "iMessage;-;+12069106512",
|
||||
});
|
||||
expect(result?.messageId).toBe("newest");
|
||||
});
|
||||
|
||||
it("requires a positive identifier match — no overlap means no fallback", () => {
|
||||
// Cache entry has only chatGuid; caller has only chatId. With the old
|
||||
// isCrossChatMismatch-as-filter, this entry would have been returned
|
||||
// (no overlap → no mismatch → pass). The strict positive-match
|
||||
// semantics require both sides to share at least one identifier kind.
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "different-chat",
|
||||
chatGuid: "iMessage;+;chat0000",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
expect(findLatestIMessageEntryForChat({ accountId: "default", chatId: 99 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("never crosses account boundaries", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "other-account",
|
||||
messageId: "foreign-account",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
expect(
|
||||
findLatestIMessageEntryForChat({
|
||||
accountId: "default",
|
||||
chatIdentifier: "+12069106512",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores entries older than the recency window", () => {
|
||||
const TWELVE_MINUTES_AGO = Date.now() - 12 * 60 * 1000;
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "stale",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: TWELVE_MINUTES_AGO,
|
||||
});
|
||||
|
||||
expect(
|
||||
findLatestIMessageEntryForChat({
|
||||
accountId: "default",
|
||||
chatIdentifier: "+12069106512",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("matches across chat-id-format flavors (iMessage;-;<phone>, any;-;<phone>, bare phone)", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "phone-msg",
|
||||
chatGuid: "any;-;+12069106512",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
for (const ctx of [
|
||||
{ accountId: "default", chatIdentifier: "iMessage;-;+12069106512" },
|
||||
{ accountId: "default", chatIdentifier: "SMS;-;+12069106512" },
|
||||
{ accountId: "default", chatGuid: "any;-;+12069106512" },
|
||||
{ accountId: "default", chatIdentifier: "+12069106512" },
|
||||
]) {
|
||||
const found = findLatestIMessageEntryForChat(ctx);
|
||||
expect(found?.messageId).toBe("phone-msg");
|
||||
}
|
||||
});
|
||||
|
||||
it("requires accountId — refuses to guess across all known chats", () => {
|
||||
rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "anywhere",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// accountId is optional in the signature; calling without it exercises the
|
||||
// runtime guard that returns undefined rather than a cross-account match.
|
||||
expect(findLatestIMessageEntryForChat({ chatIdentifier: "+12069106512" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hydrate-on-resolve (post-restart short-id persistence)", () => {
|
||||
it("hydrates SQLite state before resolving a short id whose mapping predates this run", () => {
|
||||
// Issue-then-restart contract: a shortId we issued before a gateway
|
||||
// restart must still resolve afterwards. The first resolve call after
|
||||
// process boot would otherwise miss the persisted mapping because the
|
||||
// in-memory maps haven't been hydrated yet — that's the bug codex
|
||||
// review flagged. resolveIMessageMessageId now hydrates on entry.
|
||||
const issued = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "outbound-guid-pre-restart",
|
||||
chatGuid: "iMessage;+;chatA",
|
||||
timestamp: Date.now(),
|
||||
isFromMe: true,
|
||||
});
|
||||
expect(issued.shortId).not.toBe("");
|
||||
|
||||
// Simulate a restart: clear only the process-local maps and leave the
|
||||
// SQLite plugin-state rows intact.
|
||||
resetIMessageShortIdState({ clearPersistent: false });
|
||||
|
||||
// Now resolve the short id we issued before the "restart". Without the
|
||||
// hydrate-on-resolve fix this throws "no longer available" because the
|
||||
// in-memory maps are empty and rememberIMessageReplyCache hasn't been
|
||||
// called yet to trigger hydration.
|
||||
expect(
|
||||
resolveIMessageMessageId(issued.shortId, {
|
||||
requireKnownShortId: true,
|
||||
chatContext: { chatGuid: "iMessage;+;chatA" },
|
||||
}),
|
||||
).toBe("outbound-guid-pre-restart");
|
||||
});
|
||||
|
||||
it("persists entries when optional chat fields are explicitly undefined", () => {
|
||||
const issued = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "guid-with-undefined-optionals",
|
||||
chatGuid: undefined,
|
||||
chatIdentifier: undefined,
|
||||
chatId: undefined,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
resetIMessageShortIdState({ clearPersistent: false });
|
||||
|
||||
expect(
|
||||
resolveIMessageMessageId(issued.shortId, {
|
||||
requireKnownShortId: true,
|
||||
chatContext: { chatIdentifier: "+15551234567" },
|
||||
}),
|
||||
).toBe("guid-with-undefined-optionals");
|
||||
});
|
||||
|
||||
it("does not reuse short ids after cached rows expire", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-08T00:00:00Z"));
|
||||
const first = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "old-guid",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
expect(first.shortId).toBe("1");
|
||||
|
||||
vi.setSystemTime(new Date("2026-05-08T07:00:00Z"));
|
||||
resetIMessageShortIdState({ clearPersistent: false });
|
||||
const second = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "new-guid",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
expect(second.shortId).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hydrate counter advancement (rowid-collision protection)", () => {
|
||||
it("advances the short-id counter past a corrupt persisted line so new allocations don't collide", () => {
|
||||
// Direct hydrate isn't easy to invoke without disk fixtures; instead
|
||||
// verify the public contract: after rememberIMessageReplyCache fires,
|
||||
// the next allocation never re-uses an existing live shortId.
|
||||
const a = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "msg-a",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
const b = rememberIMessageReplyCache({
|
||||
accountId: "default",
|
||||
messageId: "msg-b",
|
||||
chatIdentifier: "+12069106512",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
expect(a.shortId).not.toBe(b.shortId);
|
||||
expect(Number.parseInt(b.shortId, 10)).toBeGreaterThan(Number.parseInt(a.shortId, 10));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user