diff --git a/lib/epochs/index.js b/lib/epochs/index.js index 9356d5c..4fc29d2 100644 --- a/lib/epochs/index.js +++ b/lib/epochs/index.js @@ -510,19 +510,16 @@ function BuildPreferredEpoch(ssb, groupId) { } // case 4.6 - groups have overlapping membership, but disjoint - else if ( - intersection(members0, members1).size > 0 - ) { + else if (intersection(members0, members1).size > 0) { // choose one for now, preferredEpoch = tieBreak(tips) // but also kick off resolution fixDisjointEpochsLater(ssb, groupId) // <<< DELAYED SIDE EFFECTS! + } else { + return cb(new Error('unknown membership case!')) } - // case 4.7 - disjoint membership (no overlap!) - - // prettier-ignore - else return cb(Error('Membership case not handled yet')) + // in this case peers should not even know about 2 distinct memberships } else return cb(Error(`case of ${tips.length} tips not handled yet`)) if (preferredEpoch.members) delete preferredEpoch.members diff --git a/test/lib/epochs.test.js b/test/lib/epochs.test.js index 8863c6d..de6eee2 100644 --- a/test/lib/epochs.test.js +++ b/test/lib/epochs.test.js @@ -16,6 +16,9 @@ function getRootIds(peers) { peers.map((peer) => p(peer.metafeeds.findOrCreate)()) ).then((feeds) => feeds.map((feed) => feed.id)) } +function closeAll(peers) { + return Promise.all(peers.map((peer) => p(peer.close)(true))) +} test('lib/epochs (getEpochs, getMembers)', async (t) => { const run = Run(t) @@ -26,7 +29,7 @@ test('lib/epochs (getEpochs, getMembers)', async (t) => { async function sync(label) { return run(`(sync ${label})`, replicate(peers), { isTest: false }) } - t.teardown(() => Promise.all(peers.map((peer) => p(peer.close)(true)))) + t.teardown(async () => await closeAll(peers)) const [aliceId, bobId, oscarId] = await getRootIds(peers) await run( @@ -153,7 +156,7 @@ test('lib/epochs (getMissingMembers)', async (t) => { { isTest: false } ) } - t.teardown(() => Promise.all(peers.map((peer) => p(peer.close)(true)))) + t.teardown(async () => await closeAll(peers)) await run( 'start tribes', @@ -260,7 +263,7 @@ test('lib/epochs (getPreferredEpoch - 4.4. same membership)', async (t) => { Server({ name: 'bob' }), Server({ name: 'oscar' }), ] - t.teardown(() => Promise.all(peers.map((peer) => p(peer.close)(true)))) + t.teardown(async () => await closeAll(peers)) const [alice, bob, oscar] = peers const [bobId, oscarId] = await getRootIds([bob, oscar]) @@ -381,7 +384,7 @@ test('lib/epochs (getPreferredEpoch - 4.5. subset membership)', async (t) => { Server({ name: 'carol' }), Server({ name: 'oscar' }), ] - t.teardown(() => Promise.all(peers.map((peer) => p(peer.close)(true)))) + t.teardown(async () => await closeAll(peers)) const [alice, bob, carol, oscar] = peers const [bobId, carolId, oscarId] = await getRootIds([bob, carol, oscar]) @@ -457,7 +460,7 @@ test('lib/epochs (getPreferredEpoch - 4.6. overlapping membership)', async (t) = Server({ name: 'carol' }), Server({ name: 'oscar' }), ] - t.teardown(() => peers.forEach((peer) => peer.close(true))) + t.teardown(async () => await closeAll(peers)) const [alice, bob, carol, oscar] = peers const [bobId, carolId, oscarId] = await getRootIds([bob, carol, oscar]) @@ -481,6 +484,7 @@ test('lib/epochs (getPreferredEpoch - 4.6. overlapping membership)', async (t) = 'others accept invites', Promise.all([ bob.tribes2.acceptInvite(group.id), + carol.tribes2.acceptInvite(group.id), oscar.tribes2.acceptInvite(group.id), ]) ) @@ -520,8 +524,97 @@ test('lib/epochs (getPreferredEpoch - 4.6. overlapping membership)', async (t) = t.end() }) -test.skip('lib/epochs (getPreferredEpoch - 4.7. disjoint membership)', async (t) => { +test('lib/epochs (getPreferredEpoch - 4.7. disjoint membership)', async (t) => { // there is no conflict in this case (doesn't need testing?) + // alice starts a group, adds bob, carol, oscar + // simultaneously: + // - alice excludes carol, oscar + // - oscar excludes alice, bob + // + // the group split! + + const run = Run(t) + + // + const peers = [ + Server({ name: 'alice' }), + Server({ name: 'bob' }), + Server({ name: 'carol' }), + Server({ name: 'oscar' }), + ] + t.teardown(async () => await closeAll(peers)) + + const [alice, bob, carol, oscar] = peers + const [aliceId, bobId, carolId, oscarId] = await getRootIds([ + alice, + bob, + carol, + oscar, + ]) + await run( + 'start tribes', + Promise.all(peers.map((peer) => peer.tribes2.start())) + ) + + const group = await run('alice creates a group', alice.tribes2.create({})) + + await run('(sync dm feeds)', replicate(alice, bob, carol, oscar)) + + await run( + 'alice invites bob, carol, oscar', + alice.tribes2.addMembers(group.id, [bobId, carolId, oscarId], {}) + ) + + await run('(sync dm feeds)', replicate(alice, bob, carol, oscar)) + + await run( + 'others accept invites', + Promise.all([ + bob.tribes2.acceptInvite(group.id), + carol.tribes2.acceptInvite(group.id), + oscar.tribes2.acceptInvite(group.id), + ]) + ) + // + + await Promise.all([ + run( + 'alice excludes carol, oscar', + alice.tribes2.excludeMembers(group.id, [carolId, oscarId], {}) + ), + run( + 'oscar excludes alice, bob', + oscar.tribes2.excludeMembers(group.id, [aliceId, bobId], {}) + ), + ]) + + await run( + '(sync exclusions)', + Promise.all([ + replicate(alice, bob), + replicate(oscar, carol), + replicate(alice, oscar), + ]) + ) + + const DELAY = 1000 + console.log('sleep', DELAY) // eslint-disable-line + await p(setTimeout)(DELAY) + + const aliceTips = await Epochs(alice).getTipEpochs(group.id) + const oscarTips = await Epochs(oscar).getTipEpochs(group.id) + t.equal(aliceTips.length, 1, 'alice sees only one tip') + t.equal(oscarTips.length, 1, 'oscar sees only one tip') + + const [alicePreferred, bobPreferred, carolPreferred, oscarPreferred] = + await Promise.all( + peers.map((peer) => Epochs(peer).getPreferredEpoch(group.id)) + ) + + t.deepEqual(alicePreferred, bobPreferred, 'alice and bob agree epoch') + t.deepEqual(carolPreferred, oscarPreferred, 'carol and oscar agree epoch') + t.notDeepEqual(alicePreferred, oscarPreferred, 'the group is split') + t.end() })