cautpartener / client / e2e / matching.e2e.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
// P2+P3 matching e2e — Tinder-style pool cards + likes as sealed dead-drops
// (0x12), hidden until mutual, then a private match conversation WITHOUT any
// contact relationship:
//   A and B (NOT contacts) both publish cards in jocuri|bucuresti; A browses
//   the pool, sees B's card and LIKES (0x12 wrap into B's inbox + {t:"mlike"}
//   home op — no match yet on A). B polls the inbox: "1 persoană te-a
//   apreciat" with NO sender identity (the parsed wraps and the received
//   store carry no pub of A); B then likes A's card → MUTUAL detected
//   instantly (token intersection) → match row. B opens the match chat and
//   sends a message; A detects the mutual on its own poll and reads the
//   message — both sides derived the SAME room from the pubLo like's
//   roomSecret. "Adaugă în contacte" runs INSIDE the match chat (card control
//   ops + mandatory fingerprint sheets both ways) → verified contact on both
//   sides. C likes A without reciprocation → A sees count 1, no identity, no
//   new match. Junk 0x12 deposits are dropped silently. A's SECOND device
//   (seed import) sees the match + conversation via home-log mlike sync +
//   inbox re-poll.
// Run: node e2e/matching.e2e.mjs        (HEADED=1 to watch)
// NEVER put a seed, token or roomSecret (or any part of one) into a fail
// message — it is logged.
import { decode as cborDecode } from "@ipld/dag-cbor";
import {
  createHarness,
  fail,
  importIdentity,
  ok,
  sleep,
  waitAllRoomsDrained,
  waitChatReady,
  waitContactsReady,
} from "./helpers.mjs";

const PORT = 5194;
const STUB_PORT = 8802;
const POOL = "jocuri|bucuresti";

const harness = createHarness({ port: PORT, stubPort: STUB_PORT });
const BASE = harness.base;

const BLURB_A = "Caut coechipier pentru Catan și Azul, serile de joi.";
const BLURB_B = "Jucător de board games, disponibil în weekend.";
const MSG_B = "Salut! Ne-am potrivit — jucăm joi?";

const myPubOf = (page) => page.evaluate(() => window.__cp.identity.identity.publicKeyB64);
const stubBase = `http://localhost:${STUB_PORT}`;

/** Onboarding that KEEPS the seed for the second-device import (never logged). */
async function createIdentityKeepSeed(page) {
  await page.goto(BASE);
  await page.waitForSelector('[data-testid="onboarding"]', { timeout: 20000 });
  await page.click('[data-testid="create-id"]');
  await page.waitForSelector('[data-testid="seed-text"]');
  const seed = (await page.textContent('[data-testid="seed-text"]')).trim();
  await page.check('[data-testid="backup-check"]');
  await page.fill('[data-testid="backup-last4"]', seed.slice(-4));
  await page.waitForSelector('[data-testid="backup-confirm"]:not([disabled])');
  await page.click('[data-testid="backup-confirm"]');
  await page.waitForSelector('[data-testid="tab-profile"]', { timeout: 20000 });
  return seed;
}

/** Publish a minimal profile (name + county + jocuri) — the card carries the name. */
async function publishProfile(page, name) {
  await page.click('[data-testid="tab-profile"]');
  await page.waitForSelector('[data-testid="pf-name"]', { timeout: 30000 });
  await page.fill('[data-testid="pf-name"]', name);
  await page.selectOption('[data-testid="pf-county"]', "bucuresti");
  await page.click('[data-testid="pf-cat"][data-cat="jocuri"]');
  await page.click('[data-testid="pf-publish"]');
  await page.waitForFunction(
    () => document.querySelector('[data-testid="pf-status"]')?.textContent.includes("publicat:"),
    undefined,
    { timeout: 30000 },
  );
}

