|
|
@@ -56,6 +56,34 @@ describe('directory-picker-native client half', () => {
|
|
|
for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(1)
|
|
|
})
|
|
|
|
|
|
+ it('rolls back wholesale and reports loudly when a rival provider wins after deferred activation', async () => {
|
|
|
+ const b = await bench()
|
|
|
+ const rejections: unknown[] = []
|
|
|
+ const onUnhandled = (reason: unknown): void => { rejections.push(reason) }
|
|
|
+ // queueMicrotask throws surface as uncaughtException, not a rejection.
|
|
|
+ process.on('unhandledRejection', onUnhandled)
|
|
|
+ process.on('uncaughtException', onUnhandled)
|
|
|
+ try {
|
|
|
+ // This provider activates BEFORE any hole exists: both deferrals wait.
|
|
|
+ // (Duplicate rows of the SAME package converge silently — the deferral
|
|
|
+ // skips a hole its own component already occupies; the conflict needs
|
|
|
+ // a rival provider.)
|
|
|
+ await b.ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
+ b.declare()
|
|
|
+ // A rival occupies both holes ahead of the pending microtask flush.
|
|
|
+ b.slots.register({ name: HOLES[0] } as never, () => null)
|
|
|
+ b.slots.register({ name: HOLES[1] } as never, () => null)
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 20))
|
|
|
+ // The rival keeps both holes; this provider rolled back wholesale and
|
|
|
+ // surfaced the conflict on the fail-loud channel — no partial mix.
|
|
|
+ for (const hole of HOLES) expect(b.slots.entries(hole)).toHaveLength(1)
|
|
|
+ expect(rejections.map(String).join('\n')).toContain('already has a registration')
|
|
|
+ } finally {
|
|
|
+ process.off('unhandledRejection', onUnhandled)
|
|
|
+ process.off('uncaughtException', onUnhandled)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
it('rolls back the first deferral when the second hole is already occupied', async () => {
|
|
|
const b = await bench()
|
|
|
b.declare()
|