@mapier/imsg-sdk 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gateway/chat-background.d.ts +40 -0
- package/dist/gateway/chat-background.js +219 -0
- package/dist/gateway/chat-background.js.map +1 -0
- package/dist/gateway/edit-message.d.ts +17 -0
- package/dist/gateway/edit-message.js +70 -0
- package/dist/gateway/edit-message.js.map +1 -0
- package/dist/gateway/external-ids.d.ts +1 -0
- package/dist/gateway/external-ids.js +3 -1
- package/dist/gateway/external-ids.js.map +1 -1
- package/dist/gateway/fake.d.ts +13 -4
- package/dist/gateway/fake.js +82 -9
- package/dist/gateway/fake.js.map +1 -1
- package/dist/gateway/imsg.d.ts +6 -4
- package/dist/gateway/imsg.js +98 -33
- package/dist/gateway/imsg.js.map +1 -1
- package/dist/gateway/send-rich.d.ts +17 -0
- package/dist/gateway/send-rich.js +43 -0
- package/dist/gateway/send-rich.js.map +1 -0
- package/dist/gateway/types.d.ts +77 -5
- package/dist/gateway/types.js +165 -0
- package/dist/gateway/types.js.map +1 -1
- package/dist/imsg/rpc.d.ts +33 -2
- package/dist/imsg/rpc.js +31 -3
- package/dist/imsg/rpc.js.map +1 -1
- package/dist/imsg/status.js +17 -0
- package/dist/imsg/status.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/docs/api.md +1 -0
- package/docs/bridge-contract.md +7 -3
- package/docs/capability-matrix.md +4 -1
- package/docs/compatibility.md +4 -1
- package/docs/gateway-contract.md +271 -13
- package/docs/imcore_selectors.md +634 -0
- package/package.json +1 -1
package/docs/gateway-contract.md
CHANGED
|
@@ -57,6 +57,10 @@ interface Gateway {
|
|
|
57
57
|
groupPhoto: boolean;
|
|
58
58
|
groupParticipants: boolean;
|
|
59
59
|
namePhotoSharing: boolean;
|
|
60
|
+
chatBackgroundSet: boolean;
|
|
61
|
+
chatBackgroundRemove: boolean;
|
|
62
|
+
editMessage: boolean;
|
|
63
|
+
mentionFormatting: boolean;
|
|
60
64
|
};
|
|
61
65
|
subscribe(sinceId?: number): AsyncIterable<GatewayEvent>;
|
|
62
66
|
history(chatId: number, limit?: number): Promise<ImsgMessage[]>;
|
|
@@ -91,7 +95,11 @@ interface Gateway {
|
|
|
91
95
|
): Promise<SendResult>;
|
|
92
96
|
sendPoll(chatId: number, question: string, options: string[]): Promise<SendResult>;
|
|
93
97
|
sendRichLink(chatId: number, url: string): Promise<SendResult>;
|
|
94
|
-
editMessage(chatId: number, targetGuid: string, text: string): Promise<{
|
|
98
|
+
editMessage(chatId: number, targetGuid: string, text: string): Promise<{
|
|
99
|
+
ok: boolean;
|
|
100
|
+
skipped?: 'unchanged';
|
|
101
|
+
effectStarted?: boolean;
|
|
102
|
+
}>;
|
|
95
103
|
unsendMessage(chatId: number, targetGuid: string): Promise<{ ok: boolean }>;
|
|
96
104
|
deleteMessage(chatId: number, targetGuid: string): Promise<{ ok: boolean }>;
|
|
97
105
|
setTyping(chatId: number, on: boolean): Promise<{ ok: boolean }>;
|
|
@@ -106,6 +114,21 @@ interface Gateway {
|
|
|
106
114
|
skipped?: 'not-offered';
|
|
107
115
|
effectStarted?: boolean;
|
|
108
116
|
}>;
|
|
117
|
+
chatBackgroundStatus(chatId: number): Promise<{
|
|
118
|
+
chatId: number;
|
|
119
|
+
backgroundSet: boolean;
|
|
120
|
+
backgroundGuid: string | null;
|
|
121
|
+
} | null>;
|
|
122
|
+
setChatBackground(chatId: number, preset?: 'gradient'): Promise<{
|
|
123
|
+
ok: boolean;
|
|
124
|
+
backgroundGuid?: string;
|
|
125
|
+
effectStarted?: boolean;
|
|
126
|
+
}>;
|
|
127
|
+
removeChatBackground(chatId: number, expectedGuid?: string): Promise<{
|
|
128
|
+
ok: boolean;
|
|
129
|
+
skipped?: 'no-background';
|
|
130
|
+
effectStarted?: boolean;
|
|
131
|
+
}>;
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
interface ReactionNote {
|
|
@@ -160,7 +183,16 @@ both the long-lived RPC process and its one-time `status --json` probe.
|
|
|
160
183
|
Patched verbs require the injected bridge's explicit selector markers
|
|
161
184
|
`emojiTapbackSend`, `stickerSend`, `groupPhotoUpdate`, `groupAddParticipant`, and
|
|
162
185
|
`groupRemoveParticipant`; Name & Photo requires both `namePhotoShouldOffer`
|
|
163
|
-
and `namePhotoShare`;
|
|
186
|
+
and `namePhotoShare`; chat backgrounds are gated per verb on
|
|
187
|
+
`chatBackgroundSetGradient` (setter + PosterBoard/ChatKit runtime) and
|
|
188
|
+
`chatBackgroundRemove` (setter + live-GUID getter), which the injected helper
|
|
189
|
+
already composes; edit message requires any one of `editMessageItemTranslation`
|
|
190
|
+
(macOS 27's five-argument `-[IMChat editMessageItem:…newPartTranslation:…]`,
|
|
191
|
+
imsg#3), `editMessageItem` (macOS 26) or `editMessage` (macOS 13) — the helper
|
|
192
|
+
uses the first present one; confirmed @mentions on `sendRich` require
|
|
193
|
+
`mentionFormatting`, which the helper advertises only when IMSharedUtilities'
|
|
194
|
+
confirmed-mention attribute key resolves at runtime (imsg#13); emoji
|
|
195
|
+
additionally requires the selected RPC binary
|
|
164
196
|
to report `rpc_features:["tapback.emoji"]`. That second half prevents a
|
|
165
197
|
patched dylib behind a stock CLI from false-advertising custom emoji support.
|
|
166
198
|
Missing status, malformed JSON, or absent markers fail closed.
|
|
@@ -175,10 +207,11 @@ guid by scanning `history(chatId, 30)` — the same 30-message ceiling
|
|
|
175
207
|
`recentReactions` already documents above — so a guid older than that is as
|
|
176
208
|
invisible to these methods as it is to `recentReactions`. `tapback` and
|
|
177
209
|
`deleteMessage` pre-check against that scan before firing, so they return
|
|
178
|
-
`{ ok: false }` without touching anything
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
`{ ok: false }` without touching anything, and so does `editMessage` (it also
|
|
211
|
+
refuses a message that is not `is_from_me`); `unsendMessage` fires the RPC
|
|
212
|
+
first and only finds the ceiling on the VERIFY pass, so a guid outside the
|
|
213
|
+
window still reports `{ ok: false }` but may have mutated the real row on the
|
|
214
|
+
way there — "no confirmable effect," not a guaranteed no-op. FakeGateway
|
|
182
215
|
must enforce the identical window regardless: finding the target guid by
|
|
183
216
|
scanning ALL of `this.messages` unconditionally would let the fake succeed
|
|
184
217
|
at targeting a message the real path can't reach (parity rule 2) — a
|
|
@@ -219,6 +252,46 @@ prevent.
|
|
|
219
252
|
this Gateway because `ImsgMessage` has no subject field. The `url` submode is
|
|
220
253
|
exposed separately as `sendRichLink` because openclaw treats URL and
|
|
221
254
|
text/subject/effect/reply as mutually exclusive modes.
|
|
255
|
+
`opts.textFormatting` is a list of ranges over the text in **UTF-16 code
|
|
256
|
+
units** (a JS string index; an emoji is two units): `styles`
|
|
257
|
+
(bold/italic/underline/strikethrough, macOS 15+, silently dropped on older
|
|
258
|
+
hosts), `effect` (a named macOS 27 text effect, resolved to Apple's id in
|
|
259
|
+
`resolveTextFormatting`), and `mention` — a **confirmed @mention**
|
|
260
|
+
(imsg#13, merged into `mapier/deploy` as `7c31772` — the pinned submodule
|
|
261
|
+
— `docs/send.md` "Text formatting and mentions"; the rules below mirror
|
|
262
|
+
`IMsgMentionFormatting.m` at that revision). `mention` is the EXACT `IMHandle.ID` of a participant of the
|
|
263
|
+
target chat as IMCore reports it (E.164 phone or the email), matched
|
|
264
|
+
byte-for-byte by the helper against the live participant list; the visible
|
|
265
|
+
text in the range must be the person's display name, never the handle (a
|
|
266
|
+
handle as visible text renders as a phone link with no highlight and no
|
|
267
|
+
notification, verified 2026-09-03 on an iPhone). Do not prefix the name
|
|
268
|
+
with "@": Messages' composer swallows the typed "@" and a genuine mention
|
|
269
|
+
row carries only the highlighted name (an "@" in the range simply renders
|
|
270
|
+
as text, as the 2026-09-03 SDK run showed). Formatting is **never
|
|
271
|
+
degraded**: the shared preflight `sendRichPreflight` /
|
|
272
|
+
`textFormattingRefusal` runs before the bridge is touched and refuses, with
|
|
273
|
+
a typed `SendResult.refused`, a mention range on a bridge without the
|
|
274
|
+
`mentionFormatting` marker (`mention-capability`), bad offsets on ANY range
|
|
275
|
+
— non-integer, zero length, outside the text, or splitting a surrogate pair
|
|
276
|
+
(`text-formatting-range`; the helper would otherwise silently drop a style
|
|
277
|
+
range and send plain text, which a caller cannot detect since formatting
|
|
278
|
+
is write-only), and a mention identifier that is not a bounded id or whose
|
|
279
|
+
visible text is the handle itself (`mention-identifier`). The helper
|
|
280
|
+
refuses a non-participant handle (`-32603` "Mention identifier is not a
|
|
281
|
+
participant handle of this chat", no row written), which `ImsgGateway`
|
|
282
|
+
classifies as `mention-not-participant`; a chat whose live participant
|
|
283
|
+
list is unreadable refuses every mention the same way. FakeGateway applies
|
|
284
|
+
the same preflight, checks membership against its chat record, and refuses
|
|
285
|
+
all mentions on an unreported-directory group. **A bare `ok:false` without
|
|
286
|
+
`refused` is NOT a no-send guarantee** (bridge timeout, transport loss);
|
|
287
|
+
only a `refused` result proves nothing was sent. Formatting and mentions
|
|
288
|
+
are write-only through this Gateway: `ImsgMessage` exposes neither, so the
|
|
289
|
+
smoke reads the confirmed-mention attribute (`__kIMMentionConfirmedMention`
|
|
290
|
+
followed by the handle) from `chat.db message.attributedBody` directly. The
|
|
291
|
+
`--only-mention` leg is opt-in via `--mention-handle` even in the full
|
|
292
|
+
Tier-2 run, because a confirmed mention notifies a real person.
|
|
293
|
+
**Host-verified 2026-09-03** through `ImsgGateway` on the shared Mac,
|
|
294
|
+
including the recipient-side highlight on an iPhone (see the ledger).
|
|
222
295
|
- **`sendAttachment(chatId, filePath, opts?)`** — `send.attachment`
|
|
223
296
|
(`handleSendAttachment`). Targets an **existing** chat only. `opts.audio:true`
|
|
224
297
|
sends the file as a native voice-note bubble; `opts.replyToGuid` sets an
|
|
@@ -265,8 +338,44 @@ prevent.
|
|
|
265
338
|
balloon lands as its own row later. `SendResult.guid` is therefore always
|
|
266
339
|
undefined here; FakeGateway must not return one either.
|
|
267
340
|
- **`editMessage(chatId, targetGuid, text)`** — `message.edit`
|
|
268
|
-
(`handleMessageEdit`).
|
|
269
|
-
|
|
341
|
+
(`handleMessageEdit`; imsg `docs/edit.md`). Capability-gated on the
|
|
342
|
+
`editMessage` marker (see above): on macOS 27 the helper calls the
|
|
343
|
+
five-argument `editMessageItem:atPartIndex:withNewPartText:newPartTranslation:backwardCompatabilityText:`
|
|
344
|
+
with part index 0 and the new text as backwards-compatibility text, and the
|
|
345
|
+
RPC answers `{ok:true}` on invocation only. **Preflight before firing**, same
|
|
346
|
+
discipline as `tapback`/`deleteMessage`: the guid must be inside the scan
|
|
347
|
+
window — located by a short poll (3 × 1 s, sleeping only between reads),
|
|
348
|
+
because a just-sent guid can lag history by a beat and a one-shot read
|
|
349
|
+
would fail closed on a message about to appear —
|
|
350
|
+
and the message must be an **editable own plain-text message**
|
|
351
|
+
(`isEditableOwnTextMessage` in `gateway/types.ts`, shared by both
|
|
352
|
+
implementations): `is_from_me` (Apple only permits editing your own
|
|
353
|
+
messages and the helper does not check), non-empty `text`, and no
|
|
354
|
+
attachments/poll — the RPC always edits part 0, which on those rows is not
|
|
355
|
+
the text, and a retracted row has nothing left to edit. Anything else is
|
|
356
|
+
refused as `ok:false, effectStarted:false` without touching the bridge, as
|
|
357
|
+
is empty or whitespace-only text. Text identical to the current row is the
|
|
358
|
+
idempotent `skipped:'unchanged'` (`ok:true`, nothing fired). The whole
|
|
359
|
+
decision is one shared function (`decideEditMessagePreflight`) so the two
|
|
360
|
+
implementations cannot drift. Known gap: a rich-link balloon carries its
|
|
361
|
+
URL as `text` and nothing on `ImsgMessage` marks it, so the real path
|
|
362
|
+
fires on it and the read-back decides; FakeGateway's rich-link row has no
|
|
363
|
+
text and is refused. After the RPC fires, a structured `-32603` whose
|
|
364
|
+
`data` is one of the helper's pre-invoke refusals (chat/message not found,
|
|
365
|
+
no selector, bad signature) is `effectStarted:false`; a bridge timeout or
|
|
366
|
+
any other `-32603` (an exception after IMChat was invoked) and an
|
|
367
|
+
unconfirmed read-back are `ok:false, effectStarted:true` — a consumer
|
|
368
|
+
reconciles those rather than treating them as a no-op. **Host-verified
|
|
369
|
+
2026-09-03** through `ImsgGateway` on the shared Mac (see the ledger
|
|
370
|
+
below); recipient-side rendering remains unobserved. `unsendMessage`
|
|
371
|
+
deliberately keeps its fire-first shape: retracting a just-sent row is a
|
|
372
|
+
legitimate use and its verify pass is the existing contract; a matching
|
|
373
|
+
preflight is a follow-up, not part of this change. Native CLI/RPC E2E on macOS 27 `26A5421a` (imsg#11, chat 8): both
|
|
374
|
+
paths edited a fresh own message and read the new text back on the same
|
|
375
|
+
guid/ROWID, with `message.date_edited` stamped — that column is not exposed
|
|
376
|
+
through `messages.history`, so the SDK smoke reads it from chat.db directly
|
|
377
|
+
while the gateway verifies by text read-back. **Edits mutate the existing
|
|
378
|
+
row's text IN PLACE: no new row, no `subscribe()` event.** `MessageWatcher`'s cursor is a ROWID
|
|
270
379
|
high-water mark (`WHERE ROWID > cursor`), so an UPDATE to an existing row is
|
|
271
380
|
structurally invisible to the watch stream — the only way to observe an
|
|
272
381
|
edit is re-reading `history()` and comparing text for a guid you already
|
|
@@ -340,6 +449,81 @@ both implementations — none of Apple's group primitives apply to a 1:1 chat.
|
|
|
340
449
|
and the receiving iPhone displayed Mapi's new contact info, closing the
|
|
341
450
|
receiver-side verification gap.
|
|
342
451
|
|
|
452
|
+
- **Chat backgrounds** — `chatBackgroundStatus` / `setChatBackground` /
|
|
453
|
+
`removeChatBackground` (Mapier-Labs/imsg#12, `chat.background.status|set|remove`).
|
|
454
|
+
A background is chat-scoped state recorded in chat.db's background tables,
|
|
455
|
+
not a message row: there is no `subscribe()` event and no history row on
|
|
456
|
+
either implementation. DM and group chats both accept all three verbs.
|
|
457
|
+
`chatBackgroundStatus` is a plain chat.db read (no selector marker, but it
|
|
458
|
+
needs the imsg#12 binary: an older build answers `-32601` and the call
|
|
459
|
+
throws). `null` means the chat does not exist (`-32602`); any other failure
|
|
460
|
+
is thrown, so a transient rpc/bridge fault can never masquerade as "no such
|
|
461
|
+
chat". It returns the persisted background channel GUID, which **lags
|
|
462
|
+
imagent** — a durable snapshot, never the live IMChat state.
|
|
463
|
+
`setChatBackground` supports the single runtime-verified `gradient` preset.
|
|
464
|
+
On the native side the RPC returns on helper acceptance — its response
|
|
465
|
+
embeds the pre-mutation chat.db snapshot, so no separate preflight read —
|
|
466
|
+
and the pipeline (PosterBoard builder → hidden `OverFullScreen` editor →
|
|
467
|
+
ChatKit poster/watch payloads → `setTranscriptBackgroundAndSendToChat:transferID:`)
|
|
468
|
+
runs asynchronously, bounded by per-stage watchdogs and a 90 s overall cap.
|
|
469
|
+
`ImsgGateway` therefore polls `chat.background.status` under a 90 s
|
|
470
|
+
deadline on a **monotonic** clock (`performance.now()`; a wall-clock
|
|
471
|
+
rollback cannot stretch the wait — the native CLI's own window; read first,
|
|
472
|
+
sleep 1 s between), in the pure loop `src/gateway/chat-background.ts`. Each poll is a
|
|
473
|
+
local chat.db read. The helper's operation record is a bridge round-trip on
|
|
474
|
+
Messages' main thread — it competes with the pipeline itself and, because
|
|
475
|
+
the native JSON-RPC loop is strictly serial, holds every other gateway verb
|
|
476
|
+
for up to 10 s on a stall — so it is read only (a) once, pulled forward,
|
|
477
|
+
when a NEW candidate appears (a background GUID different from the
|
|
478
|
+
pre-mutation snapshot) and (b) on a 15 s cadence for early failure
|
|
479
|
+
detection; a helper read that times out doubles that gap, up to 60 s, and
|
|
480
|
+
chat.db polling carries on meanwhile. **Success requires operation
|
|
481
|
+
correlation**: a candidate is accepted only when it equals the record's
|
|
482
|
+
`transfer_id` (the identifier IMChat persists as the background GUID, imsg
|
|
483
|
+
`docs/chat-background.md`). A changed guid, a newer `set` event, or both
|
|
484
|
+
are NOT evidence — a delayed write from an earlier operation satisfies them
|
|
485
|
+
— and a helper that no longer tracks the operation (bounded record table,
|
|
486
|
+
Messages relaunch) leaves nothing to correlate with, so that case ends the
|
|
487
|
+
wait as ambiguous rather than guessing. A bridge-timeout on a verification
|
|
488
|
+
read keeps polling; any other read failure (dead rpc child, transport loss)
|
|
489
|
+
ends the wait as ambiguous. `ok:true` carries the persisted GUID; a re-set
|
|
490
|
+
replaces the background and the GUID changes. FakeGateway mints a fresh GUID
|
|
491
|
+
per set from a sequence it persists in its snapshot. Result semantics
|
|
492
|
+
follow `shareNamePhoto`: `effectStarted:false` is a proven preflight
|
|
493
|
+
rejection — capability absent, `-32602` (unknown chat, bad preset), or a
|
|
494
|
+
helper refusal the SDK recognises in the JSON-RPC `error.data` (busy with
|
|
495
|
+
another background operation — the helper holds one in-process lock —,
|
|
496
|
+
guard mismatch, missing selector/runtime, "did not queue"); `ok:false,
|
|
497
|
+
effectStarted:true` means the effect may have started and persistence was
|
|
498
|
+
not proven — helper record `failed`, the deadline passed, an
|
|
499
|
+
rpc-child/transport failure, a bridge timeout, or any other `-32603` such as
|
|
500
|
+
an exception raised after IMChat was invoked (the native server reports all
|
|
501
|
+
of these under one code with the reason only in `data`) — and must be
|
|
502
|
+
reconciled, never reported as a proven no-op.
|
|
503
|
+
`removeChatBackground` is driven by the helper's **live** IMChat GUID, never
|
|
504
|
+
the caller's chat.db snapshot. The optional `expectedGuid` is a
|
|
505
|
+
compare-and-clear guard evaluated against that live GUID **first**, before
|
|
506
|
+
the no-background check: a mismatch — including a guard on a chat with no
|
|
507
|
+
live background — is `ok:false, effectStarted:false` and leaves the
|
|
508
|
+
background in place. An empty string is no guard (`normalizeChatBackgroundGuard`
|
|
509
|
+
in `gateway/types.ts`, shared by both implementations; the wire only
|
|
510
|
+
carries a non-empty one). Only then is a chat with no live background the
|
|
511
|
+
explicit `skipped:'no-background'` no-op (`ok:true`, IMChat never invoked).
|
|
512
|
+
The gateway reads chat.db before firing (the remove response's own snapshot
|
|
513
|
+
is post-invoke) and after a real clear checks that response, then polls
|
|
514
|
+
under a 30 s deadline for a `clear` event newer than the baseline; an unchanged empty
|
|
515
|
+
snapshot counts only when the pre-mutation read recorded the background
|
|
516
|
+
that is now gone, because an already-lagging empty snapshot proves nothing.
|
|
517
|
+
Native Set/Remove semantics were smoke-verified on macOS 27 `26A5421a`
|
|
518
|
+
(matching live GUID, chat.db event, cache, watch cache, upload metadata),
|
|
519
|
+
and the production CLI/RPC path went through its matched-build E2E before
|
|
520
|
+
imsg#12 merged. The SDK verbs were **host-verified 2026-09-03** through
|
|
521
|
+
`ImsgGateway` on the shared Mac (`--only-chat-background` leg of
|
|
522
|
+
`scripts/tier2-smoke.ts`; see the ledger below); recipient-side rendering,
|
|
523
|
+
group chats and the replace path remain unobserved. The leg
|
|
524
|
+
(`scripts/tier2-chat-background-leg.ts`) stops at the first failed check
|
|
525
|
+
and never clears a background whose guid it did not read back.
|
|
526
|
+
|
|
343
527
|
**Host-verification ledger.** Exercised through `ImsgGateway` on the host Mac
|
|
344
528
|
2026-07-14 (`scripts/tier2-smoke.ts`, chat 3996):
|
|
345
529
|
|
|
@@ -361,6 +545,66 @@ Each leg prints an EYEBALL step because a local write is not proof of
|
|
|
361
545
|
receiver-visible delivery (the group-photo lesson). The `sendRich`
|
|
362
546
|
text/effect/reply path was already live-verified.
|
|
363
547
|
|
|
548
|
+
Exercised 2026-09-03 on the shared Mac (macOS 27 `26A5421a`, user
|
|
549
|
+
`mapidev`, disposable DM chat 8, SDK `bf92147` against the deployed imsg#12
|
|
550
|
+
CLI/helper: `bridge_version 2`, `v2_ready true`, `chatBackgroundSetGradient
|
|
551
|
+
true`, `chatBackgroundRemove true`; `scripts/tier2-smoke.ts 8
|
|
552
|
+
--only-chat-background`, run dir `map175-sdk-bg-e2e.PCVLad`):
|
|
553
|
+
|
|
554
|
+
- **`setChatBackground` / `removeChatBackground` / `chatBackgroundStatus`**
|
|
555
|
+
— live-verified: both capability rows; baseline `backgroundSet:false`;
|
|
556
|
+
Set Gradient persisted a NEW background guid in ~7 s and a separate status
|
|
557
|
+
read returned the same guid; Remove with `NOT-THE-LIVE-GUID` →
|
|
558
|
+
`ok:false, effectStarted:false` with the background unchanged (the logged
|
|
559
|
+
`ImsgRpcError` is the deliberate mismatch); guarded Remove cleared it,
|
|
560
|
+
status read `backgroundSet:false`, and an unguarded Remove on the bare
|
|
561
|
+
chat → `skipped:'no-background'`. `ALL PASS`, exit 0; the audit recorded
|
|
562
|
+
two events, both in chat 8; native status identical before and after and
|
|
563
|
+
the fixture back at its no-background baseline. The `live-smoke.ts 8
|
|
564
|
+
--read-only` gate passed first. Not observed: the receiving device
|
|
565
|
+
rendering the Gradient; not covered: group chats, the replace (re-set)
|
|
566
|
+
path, relay/public-API integration.
|
|
567
|
+
|
|
568
|
+
Exercised 2026-09-03 on the same host (SDK `6cf6ac5` against the same
|
|
569
|
+
deployed helper: `editMessageItemTranslation true`, the two four-argument
|
|
570
|
+
markers false; `scripts/tier2-smoke.ts 8 --only-edit-message`, run dir
|
|
571
|
+
`map175-sdk-edit-e2e.fgUq4t`):
|
|
572
|
+
|
|
573
|
+
Exercised 2026-09-03 on the same host (SDK `6e28c82` against the deployed
|
|
574
|
+
imsg#13 helper, `mentionFormatting true`; disposable group chat 12 with
|
|
575
|
+
three phone-handle participants; `scripts/tier2-smoke.ts 12 --only-mention
|
|
576
|
+
--mention-handle <participant IMHandle.ID> --mention-name <display name>`, run dir
|
|
577
|
+
`map175-sdk-mention-e2e.PuauKG`):
|
|
578
|
+
|
|
579
|
+
- **`sendRich` confirmed @mention** — live-verified: capability row;
|
|
580
|
+
`@Neo tier2-smoke 21:13:08 mention` sent
|
|
581
|
+
(`86246294-B16D-4E5C-9E15-D5E80C90CB14`, row 3143) and read back on the
|
|
582
|
+
same guid; `chat.db attributedBody` carries `__kIMMentionConfirmedMention`
|
|
583
|
+
at byte 266 followed by the handle at byte 300; a non-participant handle
|
|
584
|
+
refused by the helper (`-32603` "Mention identifier is not a participant
|
|
585
|
+
handle of this chat") → `refused:'mention-not-participant'`, a range
|
|
586
|
+
outside the text → `'text-formatting-range'`, and the handle as visible
|
|
587
|
+
text → `'mention-identifier'`, none of the three texts in history
|
|
588
|
+
afterwards. 9/9 PASS, `ALL PASS`, exit 0; the audit recorded exactly one
|
|
589
|
+
event, in chat 12; native status identical before and after. The
|
|
590
|
+
`live-smoke.ts 12 --read-only` gate passed first (6/6). **Recipient-side
|
|
591
|
+
observed** this time: on the mentioned participant's iPhone the message
|
|
592
|
+
rendered with "@Neo" highlighted in the same style as a native mention.
|
|
593
|
+
Not covered: email-handle mentions (not covered natively either), the
|
|
594
|
+
`send-attachment` / multipart mention legs (no SDK surface).
|
|
595
|
+
|
|
596
|
+
- **`editMessage`** — live-verified: capability row from the three-marker
|
|
597
|
+
fold; marker message sent (`73263626-5A07-4B3C-8244-D3133B8A8EA6`), edited,
|
|
598
|
+
new text read back on the SAME guid, `chat.db message.date_edited`
|
|
599
|
+
stamped (`810141791007747072`); unchanged text → `skipped:'unchanged'`;
|
|
600
|
+
whitespace text → `ok:false, effectStarted:false` with the edited text
|
|
601
|
+
still in place; the chat's newest inbound row (the DM partner's echo of
|
|
602
|
+
the marker) → `ok:false, effectStarted:false` and untouched. 10/10 PASS,
|
|
603
|
+
`ALL PASS`, exit 0; native status identical before and after. The
|
|
604
|
+
`live-smoke.ts 8 --read-only` gate passed first (6/6). Not observed: the
|
|
605
|
+
receiving device rendering "Edited" (the inbound echo carried the original
|
|
606
|
+
text, which is the partner's copy, not evidence either way).
|
|
607
|
+
|
|
364
608
|
- **subscribe(sinceId?)** — long-lived event stream of new messages/reactions across all
|
|
365
609
|
chats the host Mac's Messages account can see. `sinceId` is an **exclusive** cursor: only
|
|
366
610
|
events with `id > sinceId` are delivered (catch-up semantics, not "starting at"). Today
|
|
@@ -783,8 +1027,16 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
783
1027
|
returns `ok:false` for fewer than two options or an unknown chat
|
|
784
1028
|
- `sendRichLink` echoes an outbound row and returns `ok:false` for an unknown chat; preview
|
|
785
1029
|
metadata is not queryable through `ImsgMessage`
|
|
1030
|
+
- `sendRich` with a confirmed mention of a participant sends once (DM and group, alongside
|
|
1031
|
+
styles); a non-participant handle, a wrongly formatted participant, a range outside the
|
|
1032
|
+
text (mention OR style), an empty or padded handle, the handle as visible text, an
|
|
1033
|
+
unreported group directory, and a missing `mentionFormatting` marker each return
|
|
1034
|
+
`ok:false` with the matching typed `refused` reason and nothing sent
|
|
786
1035
|
- `editMessage` changes the target row's text in place — same row count before/after, no
|
|
787
|
-
new row
|
|
1036
|
+
new row; refuses an inbound (not `is_from_me`) message, empty/whitespace text, and our own
|
|
1037
|
+
message outside the scan window without mutation;
|
|
1038
|
+
unchanged text is the idempotent `skipped:'unchanged'`; returns `ok:false` without
|
|
1039
|
+
mutation when the `editMessage` capability marker is absent
|
|
788
1040
|
- `unsendMessage` retracts the target row (text clears, row persists);
|
|
789
1041
|
`deleteMessage` is dispatch-only with no stable history postcondition (the
|
|
790
1042
|
fake conservatively preserves the row); both return `ok: false` for an
|
|
@@ -794,13 +1046,19 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
794
1046
|
- `renameGroup`/`addParticipant`/`removeParticipant` are reflected in a message sent
|
|
795
1047
|
afterward (`chat_name`/`participants`) and return `ok: false` against a DM
|
|
796
1048
|
- `setGroupPhoto`/`leaveGroup` return `ok: true` against a group and `ok: false` against a DM
|
|
797
|
-
- patched sticker, group-photo, and
|
|
798
|
-
when their `imsg status` capability markers are absent
|
|
1049
|
+
- patched sticker, group-photo, participant, and chat-background verbs return `ok:false`
|
|
1050
|
+
without mutation when their `imsg status` capability markers are absent
|
|
1051
|
+
- chat background: `setChatBackground` persists a NEW guid on a DM and a group (a re-set
|
|
1052
|
+
changes the guid), `chatBackgroundStatus` reflects it, `removeChatBackground` clears it;
|
|
1053
|
+
remove on a bare chat is the explicit `no-background` skip (also with an empty guard), a
|
|
1054
|
+
mismatched `expectedGuid` is a preflight rejection that leaves the background (and is
|
|
1055
|
+
checked before the no-background branch, so a stale guard on a bare chat rejects), and an
|
|
1056
|
+
unknown chat never starts an effect
|
|
799
1057
|
- `recentReactions` surfaces an inbound custom-emoji reaction (`reaction: 'custom'`) with the
|
|
800
1058
|
real `emoji` intact
|
|
801
1059
|
- Fake-only restart coverage snapshots and restores a world, then proves
|
|
802
|
-
`sendStatus`, `checkHandle`, Name & Photo idempotency,
|
|
803
|
-
fixture state, and monotonic IDs survive exactly
|
|
1060
|
+
`sendStatus`, `checkHandle`, Name & Photo idempotency, chat backgrounds (and their
|
|
1061
|
+
guid sequence), current group metadata, fixture state, and monotonic IDs survive exactly
|
|
804
1062
|
|
|
805
1063
|
FakeGateway runs this suite in CI on every PR. ImsgGateway runs it as a manual runbook step on
|
|
806
1064
|
the host Mac (see `docs/host-mac-control.md`) until a host-Mac CI runner exists — at that point
|