/** Enter the jocuri|bucuresti feed (via the picker), then the Potriviri segment. */
async function enterMatch(page) {
  await page.click('[data-testid="tab-feed"]');
  await page.waitForSelector('[data-testid="feed-frame"]', { timeout: 20000 });
  if ((await page.getAttribute('[data-testid="feed-frame"]', "data-shard")) !== "jocuri|bucuresti") {
    await page.selectOption('[data-testid="feed-county"]', "bucuresti");
    await page.waitForSelector('[data-testid="feed-frame"][data-shard$="|bucuresti"]', { timeout: 20000 });
    await page.click('[data-testid="feed-cat"][data-cat="jocuri"]');
    await page.waitForSelector('[data-testid="feed-frame"][data-shard="jocuri|bucuresti"]', { timeout: 20000 });
  }
  await page.click('[data-testid="feed-seg-match"]');
  await page.waitForSelector('[data-testid="mt-mycard"]', { timeout: 20000 });
  // wait for the pool browse to settle (stack or empty note rendered)
  await page.waitForSelector('[data-testid="mt-card"], [data-testid="mt-empty"]', { timeout: 30000 });
}

/** Activate my card in the current pool with `blurb`. */
async function activateCard(page, blurb) {
  await page.click('[data-testid="mt-activate"]');
  await page.waitForSelector('[data-testid="mt-blurb"]');
  await page.fill('[data-testid="mt-blurb"]', blurb);
  await page.click('[data-testid="mt-card-publish"]');
  await page.waitForSelector('[data-testid="mt-mycard-status"][data-active="1"]', { timeout: 30000 });
}

/** Skip stack cards until `pub` is on top, then like it. */
async function likePub(page, pub) {
  for (let i = 0; i < 10; i++) {
    const top = await page.getAttribute('[data-testid="mt-card"]', "data-pub");
    if (top === pub) {
      await page.click('[data-testid="mt-like"]');
      return;
    }
    await page.click('[data-testid="mt-skip"]');
    // the fly-out animation removes the old card ~150 ms later — wait for the
    // TOP CARD to actually change (or the stack to empty) before re-reading
    await page.waitForFunction(
      (prev) => {
        const el = document.querySelector('[data-testid="mt-card"]');
        return !el || el.dataset.pub !== prev;
      },
      top,
      { timeout: 10000 },
    );
    if (!(await page.$('[data-testid="mt-card"]'))) break;
  }
  fail("the target card never surfaced in the stack");
}

const likedCount = (page) => page.getAttribute('[data-testid="mt-liked-count"]', "data-n");

/** The pool/inbox mailbox room ids, computed by the CLIENT's own derivations. */
const poolRoomIdOf = (page) =>
  page.evaluate(async (pool) => {
    const { poolRoom } = await import("/src/public/public-rooms.ts");
    return await poolRoom(pool);
  }, POOL);
const inboxRoomIdOf = (page, pub) =>
  page.evaluate(async (p) => {
    const { inboxRoom } = await import("/src/public/public-rooms.ts");
    return await inboxRoom(p);
  }, pub);

/** Read every message of a stub room forward (node-side, bearer stub token). */
async function readRoom(roomId) {
  const auth = { authorization: `Bearer stub:${roomId}` };
  const url = `${stubBase}/mailbox/v1/rooms/${encodeURIComponent(roomId)}/messages`;
  const msgs = [];
  for (let after = null; ; ) {
    const q = after ? `?limit=100&after=${encodeURIComponent(after)}` : "?limit=100";
    const r = await (await fetch(url + q, { headers: auth })).json();
    msgs.push(...r.messages);
    if (!r.has_more || r.messages.length === 0) break;
    after = r.messages.at(-1).seq;
  }
  return msgs;
}

// ---- scenario ----------------------------------------------------------------

