1import { describe, expect, it, vi } from "@effect/vitest";
2import type { SharePayload } from "expo-sharing";
3
4import type { IncomingShareDraft } from "./incoming-share-model";
5import { IncomingShareInbox, type IncomingShareInboxDependencies } from "./incoming-share-inbox";
6
7const PAYLOAD: SharePayload = {
8 shareType: "text",
9 mimeType: "text/plain",
10 value: "Fix this",
11};
12
13function draft(id: string, createdAt = "2026-07-16T08:00:00.000Z"): IncomingShareDraft {
14 return {
15 schemaVersion: 1,
16 id,
17 createdAt,
18 text: PAYLOAD.value,
19 attachments: [],
20 warnings: [],
21 };
22}
23
24function deferred() {
25 let resolve!: () => void;
26 const promise = new Promise<void>((next) => {
27 resolve = next;
28 });
29 return { promise, resolve };
30}
31
32function createHarness(overrides: Partial<IncomingShareInboxDependencies> = {}) {
33 const persisted = new Map<string, IncomingShareDraft>();
34 let payloads: ReadonlyArray<SharePayload> = [PAYLOAD];
35 const dependencies: IncomingShareInboxDependencies = {
36 loadDrafts: async () => [...persisted.values()],
37 writeDraft: async (value) => {
38 persisted.set(value.id, value);
39 },
40 removeDraft: async (shareId) => {
41 persisted.delete(shareId);
42 },
43 getPayloads: () => payloads,
44 clearPayloads: () => {
45 payloads = [];
46 },
47 buildDraft: async ({ id, createdAt }) => ({
48 draft: draft(id, createdAt),
49 cleanup: async () => undefined,
50 }),
51 idForPayloads: async () => "share-stable",
52 now: () => "2026-07-16T08:00:00.000Z",
53 ...overrides,
54 };
55 return { inbox: new IncomingShareInbox(dependencies), persisted };
56}
57
58describe("IncomingShareInbox", () => {
59 it("coalesces a replay of an already-persisted native handoff", async () => {
60 const buildDraft = vi.fn(async ({ id, createdAt }) => ({
61 draft: draft(id, createdAt),
62 cleanup: async () => undefined,
63 }));
64 const cleanupReplayedPayloads = vi.fn(async () => undefined);
65 const { inbox, persisted } = createHarness({ buildDraft, cleanupReplayedPayloads });
66 persisted.set("share-stable", draft("share-stable"));
67
68 await expect(inbox.refresh({ ingestNative: true })).resolves.toEqual([draft("share-stable")]);
69 expect(buildDraft).not.toHaveBeenCalled();
70 expect(cleanupReplayedPayloads).toHaveBeenCalledWith([PAYLOAD]);
71 });
72
73 it("serializes concurrent refreshes so one native payload creates one inbox item", async () => {
74 const building = deferred();
75 const buildDraft = vi.fn(async ({ id, createdAt }) => {
76 await building.promise;
77 return { draft: draft(id, createdAt), cleanup: async () => undefined };
78 });
79 const { inbox } = createHarness({ buildDraft });
80
81 const first = inbox.refresh({ ingestNative: true });
82 const second = inbox.refresh({ ingestNative: true });
83 building.resolve();
84
85 await expect(Promise.all([first, second])).resolves.toEqual([
86 [draft("share-stable")],
87 [draft("share-stable")],
88 ]);
89 expect(buildDraft).toHaveBeenCalledTimes(1);
90 });
91
92 it("orders consumption after an in-flight refresh without restoring stale state", async () => {
93 const building = deferred();
94 const { inbox, persisted } = createHarness({
95 buildDraft: async ({ id, createdAt }) => {
96 await building.promise;
97 return { draft: draft(id, createdAt), cleanup: async () => undefined };
98 },
99 });
100
101 const refresh = inbox.refresh({ ingestNative: true });
102 const consume = inbox.consume("share-stable");
103 building.resolve();
104
105 await expect(refresh).resolves.toEqual([draft("share-stable")]);
106 await expect(consume).resolves.toEqual([]);
107 expect([...persisted.values()]).toEqual([]);
108 });
109
110 it("consumes only the addressed share when another share has identical content", async () => {
111 const { inbox, persisted } = createHarness();
112 persisted.set("share-first", draft("share-first", "2026-07-16T07:59:00.000Z"));
113 persisted.set("share-second", draft("share-second"));
114
115 await expect(inbox.refresh({ ingestNative: false })).resolves.toEqual([
116 draft("share-second"),
117 draft("share-first", "2026-07-16T07:59:00.000Z"),
118 ]);
119 await expect(inbox.consume("share-second")).resolves.toEqual([
120 draft("share-first", "2026-07-16T07:59:00.000Z"),
121 ]);
122 expect([...persisted.values()]).toEqual([draft("share-first", "2026-07-16T07:59:00.000Z")]);
123 });
124
125 it("keeps content-identical shares addressable by their own ids", async () => {
126 const { inbox, persisted } = createHarness();
127 persisted.set("share-open-flow", draft("share-open-flow", "2026-07-16T07:59:00.000Z"));
128 persisted.set("share-newer", draft("share-newer"));
129
130 await expect(inbox.refresh({ ingestNative: false })).resolves.toEqual([
131 draft("share-newer"),
132 draft("share-open-flow", "2026-07-16T07:59:00.000Z"),
133 ]);
134 });
135
136 it("does not acknowledge a supported payload when its durable write fails", async () => {
137 const clearPayloads = vi.fn();
138 const cleanup = vi.fn(async () => undefined);
139 const rollback = vi.fn(async () => undefined);
140 const { inbox } = createHarness({
141 clearPayloads,
142 buildDraft: async ({ id, createdAt }) => ({
143 draft: draft(id, createdAt),
144 cleanup,
145 rollback,
146 }),
147 writeDraft: async () => {
148 throw new Error("disk full");
149 },
150 });
151
152 await expect(inbox.refresh({ ingestNative: true })).rejects.toThrow("disk full");
153 expect(clearPayloads).not.toHaveBeenCalled();
154 expect(cleanup).not.toHaveBeenCalled();
155 expect(rollback).toHaveBeenCalledOnce();
156 });
157
158 it("durably reserves a share for one project before draft import", async () => {
159 const { inbox, persisted } = createHarness();
160 persisted.set("share-stable", draft("share-stable"));
161 const destination = { environmentId: "environment-1", projectId: "project-1" };
162
163 await expect(inbox.reserve("share-stable", destination)).resolves.toEqual([
164 { ...draft("share-stable"), destination },
165 ]);
166 expect(persisted.get("share-stable")?.destination).toEqual(destination);
167 await expect(inbox.reserve("share-stable", destination)).resolves.toEqual([
168 { ...draft("share-stable"), destination },
169 ]);
170 });
171
172 it("rejects moving a reserved share to another project", async () => {
173 const { inbox, persisted } = createHarness();
174 persisted.set("share-stable", {
175 ...draft("share-stable"),
176 destination: { environmentId: "environment-1", projectId: "project-1" },
177 });
178
179 await expect(
180 inbox.reserve("share-stable", {
181 environmentId: "environment-1",
182 projectId: "project-2",
183 }),
184 ).rejects.toThrow("already reserved");
185 expect(persisted.get("share-stable")?.destination?.projectId).toBe("project-1");
186 });
187
188 it("conditionally releases a reservation when its project is unavailable", async () => {
189 const { inbox, persisted } = createHarness();
190 const destination = { environmentId: "environment-1", projectId: "project-1" };
191 persisted.set("share-stable", { ...draft("share-stable"), destination });
192
193 await expect(inbox.releaseReservation("share-stable", destination)).resolves.toEqual([
194 draft("share-stable"),
195 ]);
196 expect(persisted.get("share-stable")?.destination).toBeUndefined();
197
198 await expect(
199 inbox.reserve("share-stable", {
200 environmentId: "environment-2",
201 projectId: "project-2",
202 }),
203 ).resolves.toEqual([
204 {
205 ...draft("share-stable"),
206 destination: { environmentId: "environment-2", projectId: "project-2" },
207 },
208 ]);
209 });
210
211 it("does not release a reservation that changed concurrently", async () => {
212 const { inbox, persisted } = createHarness();
213 persisted.set("share-stable", {
214 ...draft("share-stable"),
215 destination: { environmentId: "environment-2", projectId: "project-2" },
216 });
217
218 await expect(
219 inbox.releaseReservation("share-stable", {
220 environmentId: "environment-1",
221 projectId: "project-1",
222 }),
223 ).rejects.toThrow("reservation changed");
224 expect(persisted.get("share-stable")?.destination?.projectId).toBe("project-2");
225 });
226
227 it("treats an already-removed share as an already-released reservation", async () => {
228 const { inbox, persisted } = createHarness();
229 persisted.set("share-other", draft("share-other"));
230
231 await expect(
232 inbox.releaseReservation("share-stable", {
233 environmentId: "environment-1",
234 projectId: "project-1",
235 }),
236 ).resolves.toEqual([draft("share-other")]);
237 expect([...persisted.values()]).toEqual([draft("share-other")]);
238 });
239});