Ng eventually #1

Open
Sylvain wants to merge 110 commits from ng-eventually into main
5 changed files with 136 additions and 98 deletions
Showing only changes of commit 4ffa055d62 - Show all commits
+26 -16
View File
@@ -19,18 +19,22 @@ Given('le créateur relaie l\'événement {string}', { timeout: 180000 }, async
Then('le créateur n\'est pas participant de l\'événement {string}', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
const eventId = (this as any).creatorEventId;
// Authoritative: the broker must hold 0 participations for (E, creator). Poll a
// little to absorb any pending write from a just-run leave.
// Wait for the REACTIVE state to settle to "not participating" (observes AD().*
// fed by the subscription push — NOT a broker re-read), the sign the sync has
// passed. Then do ONE authoritative broker read to prove the 0 is durable.
await this.appFrame!.waitForFunction(
(eventId) => {
const td = (window as any).__testData;
const uid = td.currentUserId;
return !!uid && !td.isParticipating(eventId, uid);
},
eventId,
{ timeout: 30000 },
).catch(() => { /* fall through to the single authoritative read */ });
const n = await this.appFrame!.evaluate(async ({ eventId }: { eventId: string }) => {
const td = (window as any).__testData;
const uid = await td.ensureCurrentUser();
let last = -1;
for (let i = 0; i < 8; i++) {
last = await td.authParticipationCount(eventId, uid);
if (last === 0) return 0;
await new Promise(r => setTimeout(r, 750));
}
return last;
return td.authParticipationCount(eventId, uid);
}, { eventId });
expect(n, `creator must NOT be participating in "${title}" (broker count)`).to.equal(0);
});
@@ -68,16 +72,22 @@ When('le créateur quitte son événement {string}', { timeout: 120000 }, async
Then('le créateur est participant de l\'événement {string}', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
const eventId = (this as any).creatorEventId;
// Wait for the REACTIVE join to settle (observes AD().isParticipating fed by the
// subscription push — NOT a broker re-read), then take ONE authoritative broker
// count to prove the participation is durable at the data level.
await this.appFrame!.waitForFunction(
(eventId) => {
const td = (window as any).__testData;
const uid = td.currentUserId;
return !!uid && td.isParticipating(eventId, uid);
},
eventId,
{ timeout: 45000 },
).catch(() => { /* fall through to the single authoritative read */ });
const n = await this.appFrame!.evaluate(async ({ eventId }: { eventId: string }) => {
const td = (window as any).__testData;
const uid = await td.ensureCurrentUser();
let last = 0;
for (let i = 0; i < 20; i++) {
last = await td.authParticipationCount(eventId, uid);
if (last >= 1) return last;
await new Promise(r => setTimeout(r, 1000));
}
return last;
return td.authParticipationCount(eventId, uid);
}, { eventId });
expect(n, `creator must be participating in "${title}" after joining`).to.equal(1);
});
@@ -88,6 +88,9 @@ Then('l\'utilisateur devient participant de l\'événement {string}', async func
// Poll: the participation is written into its own protected doc and read back
// reactively; under a busy wallet that read can lag, so accept the AUTHORITATIVE
// broker count as well (the write is durable regardless of the reactive re-read).
// Wait for the REACTIVE participation to settle (observes AD().liveIsParticipating
// fed by the subscription push — NOT a broker re-read). Generous window: this
// reactive convergence IS the sync-passed signal.
await this.appFrame!.waitForFunction(
(title) => {
const td = (window as any).__testData;
@@ -97,23 +100,16 @@ Then('l\'utilisateur devient participant de l\'événement {string}', async func
},
eventTitle,
{ timeout: 45000 },
).catch(async () => {
// Reactive read lagged — confirm authoritatively against the broker, polling
// to absorb the index-append propagation lag of the per-entity fan-out.
const n = await this.appFrame!.evaluate(async (title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return 0;
const uid = await td.ensureCurrentUser();
for (let i = 0; i < 12; i++) {
const c = await td.authParticipationCount(event['@id'], uid);
if (c > 0) return c;
await new Promise(r => setTimeout(r, 1500));
}
return 0;
}, eventTitle);
expect(n, `participation to "${eventTitle}" must exist on the broker`).to.be.greaterThan(0);
});
).catch(() => { /* fall through to a single authoritative confirmation */ });
// Then ONE authoritative broker read to prove the participation is durable.
const n = await this.appFrame!.evaluate(async (title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return 0;
const uid = await td.ensureCurrentUser();
return td.authParticipationCount(event['@id'], uid);
}, eventTitle);
expect(n, `participation to "${eventTitle}" must exist on the broker`).to.be.greaterThan(0);
});
Then('le broker ne contient plus aucune participation à l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
@@ -97,6 +97,9 @@ Then('l\'utilisateur est participant de l\'événement {string}', async function
// The participation is written into its own protected document and read back
// reactively — poll (the read lags the write against the broker), resolving the
// current user id at call time.
// Wait for the REACTIVE participation to settle (observes AD().isParticipating fed
// by the subscription push — NOT a broker re-read). Generous window: this reactive
// convergence IS the sync-passed signal.
await this.appFrame!.waitForFunction(
(title) => {
const td = (window as any).__testData;
@@ -106,24 +109,18 @@ Then('l\'utilisateur est participant de l\'événement {string}', async function
return !!event && td.isParticipating(event['@id'], uid);
},
eventTitle,
{ timeout: 30000 },
).catch(async () => {
// Reactive read lagged — confirm authoritatively against the broker, polling
// to absorb the index-append propagation lag of the per-entity fan-out.
const n = await this.appFrame!.evaluate(async (title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return 0;
const uid = await td.ensureCurrentUser();
for (let i = 0; i < 12; i++) {
const c = await td.authParticipationCount(event['@id'], uid);
if (c > 0) return c;
await new Promise(r => setTimeout(r, 1500));
}
return 0;
}, eventTitle);
expect(n, `User should be participating in "${eventTitle}"`).to.be.greaterThan(0);
});
{ timeout: 45000 },
).catch(() => { /* fall through to a single authoritative confirmation */ });
// Then ONE authoritative broker read (bypasses the reactive set) to prove the
// participation is durable at the data level.
const n = await this.appFrame!.evaluate(async (title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return 0;
const uid = await td.ensureCurrentUser();
return td.authParticipationCount(event['@id'], uid);
}, eventTitle);
expect(n, `User should be participating in "${eventTitle}"`).to.be.greaterThan(0);
});
Then('l\'utilisateur n\'est plus participant de l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
@@ -163,17 +160,27 @@ Then('le compteur dérivé de l\'événement {string} reflète l\'inscription',
// (The absolute 1→2 convergence stays proven end-to-end by the @multibrowser
// reactive scenario with a real owner A + joiner B.) Poll (the deposit's index
// append + broker sync lag), bounded.
// First wait for the REACTIVE join to settle (observes AD().isParticipating fed by
// the subscription push — the sign the deposit's write path has run — NOT a broker
// re-read). THEN materialize the owner's derived active set from the broker ONCE.
await this.appFrame!.waitForFunction(
(title) => {
const td = (window as any).__testData;
const uid = td.currentUserId;
if (!uid) return false;
const ev = [...td.events].find((e: any) => e.title === title);
return !!ev && td.isParticipating(ev['@id'], uid);
},
eventTitle,
{ timeout: 45000 },
).catch(() => { /* fall through to the single authoritative materialization */ });
const inActive = await this.appFrame!.evaluate(async (title) => {
const td = (window as any).__testData;
const ev = [...td.events].find((e: any) => e.title === title);
const uid = await td.ensureCurrentUser();
if (!ev || !uid) return false;
for (let i = 0; i < 20; i++) {
const users: (string | null)[] = await td.activeRegistrationUsers(ev['@id']);
if (users.includes(uid)) return true;
await new Promise(r => setTimeout(r, 750));
}
return false;
const users: (string | null)[] = await td.activeRegistrationUsers(ev['@id']);
return users.includes(uid);
}, eventTitle);
expect(inActive, `the just-joined user must be in the owner-derived active set for "${eventTitle}"`).to.be.true;
});
@@ -221,19 +228,27 @@ Then('l\'inscription est idempotente pour l\'événement {string}', async functi
// and its index-append propagates async, so a single unpolled read can catch 0
// before the write is queryable (observed flake) — poll until the durable state
// (exactly 1) is visible, which also proves the second join did NOT add a dupe.
// First wait for the REACTIVE participation to settle (observes AD().isParticipating
// fed by the subscription push — the sync-passed signal — NOT a broker re-read).
// THEN read the AUTHORITATIVE broker count ONCE: exactly 1 proves the second join
// added no dupe (bypasses reactive-read lag).
await this.appFrame!.waitForFunction(
(title) => {
const td = (window as any).__testData;
const uid = td.currentUserId;
if (!uid) return false;
const event = [...td.events].find((e: any) => e.title === title);
return !!event && td.isParticipating(event['@id'], uid);
},
eventTitle,
{ timeout: 45000 },
).catch(() => { /* fall through to the single authoritative count */ });
const n = await this.appFrame!.evaluate(async (title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return -1;
const uid = await td.ensureCurrentUser();
let last = 0;
for (let i = 0; i < 20; i++) {
last = await td.authParticipationCount(event['@id'], uid);
if (last === 1) return 1; // exactly one — idempotent, stop early
if (last > 1) return last; // a dupe leaked — fail fast with the real count
await new Promise(r => setTimeout(r, 1000));
}
return last;
return td.authParticipationCount(event['@id'], uid);
}, eventTitle);
expect(n, 'User should have exactly one participation record on the broker').to.equal(1);
});
@@ -20,14 +20,18 @@ Given('l\'identité A crée l\'événement {string} et s\'y inscrit', { timeout:
const aId = await td.ensureCurrentUser();
const created = await td.createEventReal(title);
await td.appJoinEvent(created.id, aId);
// Wait until A's own participation is in A's reactive set (authoritative-ish).
for (let i = 0; i < 30; i++) {
if (td.isParticipating(created.id, aId)) break;
await new Promise(r => setTimeout(r, 500));
}
return { eventId: created.id, aId, aParticipates: td.isParticipating(created.id, aId) };
return { eventId: created.id, aId };
}, title);
expect(out.aParticipates, 'A must be participating in its own event before B arrives').to.be.true;
// OBSERVE the REACTIVE set until A's own join has settled (isParticipating reads
// AD() fed by the subscription push — NOT a broker re-read). This is the barrier
// that A's participation exists before B arrives; no broker polling.
const aParticipates = await this.appFrame!.waitForFunction(
({ eventId, aId }: { eventId: string; aId: string }) =>
(window as any).__testData.isParticipating(eventId, aId),
{ eventId: out.eventId, aId: out.aId },
{ timeout: 45000 },
).then(() => true).catch(() => false);
expect(aParticipates, 'A must be participating in its own event before B arrives').to.be.true;
(this as any).isoEventId = out.eventId;
(this as any).isoEventTitle = title;
(this as any).isoAId = out.aId;
@@ -59,45 +59,58 @@ When('une page fraîche pour la MÊME identité A recharge sur le même wallet',
// flake. This mirrors how the app's own steps already poll participation.
Then('l\'événement {string} est sur l\'accueil de la page fraîche A', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
const freshFrame = (this as any).recoFreshFrame;
const onHome = await freshFrame.evaluate(async (title: string) => {
const td = (window as any).__testData;
for (let i = 0; i < 30; i++) {
if (td.homeEventTitles().includes(title)) return true;
await new Promise(r => setTimeout(r, 500));
}
return td.homeEventTitles().includes(title);
}, title);
// OBSERVE the REACTIVE home set (homeEventTitles reads AD().getUserEvents fed by
// the subscription push — NOT a broker re-read). waitForFunction re-evaluates the
// reactive getter until the cold-start union read converges; no broker polling.
const onHome = await freshFrame.waitForFunction(
(title: string) => (window as any).__testData.homeEventTitles().includes(title),
title,
{ timeout: 30000 },
).then(() => true).catch(() => false);
expect(onHome, `"${title}" MUST appear on the fresh A page's home (getUserEvents(A) after reconnect)`).to.be.true;
});
Then('la page fraîche A est participante de l\'événement {string}', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
const freshFrame = (this as any).recoFreshFrame;
const eventId = (this as any).isoEventId;
const isPart = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => {
const td = (window as any).__testData;
const aId = await td.ensureCurrentUser();
for (let i = 0; i < 30; i++) {
if (td.isParticipating(eventId, aId)) return true;
await new Promise(r => setTimeout(r, 500));
}
return td.isParticipating(eventId, aId);
}, { eventId });
await freshFrame.evaluate(async () => { await (window as any).__testData.ensureCurrentUser(); });
// OBSERVE the REACTIVE participation set (isParticipating reads AD() fed by the
// subscription push — NOT a broker re-read). waitForFunction re-evaluates the
// reactive getter until the cold-start union read converges; no broker polling.
const isPart = await freshFrame.waitForFunction(
(eventId: string) => {
const td = (window as any).__testData;
const aId = td.currentUserId;
return !!aId && td.isParticipating(eventId, aId);
},
eventId,
{ timeout: 30000 },
).then(() => true).catch(() => false);
expect(isPart, `The fresh A page MUST read its own participation in "${title}" after reconnect`).to.be.true;
});
Then('le compte autoritatif de participation de A à l\'événement {string} est {int}', { timeout: 60000 }, async function (this: FestipodWorld, _title: string, expected: number) {
const freshFrame = (this as any).recoFreshFrame;
const eventId = (this as any).isoEventId;
const count = await freshFrame.evaluate(async ({ eventId, expected }: { eventId: string; expected: number }) => {
await freshFrame.evaluate(async () => { await (window as any).__testData.ensureCurrentUser(); });
// First wait for the REACTIVE state to reflect the expectation (the sign the
// cold-start sync has passed — observes AD().isParticipating fed by the push, NOT
// a broker re-read). THEN do ONE authoritative broker read.
await freshFrame.waitForFunction(
({ eventId, expected }: { eventId: string; expected: number }) => {
const td = (window as any).__testData;
const aId = td.currentUserId;
if (!aId) return false;
const isPart = td.isParticipating(eventId, aId);
return expected > 0 ? isPart : !isPart;
},
{ eventId, expected },
{ timeout: 30000 },
).catch(() => { /* fall through to the single authoritative read */ });
const count = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => {
const td = (window as any).__testData;
const aId = await td.ensureCurrentUser();
let last = -1;
for (let i = 0; i < 30; i++) {
last = await td.authParticipationCount(eventId, aId);
if (last === expected) return last;
await new Promise(r => setTimeout(r, 500));
}
return last;
}, { eventId, expected });
return td.authParticipationCount(eventId, aId);
}, { eventId });
expect(count, `Authoritative broker count of A's participation must be ${expected} after reconnect`).to.equal(expected);
});