async function main() {
  await harness.start();

  const dirA = harness.newProfileDir("cp-mt-a-");
  const dirA2 = harness.newProfileDir("cp-mt-a2-");
  const dirB = harness.newProfileDir("cp-mt-b-");
  const dirC = harness.newProfileDir("cp-mt-c-");

  // --- A: identity + profile + card in the pool --------------------------------
  let A = await harness.openDevice("A", dirA);
  const seedA = await createIdentityKeepSeed(A.page);
  await waitContactsReady(A.page);
  const pubA = await myPubOf(A.page);
  await publishProfile(A.page, "Ana");
  await enterMatch(A.page);
  if ((await likedCount(A.page)) !== "0") fail("A starts with a non-zero liked count");
  await activateCard(A.page, BLURB_A);
  const poolRoomId = await poolRoomIdOf(A.page);
  {
    let count = 0;
    for (let i = 0; i < 40 && count < 1; i++) {
      count = await harness.roomCount(poolRoomId);
      if (count < 1) await sleep(500);
    }
    if (count < 1) fail("A's card did not reach the pool room");
  }
  const inboxA = await inboxRoomIdOf(A.page, pubA);
  await waitAllRoomsDrained(A.page);
  await A.close();
  ok("A: profile + pool card published (cp-card|v1 landed in poolRoom)");

  // --- B: identity + card; sees A's card in the stack --------------------------
  let B = await harness.openDevice("B", dirB);
  await createIdentityKeepSeed(B.page);
  await waitContactsReady(B.page);
  const pubB = await myPubOf(B.page);
  await publishProfile(B.page, "Bogdan");
  await enterMatch(B.page);
  await B.page.waitForSelector(`[data-testid="mt-card"][data-pub="${pubA}"]`, { timeout: 30000 });
  const cardName = (await B.page.textContent('[data-testid="mt-card-name"]')).trim();
  if (cardName !== "Ana") fail(`A's card renders name "${cardName}"`);
  const cardBlurb = (await B.page.textContent('[data-testid="mt-card-blurb"]')).trim();
  if (cardBlurb !== BLURB_A) fail("A's card blurb does not render verbatim");
  await activateCard(B.page, BLURB_B);
  const inboxB = await inboxRoomIdOf(B.page, pubB);
  await waitAllRoomsDrained(B.page);
  await B.close();
  ok("B: sees A's verified card (name + blurb) in the stack; B's own card published");

  // --- A: browses, likes B — wrap deposited, mlike stored, NO match yet --------
  A = await harness.openDevice("A2", dirA);
  await A.page.goto(BASE);
  await waitContactsReady(A.page);
  await enterMatch(A.page);
  await A.page.waitForSelector(`[data-testid="mt-card"][data-pub="${pubB}"]`, { timeout: 30000 });
  const inboxBBefore = await harness.roomCount(inboxB);
  await likePub(A.page, pubB);
  await A.page.waitForFunction(() => window.__cp.selfroom.state.mlikes.size === 1, undefined, { timeout: 30000 });
  const mlike = await A.page.evaluate(() => {
    const l = [...window.__cp.selfroom.state.mlikes.values()][0];
    // identity-safe projection: NEVER extract token/roomSecret to the test log
    return { target: l.target, pool: l.pool, hasToken: /^[A-Za-z0-9_-]{43}$/.test(l.token), hasSecret: /^[A-Za-z0-9_-]{43}$/.test(l.roomSecret) };
  });
  if (mlike.target !== pubB || mlike.pool !== POOL || !mlike.hasToken || !mlike.hasSecret) {
    fail("the mlike home op is malformed");
  }
  {
    let count = inboxBBefore;
    for (let i = 0; i < 40 && count <= inboxBBefore; i++) {
      count = await harness.roomCount(inboxB);
      if (count <= inboxBBefore) await sleep(500);
    }
    if (count <= inboxBBefore) fail("A's like wrap did not reach B's inbox room");
  }
  if ((await A.page.evaluate(() => window.__cp.match.matches().length)) !== 0) fail("A has a match before reciprocity");
  await A.page.waitForSelector('[data-testid="mt-no-matches"]', { timeout: 10000 });
  // the card that was liked never re-renders in the stack
  if (await A.page.$(`[data-testid="mt-card"][data-pub="${pubB}"]`)) fail("a liked card still shows in the stack");
  await waitAllRoomsDrained(A.page);
  await A.close();
  ok("A: liked B — 0x12 wrap in B's inbox + mlike home op; NO match, no leak on A");

  // --- wire-level anonymity: B's inbox wraps carry NO sender identity ----------
  {
    const msgs = await readRoom(inboxB);
    const wraps = [];
    for (const m of msgs) {
      const raw = Buffer.from(m.payload, "base64");
      if (raw[0] !== 0x12) continue;
      wraps.push(cborDecode(raw.subarray(1)));
    }
    if (wraps.length !== 1) fail(`expected exactly 1 wrap in B's inbox, found ${wraps.length}`);
    const w = wraps[0];
    if (w.to !== pubB) fail("the wrap is not addressed to B");
    const flat = JSON.stringify(w);
    if (flat.includes(pubA)) fail("the wrap envelope leaks the sender's pub");
    // and the raw bytes never contain A's pub bytes either
    const rawWrap = Buffer.from(msgs.find((m) => Buffer.from(m.payload, "base64")[0] === 0x12).payload, "base64");
    if (rawWrap.includes(Buffer.from(pubA, "base64url"))) fail("the wrap bytes leak the sender's raw pub");
    ok("wire: the sealed like names only the RECIPIENT — no sender identity in the envelope or bytes");
  }

  // --- B: polls — anonymous count 1; likes back → INSTANT mutual ---------------
  B = await harness.openDevice("B2", dirB);
  await B.page.goto(BASE);
  await waitContactsReady(B.page);
  await enterMatch(B.page);
  await B.page.waitForFunction(
    () => document.querySelector('[data-testid="mt-liked-count"]')?.dataset.n === "1",
    undefined,
    { timeout: 60000 },
  );
  const likedLine = (await B.page.textContent('[data-testid="mt-liked-count"]')).trim();
  if (!likedLine.includes("1 persoană te-a apreciat")) fail(`liked-count line reads "${likedLine}"`);
  // the counter section never names anyone; the received store holds no pub
  const likesSectionText = await B.page.$eval(".mt-likes", (el) => el.innerText);
  if (likesSectionText.includes(pubA.slice(0, 10)) || likesSectionText.includes("Ana")) {
    fail("the liked-count section leaks the liker's identity");
  }
  const storeLeak = await B.page.evaluate((p) => {
    const raw = localStorage.getItem("cautpartener:match-recv") ?? "";
    return raw.includes(p);
  }, pubA);
  if (storeLeak) fail("the received-like store contains the sender's pub");
  ok('B: "1 persoană te-a apreciat" — anonymous (no pub, no name anywhere in count or store)');

  await B.page.waitForSelector(`[data-testid="mt-card"][data-pub="${pubA}"]`, { timeout: 30000 });
  await likePub(B.page, pubA);
  await B.page.waitForSelector(`[data-testid="mt-match-row"][data-pub="${pubA}"]`, { timeout: 30000 });
  const bMatches = await B.page.evaluate(() => window.__cp.match.matches());
  if (bMatches.length !== 1 || bMatches[0].peer !== pubA || bMatches[0].name !== "Ana") {
    fail("B's mutual match is not derived correctly");
  }
  // count drops back to 0 — that token is now a match, not an anonymous like
  if ((await likedCount(B.page)) !== "0") fail("B's anonymous count did not drop after the mutual");
  ok("B: liking back detected the mutual INSTANTLY (token intersection) — match row for Ana");

  // --- B: match chat works without any contact relationship --------------------
  await B.page.click(`[data-testid="mt-match-row"][data-pub="${pubA}"]`);
  await B.page.waitForSelector('[data-testid="chat-view"]', { timeout: 30000 });
  await B.page.waitForSelector('[data-testid="chat-match-badge"]', { timeout: 30000 });
  await B.page.waitForSelector('[data-testid="mc-add-open"]', { timeout: 10000 });
  await B.page.fill('[data-testid="chat-input"]', MSG_B);
  await B.page.click('[data-testid="chat-send"]');
  await B.page.waitForSelector('[data-testid="chat-msg"]', { timeout: 30000 });
  await waitAllRoomsDrained(B.page);
  await B.close();
  ok("B: match conversation opened (badge + add-contact affordance) and a message sent");

  // --- A: detects the mutual on poll; SAME room delivers B's message -----------
  A = await harness.openDevice("A3", dirA);
  await A.page.goto(BASE);
  await waitChatReady(A.page);
  await A.page.waitForFunction(
    (p) => window.__cp.match.matches().some((m) => m.peer === p),
    pubB,
    { timeout: 90000 },
  );
  await A.page.goto(`${BASE}/#/c/m~${pubB}`);
  await A.page.waitForSelector('[data-testid="chat-view"]', { timeout: 30000 });
  await A.page.waitForFunction(
    (msg) => [...document.querySelectorAll('[data-testid="chat-msg"]')].some((el) => el.textContent.includes(msg)),
    MSG_B,
    { timeout: 60000 },
  );
  ok("A: mutual detected via inbox poll — B's message arrives (both sides derived the SAME room)");

  // conversation list shows the match badge
  await A.page.goto(`${BASE}/#/`);
  await A.page.click('[data-testid="tab-chats"]');
  await A.page.waitForSelector(`[data-testid="conv-row"][data-conv="m~${pubB}"]`, { timeout: 30000 });
  await A.page.waitForSelector(`[data-testid="conv-row"][data-conv="m~${pubB}"] [data-testid="conv-match-badge"]`, { timeout: 10000 });
  ok("A: Conversații lists the match with the ❤️ badge");

  // --- in-chat contact exchange: A offers a card -------------------------------
  await A.page.goto(`${BASE}/#/c/m~${pubB}`);
  await A.page.waitForSelector('[data-testid="mc-add-open"]', { timeout: 30000 });
  await A.page.click('[data-testid="mc-add-open"]');
  await A.page.waitForSelector('[data-testid="mc-add-sheet"]');
  await A.page.fill('[data-testid="mc-add-name"]', "Ana");
  await A.page.click('[data-testid="mc-add-send"]');
  await A.page.waitForFunction(() => !document.querySelector('[data-testid="mc-add-sheet"]'), undefined, { timeout: 30000 });
  await waitAllRoomsDrained(A.page);
  await A.close();
  ok("A: contact card offered inside the match chat (control op)");

  // --- B: reviews A's card (fingerprint sheet), adds, sends own card back ------
  B = await harness.openDevice("B3", dirB);
  await B.page.goto(`${BASE}/#/c/m~${pubA}`);
  await waitChatReady(B.page);
  await B.page.waitForSelector('[data-testid="mc-card-offer"]', { timeout: 60000 });
  const offer = (await B.page.textContent('[data-testid="mc-card-offer"]')).trim();
  if (!offer.includes("Ana")) fail(`card offer reads "${offer}"`);
  await B.page.click('[data-testid="mc-card-review"]');
  await B.page.waitForSelector('[data-testid="mc-review-sheet"]', { timeout: 10000 });
  const fpEmoji = (await B.page.textContent('[data-testid="mc-review-fp"]')).trim();
  if (fpEmoji.length === 0) fail("the review sheet shows no fingerprint");
  await B.page.click('[data-testid="mc-review-yes"]');
  // reciprocity: my-card sheet auto-opens — send it back
  await B.page.waitForSelector('[data-testid="mc-add-sheet"]', { timeout: 30000 });
  await B.page.fill('[data-testid="mc-add-name"]', "Bogdan");
  await B.page.click('[data-testid="mc-add-send"]');
  // the sheet closes only after the card op is appended (and thus enqueued)
  await B.page.waitForFunction(() => !document.querySelector('[data-testid="mc-add-sheet"]'), undefined, {
    timeout: 30000,
  });
  await B.page.waitForFunction(
    (p) => window.__cp.contacts.state.contacts.get(p)?.status === "active",
    pubA,
    { timeout: 30000 },
  );
  await B.page.waitForSelector('[data-testid="mc-contact-ok"]', { timeout: 30000 });
  await waitAllRoomsDrained(B.page);
  await B.close();
  ok("B: fingerprint sheet → A added as VERIFIED contact; own card sent back");

  // --- A: reviews B's card — contact established both ways ---------------------
  A = await harness.openDevice("A4", dirA);
  await A.page.goto(`${BASE}/#/c/m~${pubB}`);
  await waitChatReady(A.page);
  await A.page.waitForSelector('[data-testid="mc-card-offer"]', { timeout: 60000 });
  await A.page.click('[data-testid="mc-card-review"]');
  await A.page.waitForSelector('[data-testid="mc-review-sheet"]', { timeout: 10000 });
  await A.page.click('[data-testid="mc-review-yes"]');
  await A.page.waitForFunction(
    (p) => window.__cp.contacts.state.contacts.get(p)?.status === "active",
    pubB,
    { timeout: 30000 },
  );
  await A.page.waitForSelector('[data-testid="mc-contact-ok"]', { timeout: 30000 });
  await waitAllRoomsDrained(A.page);
  await A.close();
  ok("A: B's card reviewed (fingerprint) — verified contact on BOTH sides, no rendezvous link needed");

  // --- C: likes A without reciprocation ----------------------------------------
  const C = await harness.openDevice("C", dirC);
  await createIdentityKeepSeed(C.page);
  await waitContactsReady(C.page);
  const pubC = await myPubOf(C.page);
  await enterMatch(C.page);
  await C.page.waitForSelector('[data-testid="mt-card"]', { timeout: 30000 });
  await likePub(C.page, pubA);
  await C.page.waitForFunction(() => window.__cp.selfroom.state.mlikes.size === 1, undefined, { timeout: 30000 });
  await waitAllRoomsDrained(C.page);
  await C.close();
  ok("C: liked A (stranger, one-way)");

  // --- A: count 1 (anonymous), still exactly ONE match; junk 0x12 dropped ------
  A = await harness.openDevice("A5", dirA);
  await A.page.goto(BASE);
  await waitContactsReady(A.page);
  await enterMatch(A.page);
  await A.page.waitForFunction(
    () => document.querySelector('[data-testid="mt-liked-count"]')?.dataset.n === "1",
    undefined,
    { timeout: 60000 },
  );
  const aLikes = await A.page.$eval(".mt-likes", (el) => el.innerText);
  if (aLikes.includes(pubC.slice(0, 10))) fail("C's identity leaks into A's liked-count section");
  let aMatches = await A.page.evaluate(() => window.__cp.match.matches());
  if (aMatches.length !== 1 || aMatches[0].peer !== pubB) fail("C's one-way like minted a phantom match");
  ok("A: C's like shows as an ANONYMOUS count of 1 — no identity, no match");

  // junk 0x12: garbage wrap envelopes are dropped silently under the budget
  {
    const auth = { authorization: `Bearer stub:${inboxA}` };
    const url = `${stubBase}/mailbox/v1/rooms/${encodeURIComponent(inboxA)}/messages`;
    for (let i = 0; i < 5; i++) {
      const junk = Buffer.concat([Buffer.from([0x12]), Buffer.from(`junk-${i}-`.repeat(8))]);
      const r = await fetch(url, { method: "POST", headers: auth, body: junk });
      if (r.status !== 201) fail(`junk deposit failed (${r.status})`);
    }
  }
  await A.page.click('[data-testid="feed-refresh"]');
  await sleep(3000);
  const afterJunk = await A.page.evaluate(() => ({
    liked: window.__cp.match.likedMeCount(),
    matches: window.__cp.match.matches().length,
  }));
  if (afterJunk.liked !== 1 || afterJunk.matches !== 1) fail("junk 0x12 changed the derived state");
  await A.close();
  ok("junk 0x12 deposits are dropped silently — counts and matches unchanged");

  // --- A's SECOND device: seed import → match + conversation appear ------------
  const A2 = await harness.openDevice("A6", dirA2);
  await importIdentity(A2.page, BASE, seedA);
  await waitChatReady(A2.page);
  // home-log sync delivers the mlike ops; the boot inbox poll re-ingests the
  // received wraps — the mutual re-derives with NO device-local state carried
  await A2.page.waitForFunction(
    (p) => window.__cp.match.matches().some((m) => m.peer === p && m.name === "Bogdan"),
    pubB,
    { timeout: 120000 },
  );
  await A2.page.goto(`${BASE}/#/c/m~${pubB}`);
  await A2.page.waitForSelector('[data-testid="chat-view"]', { timeout: 30000 });
  await A2.page.waitForFunction(
    (msg) => [...document.querySelectorAll('[data-testid="chat-msg"]')].some((el) => el.textContent.includes(msg)),
    MSG_B,
    { timeout: 60000 },
  );
  await A2.close();
  ok("A's second device: mlike home-log sync + inbox re-poll re-derive the match AND the same room (message readable)");
}

let exitCode = 0;
try {
  await main();
  console.log(
    "\n🎉 matching: pool cards + anonymous sealed likes + instant mutual + same-room chat + in-chat contact exchange + one-way anonymity + junk resilience + second-device sync passed",
  );
} catch (err) {
  console.error("❌ " + (err?.message ?? err));
  exitCode = 1;
} finally {
  await harness.shutdown();
}
process.exit(exitCode);

static mirror of HEAD · about · clone: git clone https://cod-sursa.cautpartener.ro/cautpartener.git