@mapier/imsg-sdk 0.3.1 → 0.5.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.
Potentially problematic release.
This version of @mapier/imsg-sdk might be problematic. Click here for more details.
- package/dist/gateway/fake.d.ts +12 -10
- package/dist/gateway/fake.js +122 -6
- package/dist/gateway/fake.js.map +1 -1
- package/dist/gateway/group-mutation.d.ts +14 -0
- package/dist/gateway/group-mutation.js +57 -0
- package/dist/gateway/group-mutation.js.map +1 -0
- package/dist/gateway/imsg.d.ts +14 -10
- package/dist/gateway/imsg.js +158 -50
- package/dist/gateway/imsg.js.map +1 -1
- package/dist/gateway/locations.d.ts +25 -0
- package/dist/gateway/locations.js +89 -0
- package/dist/gateway/locations.js.map +1 -0
- package/dist/gateway/types.d.ts +35 -9
- package/dist/gateway/types.js +29 -0
- package/dist/gateway/types.js.map +1 -1
- package/dist/imsg/location-watch.d.ts +34 -0
- package/dist/imsg/location-watch.js +79 -0
- package/dist/imsg/location-watch.js.map +1 -0
- package/dist/imsg/rpc.d.ts +25 -3
- package/dist/imsg/rpc.js +33 -2
- package/dist/imsg/rpc.js.map +1 -1
- package/dist/imsg/status.js +8 -0
- package/dist/imsg/status.js.map +1 -1
- package/dist/imsg/watch.d.ts +2 -1
- package/dist/imsg/watch.js +7 -1
- package/dist/imsg/watch.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/docs/api.md +1 -0
- package/docs/capability-matrix.md +2 -0
- package/docs/gateway-contract.md +254 -8
- package/package.json +1 -1
package/docs/gateway-contract.md
CHANGED
|
@@ -61,6 +61,8 @@ interface Gateway {
|
|
|
61
61
|
chatBackgroundRemove: boolean;
|
|
62
62
|
editMessage: boolean;
|
|
63
63
|
mentionFormatting: boolean;
|
|
64
|
+
sharedLocations: boolean;
|
|
65
|
+
locationRequest: boolean;
|
|
64
66
|
};
|
|
65
67
|
subscribe(sinceId?: number): AsyncIterable<GatewayEvent>;
|
|
66
68
|
history(chatId: number, limit?: number): Promise<ImsgMessage[]>;
|
|
@@ -104,10 +106,10 @@ interface Gateway {
|
|
|
104
106
|
deleteMessage(chatId: number, targetGuid: string): Promise<{ ok: boolean }>;
|
|
105
107
|
setTyping(chatId: number, on: boolean): Promise<{ ok: boolean }>;
|
|
106
108
|
markRead(chatId: number): Promise<{ ok: boolean }>;
|
|
107
|
-
renameGroup(chatId: number, name: string): Promise<
|
|
109
|
+
renameGroup(chatId: number, name: string): Promise<GroupMutationResult>;
|
|
108
110
|
setGroupPhoto(chatId: number, filePath?: string): Promise<{ ok: boolean }>;
|
|
109
|
-
addParticipant(chatId: number, handle: string): Promise<
|
|
110
|
-
removeParticipant(chatId: number, handle: string): Promise<
|
|
111
|
+
addParticipant(chatId: number, handle: string): Promise<GroupMutationResult>;
|
|
112
|
+
removeParticipant(chatId: number, handle: string): Promise<GroupMutationResult>;
|
|
111
113
|
leaveGroup(chatId: number): Promise<{ ok: boolean }>;
|
|
112
114
|
shareNamePhoto(chatId: number): Promise<{
|
|
113
115
|
ok: boolean;
|
|
@@ -129,8 +131,30 @@ interface Gateway {
|
|
|
129
131
|
skipped?: 'no-background';
|
|
130
132
|
effectStarted?: boolean;
|
|
131
133
|
}>;
|
|
134
|
+
|
|
135
|
+
// Find My locations (M2B-31). See "Location" below.
|
|
136
|
+
readSharedLocations(chatGuid?: string): Promise<SharedLocation[]>;
|
|
137
|
+
sendLocationRequest(chatId: number): Promise<SendResult>;
|
|
138
|
+
watchLocations(): AsyncIterable<LocationUpdate>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface SharedLocation {
|
|
142
|
+
handle: string; // IMFindMyHandle.identifier: E.164 phone or Apple ID email
|
|
143
|
+
chatGuid?: string; // the handle's DM chat when resolvable
|
|
144
|
+
latitude?: number; // present together with longitude, only on a valid fix
|
|
145
|
+
longitude?: number;
|
|
146
|
+
horizontalAccuracyM?: number;
|
|
147
|
+
altitudeM?: number;
|
|
148
|
+
speedMps?: number; // -1 = unknown
|
|
149
|
+
locationType?: number; // FMLLocation.locationType raw
|
|
150
|
+
coarseAddress?: string;
|
|
151
|
+
capturedAt?: string; // ISO8601 UTC, FMLLocation.timestamp
|
|
152
|
+
isValid: boolean; // false = sharing, no fix yet (no coordinates)
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
type LocationShareEvent = 'location_update' | 'share_started' | 'share_ended';
|
|
156
|
+
type LocationUpdate = SharedLocation & { event: LocationShareEvent };
|
|
157
|
+
|
|
134
158
|
interface ReactionNote {
|
|
135
159
|
id: number; // the reaction's global rowid
|
|
136
160
|
// 'custom' covers an arbitrary-emoji tapback (iOS 18+, imsg surfaces it as
|
|
@@ -145,7 +169,12 @@ interface ReactResult {
|
|
|
145
169
|
skipped?: 'already-reacted' | 'stale-target' | 'not-reacted';
|
|
146
170
|
}
|
|
147
171
|
|
|
148
|
-
|
|
172
|
+
// message or reaction; see §2 shape. Deliberately NOT widened to a union
|
|
173
|
+
// for Find My updates (0.4.0): every pinned consumer reads `.id`/`.chat_guid`
|
|
174
|
+
// straight off the subscribe value (imsg-relay mac-daemon, imsg-agent
|
|
175
|
+
// runtime-event-source), so live location changes have their own stream,
|
|
176
|
+
// `watchLocations()`, and this type stays exactly what it was.
|
|
177
|
+
type GatewayEvent = ImsgMessage;
|
|
149
178
|
|
|
150
179
|
type Reaction = 'love' | 'like' | 'dislike' | 'laugh' | 'emphasis' | 'question';
|
|
151
180
|
|
|
@@ -160,6 +189,20 @@ interface GroupChatResolutionRequest {
|
|
|
160
189
|
expectedParticipantExternalIds: readonly string[];
|
|
161
190
|
excludedParticipantExternalIds?: readonly string[];
|
|
162
191
|
}
|
|
192
|
+
|
|
193
|
+
// renameGroup / addParticipant / removeParticipant. A success is proven (read
|
|
194
|
+
// back from a fresh chats.list) and carries neither field. Every failure
|
|
195
|
+
// carries both, and `effectStarted` follows from `reason` alone.
|
|
196
|
+
interface GroupMutationResult {
|
|
197
|
+
ok: boolean;
|
|
198
|
+
effectStarted?: boolean;
|
|
199
|
+
reason?:
|
|
200
|
+
| 'unsupported' // capability marker absent — effectStarted:false
|
|
201
|
+
| 'refused' // proven rejection before IMChat — effectStarted:false
|
|
202
|
+
| 'fire-failed' // the call errored, may have applied — effectStarted:true
|
|
203
|
+
| 'unverified' // fired, never confirmed in the window — effectStarted:true
|
|
204
|
+
| 'verify-read-failed'; // fired, the verification read broke — effectStarted:true
|
|
205
|
+
}
|
|
163
206
|
```
|
|
164
207
|
|
|
165
208
|
### Tier 2 (bridge-backed)
|
|
@@ -403,7 +446,63 @@ prevent.
|
|
|
403
446
|
The long-lived RPC process keeps a stale chat-metadata view after its own
|
|
404
447
|
mutation on macOS 26, while a fresh `imsg chats` process sees the update.
|
|
405
448
|
Verification therefore polls fresh, read-only CLI processes (using the
|
|
406
|
-
same `IMSG_BIN`)
|
|
449
|
+
same `IMSG_BIN`) — `GROUP_MUTATION_VERIFY_ATTEMPTS` (20) × 1 s, so a
|
|
450
|
+
**20-second** window — before giving up.
|
|
451
|
+
|
|
452
|
+
**A failure says which failure it is** (`GroupMutationResult`, M2B-40).
|
|
453
|
+
These three verbs used to answer a bare `ok:false` whether nothing had
|
|
454
|
+
fired or the mutation had gone out unconfirmed, which left a consumer no
|
|
455
|
+
choice but to settle every failure as possibly-applied — un-retirable
|
|
456
|
+
reconciliation work by construction. The classification, in the order the
|
|
457
|
+
gateway reaches it:
|
|
458
|
+
|
|
459
|
+
- **`unsupported`** (`effectStarted:false`) — `addParticipant`/
|
|
460
|
+
`removeParticipant` without the `groupParticipants` marker. Nothing was
|
|
461
|
+
called and no request on this host build ever could be.
|
|
462
|
+
- **`refused`** (`effectStarted:false`) — a proven rejection before IMChat
|
|
463
|
+
was invoked. Either the gateway's own preflight (target is a DM; the
|
|
464
|
+
removal would break the member floor below) or the helper's: a `-32602`,
|
|
465
|
+
or a `-32603` whose `error.data` carries one of that verb's known
|
|
466
|
+
pre-invoke refusals (`Chat not found`, `Could not vend handle`,
|
|
467
|
+
`Participant not found on chat`, `_setDisplayName: not available`, the
|
|
468
|
+
missing-selector and missing-parameter texts). The lists are per verb, in
|
|
469
|
+
`gateway/group-mutation.ts`, and are matched the same way the
|
|
470
|
+
chat-background pair matches its own.
|
|
471
|
+
- **`fire-failed`** (`effectStarted:true`) — the mutation call errored but
|
|
472
|
+
the error does NOT prove the helper stopped short: a bridge timeout, an
|
|
473
|
+
exception raised after IMChat was invoked, a dead rpc child, any
|
|
474
|
+
unmodelled `-32603`. It may already have applied.
|
|
475
|
+
- **`unverified`** (`effectStarted:true`) — it fired cleanly and the 20 s
|
|
476
|
+
window closed with `chats.list` never showing the wanted state.
|
|
477
|
+
- **`verify-read-failed`** (`effectStarted:true`) — it fired cleanly and
|
|
478
|
+
then the verification read itself failed, so there is no evidence either
|
|
479
|
+
way. One failed read ends the wait (it always has): `imsg chats` is a
|
|
480
|
+
local chat.db read, so a failure means a broken host, not a blip. Kept
|
|
481
|
+
distinct from `unverified` because the remediation is an operator's, not
|
|
482
|
+
a retry.
|
|
483
|
+
|
|
484
|
+
A success stays the bare `{ok:true}` it has always been: it was read back
|
|
485
|
+
from chat.db, so neither field would add anything.
|
|
486
|
+
|
|
487
|
+
**Apple's three-member floor.** A group conversation stays at three or more
|
|
488
|
+
members, the local user included; Apple silently declines a removal that
|
|
489
|
+
would drop it below that — `removeParticipants:reason:` returns normally and
|
|
490
|
+
the membership does not change. There is no error to classify, so
|
|
491
|
+
`removeParticipant` counts first: one fresh `chats.list` read before firing,
|
|
492
|
+
and a group whose `participants[]` (which excludes the local user, so the
|
|
493
|
+
group has `participants.length + 1` members) is shorter than
|
|
494
|
+
`GROUP_MIN_MEMBERS` is `refused` outright instead of costing the full 20 s
|
|
495
|
+
and then reporting ambiguity. The same read refuses a DM target for all
|
|
496
|
+
three verbs, which the bridge likewise answers by doing nothing.
|
|
497
|
+
|
|
498
|
+
Both preflight rules are **best-effort and fail open**: only what that read
|
|
499
|
+
actually reported is acted on. A chat outside `CHAT_SCAN_LIMIT`, a read that
|
|
500
|
+
failed, a row without the field, or a membership list that no longer
|
|
501
|
+
contains the target handle all fall through to fire-and-verify. An unknown
|
|
502
|
+
target must never become a confident refusal — the same discipline
|
|
503
|
+
`participant_directory_unavailable` applies in `resolveGroupChat`. Trusting
|
|
504
|
+
the count only when the target is still listed is what keeps a stale
|
|
505
|
+
chat.db view from refusing a removal that would have worked.
|
|
407
506
|
- **`setGroupPhoto`/`leaveGroup`** — `group.setIcon` / `group.leave`. No field
|
|
408
507
|
in imsg's JSON output reflects a group photo, and `participants[]` always
|
|
409
508
|
excludes the local user (see "Participants exclude the local user" above),
|
|
@@ -527,6 +626,73 @@ both implementations — none of Apple's group primitives apply to a 1:1 chat.
|
|
|
527
626
|
(`scripts/tier2-chat-background-leg.ts`) stops at the first failed check
|
|
528
627
|
and never clears a background whose guid it did not read back.
|
|
529
628
|
|
|
629
|
+
- **Location** — `readSharedLocations` / `watchLocations` /
|
|
630
|
+
`sendLocationRequest` (M2B-31; the shared wire contract lives with the
|
|
631
|
+
Mac and relay tracks, `location-wire.md`). Find My shares are
|
|
632
|
+
account-scoped state on the host Mac — the handles in
|
|
633
|
+
`findMyHandlesSharingLocationWithMe` and each handle's latest
|
|
634
|
+
`FMLLocation` — not message rows: no `subscribe()` event, no history row
|
|
635
|
+
on either implementation. Two separate helper markers gate the surface,
|
|
636
|
+
read from `imsg status --json` like every other patched verb:
|
|
637
|
+
`sharedLocations` (the read path) and `locationRequest` (the request
|
|
638
|
+
card, which reuses polls' extension-balloon initializer); neither implies
|
|
639
|
+
the other.
|
|
640
|
+
`readSharedLocations(chatGuid?)` is the native `locations.read`
|
|
641
|
+
(`{chat_guid?}` → `{locations, read_at}`): every sharing handle, or only
|
|
642
|
+
the one whose DM chat is `chatGuid`. The helper turns tracking on for a
|
|
643
|
+
handle's DM chat once per process and reads the current fix; a handle
|
|
644
|
+
that shares but has no fix yet comes back `isValid:false` with **no
|
|
645
|
+
coordinates** — never a zero fix. The mapping (`src/gateway/locations.ts`)
|
|
646
|
+
is total over malformed rows and drops what it cannot vouch for (no
|
|
647
|
+
handle; a "valid" row missing a coordinate). **It throws** — on both
|
|
648
|
+
implementations — when the bridge does not advertise `sharedLocations`
|
|
649
|
+
and on any rpc failure: an empty list is a positive claim ("nobody is
|
|
650
|
+
sharing") that a bridge which could not read must not make.
|
|
651
|
+
`sendLocationRequest(chatId)` is `location_request.send` (`{chat_id}`;
|
|
652
|
+
the handler resolves `chat_id`/`chat_guid`/`chat_identifier` alike, so the
|
|
653
|
+
Gateway keeps its numeric local id like every other verb): Apple's native
|
|
654
|
+
"Request Location" card, the one Messages sends from + › Location ›
|
|
655
|
+
Request, built by the helper with a fresh payload id. Targets an
|
|
656
|
+
**existing** chat only. **Fire-and-forget like `sendPoll`**: `ok:true`
|
|
657
|
+
means the helper dispatched the card; `guid` is the balloon row when
|
|
658
|
+
Messages exposed it synchronously and is absent otherwise (documented on
|
|
659
|
+
the native side). The recipient's answer, if any, arrives as a location
|
|
660
|
+
share — a `location` update below, or a decoded Find My share card on a
|
|
661
|
+
message row (imsg `docs/json.md` "Location card"), which this Gateway
|
|
662
|
+
does not model yet. FakeGateway emits the bare outbound row (like
|
|
663
|
+
`sendSticker`; `ImsgMessage` models nothing of the card) and returns only
|
|
664
|
+
the guid.
|
|
665
|
+
`watchLocations()` streams one `LocationUpdate` per change the native
|
|
666
|
+
watch reports — the `location` notification the watch stream emits
|
|
667
|
+
alongside `message`: a new fix (`location_update`), a handle starting
|
|
668
|
+
(`share_started`) or stopping (`share_ended`, which may carry no
|
|
669
|
+
coordinates) to share; on the native side this is a 30 s poll of
|
|
670
|
+
`locations.read` inside the watch session. `ImsgGateway` therefore holds
|
|
671
|
+
a **dedicated, non-rotated, cursorless** native watch session for it
|
|
672
|
+
(`src/imsg/location-watch.ts`, opened on the first `watchLocations()`
|
|
673
|
+
call): the message watch retires its process every 10 s once it has a
|
|
674
|
+
cursor, which would starve that poll. Its `message` notifications are
|
|
675
|
+
ignored. That poll runs in *every* subscription unless the subscriber
|
|
676
|
+
turns it off, so the message watch subscribes with `location_interval_s:
|
|
677
|
+
0` and this session is the only one that leaves the interval unset — one
|
|
678
|
+
Find My reader per connector, not one per watch session.
|
|
679
|
+
Location updates carry no rowid, so there is no catch-up: a
|
|
680
|
+
restarted session (child death) resumes nothing and a consumer takes the
|
|
681
|
+
baseline from `readSharedLocations()`, then applies updates. Malformed
|
|
682
|
+
notifications are dropped and reported as
|
|
683
|
+
`imsg_watch_location_malformed`, never forwarded. Single consumer, like
|
|
684
|
+
`subscribe()`, with the same documented divergence (FakeGateway allows
|
|
685
|
+
sequential re-watch). `GatewayEvent` is unchanged: see the note on it in
|
|
686
|
+
§1. FakeGateway's `injectSharedLocation(update)` is the fixture driver —
|
|
687
|
+
it goes through the real wire mapper, updates the in-memory share list
|
|
688
|
+
(`share_ended` removes the handle) and feeds `watchLocations()`.
|
|
689
|
+
**Host-verified 2026-09-13** (ledger below): `scripts/tier2-smoke.ts
|
|
690
|
+
<chatId> --only-location` (read leg, sends nothing) and
|
|
691
|
+
`--location-request` (posts the card; opt-in, the recipient sees it) are
|
|
692
|
+
the gate. Still open on the native side, to be confirmed in the relay
|
|
693
|
+
e2e: whether a fresh watch session emits the current snapshot
|
|
694
|
+
immediately or only diffs after its first poll.
|
|
695
|
+
|
|
530
696
|
**Host-verification ledger.** Exercised through `ImsgGateway` on the host Mac
|
|
531
697
|
2026-07-14 (`scripts/tier2-smoke.ts`, chat 3996):
|
|
532
698
|
|
|
@@ -608,6 +774,29 @@ three phone-handle participants; `scripts/tier2-smoke.ts 12 --only-mention
|
|
|
608
774
|
receiving device rendering "Edited" (the inbound echo carried the original
|
|
609
775
|
text, which is the partner's copy, not evidence either way).
|
|
610
776
|
|
|
777
|
+
Exercised 2026-09-13 on the host (SDK `d6fecca` against the deployed M2B-31
|
|
778
|
+
helper, imsg `0071842` built on the host, `sharedLocations true` /
|
|
779
|
+
`locationRequest true`; `scripts/tier2-smoke.ts 7 --only-location
|
|
780
|
+
--location-request`, bench `~/imsg-sdk-location-e2e-d6fecca`):
|
|
781
|
+
|
|
782
|
+
- **`readSharedLocations` / `sendLocationRequest`** — live-verified: both
|
|
783
|
+
capability rows; `readSharedLocations()` returned a list (0 handles: no
|
|
784
|
+
share was active during the run, so the fix shape was not exercised
|
|
785
|
+
through the SDK here); `sendLocationRequest(7)` returned
|
|
786
|
+
`{ok:true, guid:23DB6804-EAAC-476D-8CA5-7AE71642B960}` and the row read
|
|
787
|
+
back from `chat.db` as a Find My balloon
|
|
788
|
+
(`…:0000000000:com.apple.findmy.FindMyMessagesApp`); an unknown chat
|
|
789
|
+
(`999999999`) → `ok:false` (native `-32602`). 6/6 PASS, `ALL PASS`, exit
|
|
790
|
+
0; the audit recorded exactly one event, in chat 7 ("Requested your
|
|
791
|
+
location"). Recipient-side, same helper build on a second Mac (macOS
|
|
792
|
+
26.6.2, 2026-09-13): the card rendered with a Share button on an iPhone
|
|
793
|
+
for a sender NOT in contacts, a 1 h share came back as a valid fix from
|
|
794
|
+
`imsg locations --json`, and `imsg watch --json --location-interval 30`
|
|
795
|
+
emitted `share_started` / `location_update` / `share_ended` — native
|
|
796
|
+
layer only; `ImsgGateway.watchLocations()` end to end is the relay e2e.
|
|
797
|
+
Not covered: group chats, email-handle shares, share expiry vs. Apple's
|
|
798
|
+
handle drop.
|
|
799
|
+
|
|
611
800
|
Eyeballed 2026-09-04 on the same host (SDK `5826316` = v0.3.0, deployed
|
|
612
801
|
`imsg` 0.13.0 helper, all four 0.3.0 capability flags true) with a human
|
|
613
802
|
watching the receiving iPhone (iOS 26.6.1) between steps, one gateway call
|
|
@@ -660,6 +849,22 @@ the phone, created that day) and group chat 12 (three phone handles):
|
|
|
660
849
|
backgrounds) THEN failure`). That is per-endpoint pruning, not a failure
|
|
661
850
|
of the send, and the phone's endpoints are unaffected.
|
|
662
851
|
|
|
852
|
+
**NOT YET HOST-VERIFIED — `GroupMutationResult` (M2B-40).** The reason
|
|
853
|
+
vocabulary, the three-member floor and the DM preflight are derived from the
|
|
854
|
+
native sources (`IMsgInjected.m` `handleAddParticipant` /
|
|
855
|
+
`handleRemoveParticipant` / `handleSetDisplayName`,
|
|
856
|
+
`RPCServer+ChatHandlers.swift` `invokeBridge`) and are covered by unit tests,
|
|
857
|
+
which under hard rule 1 is **not** proof: `imsg` exit codes lie and Messages.app
|
|
858
|
+
cannot be faked. Before this ships, `scripts/tier2-smoke.ts --group-chat-id
|
|
859
|
+
<group> --participant <handle>` has to run on the host and answer three
|
|
860
|
+
questions: (1) does a removal at the floor actually get refused by Apple, or
|
|
861
|
+
does it land — the whole preflight is wrong if it lands; (2) is the floor three
|
|
862
|
+
members counting the local user, i.e. does a 4-member group allow exactly one
|
|
863
|
+
removal; (3) do the add/remove/rename round trips still return the bare
|
|
864
|
+
`{ok:true}` they did before the preflight read was added. The smoke's group leg
|
|
865
|
+
prints the measured membership and asserts the floor refusal, so a single run
|
|
866
|
+
settles all three.
|
|
867
|
+
|
|
663
868
|
- **subscribe(sinceId?)** — long-lived event stream of new messages/reactions across all
|
|
664
869
|
chats the host Mac's Messages account can see. `sinceId` is an **exclusive** cursor: only
|
|
665
870
|
events with `id > sinceId` are delivered (catch-up semantics, not "starting at"). Today
|
|
@@ -916,6 +1121,28 @@ a different product.
|
|
|
916
1121
|
There is no "create empty group". Adding/removing a member on an *existing* group is possible —
|
|
917
1122
|
see "Tier 2" above (`addParticipant`/`removeParticipant`, bridge-backed) — but only through
|
|
918
1123
|
those methods, not through `createGroup`.
|
|
1124
|
+
- **A group never shrinks below three members.** Apple keeps a group
|
|
1125
|
+
conversation at `GROUP_MIN_MEMBERS` (3) or more, counting the local user,
|
|
1126
|
+
and silently declines any removal that would break that — no error, no
|
|
1127
|
+
change. So `removeParticipant` on a group whose `participants[]` (local
|
|
1128
|
+
user excluded, see "Participants exclude the local user") is shorter than
|
|
1129
|
+
three is `{ok:false, effectStarted:false, reason:'refused'}` and nothing
|
|
1130
|
+
is fired. FakeGateway models the floor too: a fake that happily shrinks a
|
|
1131
|
+
group to two exceeds what the real gateway can do, and fixtures built on
|
|
1132
|
+
it would rehearse a removal iMessage refuses.
|
|
1133
|
+
- **A group mutation that failed says where it failed.** `renameGroup`,
|
|
1134
|
+
`addParticipant` and `removeParticipant` return `GroupMutationResult`
|
|
1135
|
+
(§1): a failure always carries `reason` and the `effectStarted` that
|
|
1136
|
+
follows from it, so a proven no-op is settled as a clean failure and only
|
|
1137
|
+
a mutation that actually reached IMChat is reconciled. The reason
|
|
1138
|
+
vocabulary and the per-verb proven-rejection lists are under "Tier 2"
|
|
1139
|
+
above. FakeGateway reaches only the proven-no-op half (`unsupported`,
|
|
1140
|
+
`refused`) — it has no bridge to time out on and must never manufacture
|
|
1141
|
+
an `effectStarted:true` that would send a consumer off to reconcile a
|
|
1142
|
+
mutation that never existed. Removing a handle the chat does not list is
|
|
1143
|
+
one of those refusals on both sides: the helper matches against the live
|
|
1144
|
+
IMChat participant list and answers `Participant not found on chat`,
|
|
1145
|
+
so the fake must not model it as a silent success either.
|
|
919
1146
|
- **Compose guard**: if Cmd+N fails to enter compose (sibling of the react Cmd+T failure,
|
|
920
1147
|
observed live 2026-07-17), every scripted keystroke would land in the focused chat's
|
|
921
1148
|
compose field and each Return would send it — recipient handles texted into an open
|
|
@@ -1099,10 +1326,20 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
1099
1326
|
- `setTyping`/`markRead` dispatch `{ ok: true }` against an existing chat and `{ ok: false }`
|
|
1100
1327
|
against an unknown one, with no queryable state either way
|
|
1101
1328
|
- `renameGroup`/`addParticipant`/`removeParticipant` are reflected in a message sent
|
|
1102
|
-
afterward (`chat_name`/`participants`)
|
|
1329
|
+
afterward (`chat_name`/`participants`); a DM target is the proven
|
|
1330
|
+
`{ok:false, effectStarted:false, reason:'refused'}`, and so is a removal that
|
|
1331
|
+
would break Apple's three-member floor or that names a handle the chat does
|
|
1332
|
+
not list — in both cases the membership is untouched afterward
|
|
1103
1333
|
- `setGroupPhoto`/`leaveGroup` return `ok: true` against a group and `ok: false` against a DM
|
|
1104
1334
|
- patched sticker, group-photo, participant, and chat-background verbs return `ok:false`
|
|
1105
|
-
without mutation when their `imsg status` capability markers are absent
|
|
1335
|
+
without mutation when their `imsg status` capability markers are absent; the
|
|
1336
|
+
participant verbs name it `reason:'unsupported'`, distinct from a request the
|
|
1337
|
+
host declined
|
|
1338
|
+
- (real-path only, `tests/group-mutation.test.ts` over the pure module) a bridge
|
|
1339
|
+
rejection raised before IMChat is `refused`; a timeout, an unmodelled `-32603`
|
|
1340
|
+
or a non-rpc error is `fire-failed`; an exhausted verify window is
|
|
1341
|
+
`unverified`; a failed verification read is `verify-read-failed` — one verb's
|
|
1342
|
+
proven-rejection text does not excuse another's
|
|
1106
1343
|
- chat background: `setChatBackground` persists a NEW guid on a DM and a group (a re-set
|
|
1107
1344
|
changes the guid), `chatBackgroundStatus` reflects it, `removeChatBackground` clears it;
|
|
1108
1345
|
remove on a bare chat is the explicit `no-background` skip (also with an empty guard), a
|
|
@@ -1111,9 +1348,18 @@ harness (`tsx --test`, see `package.json`). Required cases:
|
|
|
1111
1348
|
unknown chat never starts an effect
|
|
1112
1349
|
- `recentReactions` surfaces an inbound custom-emoji reaction (`reaction: 'custom'`) with the
|
|
1113
1350
|
real `emoji` intact
|
|
1351
|
+
- `readSharedLocations` lists every sharing handle, narrows by chat guid, replaces (never
|
|
1352
|
+
accumulates) a handle's row on a newer fix, and keeps a no-fix handle coordinate-free;
|
|
1353
|
+
it throws without the `sharedLocations` marker
|
|
1354
|
+
- `watchLocations` streams `share_started` → `location_update` → `share_ended` in order,
|
|
1355
|
+
`share_ended` drops the handle from the read, and a second concurrent consumer is refused
|
|
1356
|
+
- `sendLocationRequest` posts the card as its own outbound row to an existing chat, returns
|
|
1357
|
+
the balloon guid, refuses an unknown chat (no find-or-create), and fails closed without
|
|
1358
|
+
the `locationRequest` marker (no row written)
|
|
1114
1359
|
- Fake-only restart coverage snapshots and restores a world, then proves
|
|
1115
1360
|
`sendStatus`, `checkHandle`, Name & Photo idempotency, chat backgrounds (and their
|
|
1116
|
-
guid sequence), current group metadata, fixture state, and monotonic IDs
|
|
1361
|
+
guid sequence), Find My shares, current group metadata, fixture state, and monotonic IDs
|
|
1362
|
+
survive exactly; a snapshot carrying a share the wire mapper would refuse does not restore
|
|
1117
1363
|
|
|
1118
1364
|
FakeGateway runs this suite in CI on every PR. ImsgGateway runs it as a manual runbook step on
|
|
1119
1365
|
the host Mac (see `docs/host-mac-control.md`) until a host-Mac CI runner exists — at that point
|
package/package.json
CHANGED