From 5b8a34d6f581569ba4f99bdee540645272c6d186 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 23 Jun 2026 15:33:44 +0000 Subject: [PATCH 1/4] Add option to manually finish Proteus->MLS for single conversations migration --- changelog.d/2-features/WPB-26018 | 1 + docs/src/understand/team-feature-settings.md | 22 ++++++------------- libs/wire-api/src/Wire/API/Team/Feature.hs | 10 ++++++--- .../ConversationSubsystem/MLS/Migration.hs | 3 ++- tools/db/migrate-features/src/Work.hs | 1 + 5 files changed, 18 insertions(+), 19 deletions(-) create mode 100644 changelog.d/2-features/WPB-26018 diff --git a/changelog.d/2-features/WPB-26018 b/changelog.d/2-features/WPB-26018 new file mode 100644 index 00000000000..a1c120f96b9 --- /dev/null +++ b/changelog.d/2-features/WPB-26018 @@ -0,0 +1 @@ +Add option to manually finish Proteus->MLS for single conversations migration. diff --git a/docs/src/understand/team-feature-settings.md b/docs/src/understand/team-feature-settings.md index 0d07d3b5aff..b7c52cfd41f 100644 --- a/docs/src/understand/team-feature-settings.md +++ b/docs/src/understand/team-feature-settings.md @@ -171,7 +171,7 @@ galley: ## MLS Migration The MLS migration configuration determines client behaviour related to -migration from Proteus to MSL, and defines the criteria enforced by the backend +migration from Proteus to MLS, and defines the criteria enforced by the backend when a conversation is finally migrated to MLS. The settings are the following: @@ -183,20 +183,13 @@ The settings are the following: started. - `finaliseRegardlessAfter`: timestamp of the date by which the migration must be finalised. -- `usersThreshold`: percentage of migrated users needed for migration to - finalise (0-100). -- `clientsThreshold`: percentage of migrated clients needed for migration to - finalise (0-100). +- `allowManualMigration`: allows clients to manually finalise MLS migration for + single conversations. The default is `false`. All of the migration finalisation values are technically optional, but at least -one of them must be specified for the configuration to be valid. If -`finaliseRegardlessAfter` is not set, `usersThreshold` or `clientsThreshold` -should be specified. In case both `usersThreshold` and `clientsThreshold` are -specified, even if one of them is set to 0, both have to be fulfilled for the -migration to be finalised. - -The `finaliseRegardlessAfter` timestamp determines a time after which the -threshold criteria are dropped, and finalisation is allowed in any case. +one of them must be specified for the configuration to be valid. The +`finaliseRegardlessAfter` timestamp determines a time after which the threshold +criteria are dropped, and finalisation is allowed in any case. An example configuration follows: @@ -215,7 +208,6 @@ galley: config: startTime: "2024-05-16T00:00:00.000Z" finaliseRegardlessAfter: "2024-10-17T00:00:00.000Z" - usersThreshold: 100 - clientsThreshold: 50 + allowManualMigration: false lockStatus: locked ``` diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index a08c8675f7b..d185e833c1a 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1449,7 +1449,8 @@ instance IsFeatureConfig MlsE2EIdConfig where data MlsMigrationConfigB t f = MlsMigrationConfig { startTime :: Wear t f (Maybe UTCTime), - finaliseRegardlessAfter :: Wear t f (Maybe UTCTime) + finaliseRegardlessAfter :: Wear t f (Maybe UTCTime), + allowManualMigration :: Wear t f (Maybe Bool) } deriving (BareB, Generic) @@ -1472,16 +1473,18 @@ deriving via (BarbieFeature MlsMigrationConfigB) instance (ToSchema MlsMigration deriving via (RenderableTypeName MlsMigrationConfig) instance (RenderableSymbol MlsMigrationConfig) instance Default MlsMigrationConfig where - def = MlsMigrationConfig Nothing Nothing + def = MlsMigrationConfig Nothing Nothing Nothing instance Arbitrary MlsMigrationConfig where arbitrary = do startTime <- fmap fromUTCTimeMillis <$> arbitrary finaliseRegardlessAfter <- fmap fromUTCTimeMillis <$> arbitrary + allowManualMigration <- arbitrary pure MlsMigrationConfig { startTime = startTime, - finaliseRegardlessAfter = finaliseRegardlessAfter + finaliseRegardlessAfter = finaliseRegardlessAfter, + allowManualMigration = allowManualMigration } instance (Typeable f, NestedMaybe f) => ToSchema (MlsMigrationConfigB Covered f) where @@ -1490,6 +1493,7 @@ instance (Typeable f, NestedMaybe f) => ToSchema (MlsMigrationConfigB Covered f) MlsMigrationConfig <$> startTime .= nestedMaybeField "startTime" (unnamed utcTimeSchema) <*> finaliseRegardlessAfter .= nestedMaybeField "finaliseRegardlessAfter" (unnamed utcTimeSchema) + <*> allowManualMigration .= nestedMaybeField "allowManualMigration" (unnamed schema) instance Default (LockableFeature MlsMigrationConfig) where def = defLockedFeature diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs index 0922d1588f2..9bbb80f0e48 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Migration.hs @@ -59,10 +59,11 @@ checkMigrationCriteria :: Sem r Bool checkMigrationCriteria now conv ws | ws.status == FeatureStatusDisabled = pure False - | afterDeadline = pure True + | afterDeadline || manualMigrationEnabled = pure True | otherwise = unApAll $ mconcat [localUsersMigrated, remoteUsersMigrated] where afterDeadline = maybe False (now >=) ws.config.finaliseRegardlessAfter + manualMigrationEnabled = fromMaybe False ws.config.allowManualMigration containsMLS = Set.member BaseProtocolMLSTag diff --git a/tools/db/migrate-features/src/Work.hs b/tools/db/migrate-features/src/Work.hs index 8aef63d20dd..46837a47990 100644 --- a/tools/db/migrate-features/src/Work.hs +++ b/tools/db/migrate-features/src/Work.hs @@ -337,6 +337,7 @@ writeFeatures ( MlsMigrationConfig @Covered (fmap unOptionalUTCTime mls_migration_start_time) (fmap unOptionalUTCTime mls_migration_finalise_regardless_after) + Nothing ) } From ee6c6ba08fd7cfb06f85bc3065b9abf420b44b55 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 6 Jul 2026 17:04:14 +0000 Subject: [PATCH 2/4] add test for manually migrating Mixed->MLS --- integration/test/Test/MLS.hs | 52 +++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index ca8dc088b64..8ec0997265e 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -394,7 +394,9 @@ testMLSProtocolUpgrade secondDomain = do void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed void $ withWebSocket bob $ \ws -> do - -- charlie is added to the group + -- charlie is added to the group, but he does not become part of the + -- conversation, since the Proteus part of the conversation is the source of + -- truth in mixed conversations void $ uploadNewKeyPackage def charlie1 void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed awaitMatch isNewMLSMessageNotif ws @@ -426,6 +428,54 @@ testMLSProtocolUpgrade secondDomain = do resp.json %. "protocol" `shouldMatch` "mls" resp.json %. "receipt_mode" `shouldMatchInt` 0 +testMLSProtocolUpgradeManual :: (HasCallStack) => Domain -> App () +testMLSProtocolUpgradeManual secondDomain = do + (alice, bob, convId) <- simpleMixedConversationSetup secondDomain + updateReceiptMode alice convId (9 :: Int) >>= assertSuccess + charlie <- randomUser OwnDomain def + + -- alice creates MLS group and bob joins + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] + createGroup def alice1 convId + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + + void $ withWebSocket bob $ \ws -> do + -- charlie is added to the group, but he does not become part of the + -- conversation, since the Proteus part of the conversation is the source of + -- truth in mixed conversations + void $ uploadNewKeyPackage def charlie1 + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + awaitMatch isNewMLSMessageNotif ws + + supportMLS alice + bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mixed" + resp.json %. "receipt_mode" `shouldMatchInt` 9 + + supportMLS bob + + withWebSockets [alice1, bob1] $ \wss -> do + bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "data.protocol" `shouldMatch` "mls" + for_ wss $ \ws -> do + n <- awaitMatch isNewMLSMessageNotif ws + msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 + let leafIndexCharlie = 2 + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie + msg %. "message.content.sender.External" `shouldMatchInt` 0 + + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mls" + -- read receipts should be disabled by default for MLS conversations for now + resp.json %. "receipt_mode" `shouldMatchInt` 0 + testAddUserSimple :: (HasCallStack) => Ciphersuite -> CredentialType -> App () testAddUserSimple suite ctype = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] From 20d41f07243d064b231dafa30f458cbdcaf307ec Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Tue, 7 Jul 2026 08:57:11 +0200 Subject: [PATCH 3/4] fixup! add test for manually migrating Mixed->MLS --- integration/test/Test/MLS.hs | 115 ++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 43 deletions(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 8ec0997265e..d9a63682d22 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -428,53 +428,82 @@ testMLSProtocolUpgrade secondDomain = do resp.json %. "protocol" `shouldMatch` "mls" resp.json %. "receipt_mode" `shouldMatchInt` 0 -testMLSProtocolUpgradeManual :: (HasCallStack) => Domain -> App () -testMLSProtocolUpgradeManual secondDomain = do - (alice, bob, convId) <- simpleMixedConversationSetup secondDomain - updateReceiptMode alice convId (9 :: Int) >>= assertSuccess - charlie <- randomUser OwnDomain def - - -- alice creates MLS group and bob joins - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - createGroup def alice1 convId - void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - - void $ withWebSocket bob $ \ws -> do - -- charlie is added to the group, but he does not become part of the - -- conversation, since the Proteus part of the conversation is the source of - -- truth in mixed conversations - void $ uploadNewKeyPackage def charlie1 - void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - awaitMatch isNewMLSMessageNotif ws - - supportMLS alice - bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do - resp.status `shouldMatchInt` 400 - resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" - bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "protocol" `shouldMatch` "mixed" - resp.json %. "receipt_mode" `shouldMatchInt` 9 - - supportMLS bob +testMLSProtocolUpgradeManual :: (HasCallStack) => App () +testMLSProtocolUpgradeManual = do + -- Without manual migration, the migration criteria must be satisfied. Since + -- bob does not support MLS, the migration from Mixed to MLS is rejected. + do + (alice, bob, convId) <- simpleMixedConversationSetup OwnDomain + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + createGroup def alice1 convId + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - withWebSockets [alice1, bob1] $ \wss -> do + supportMLS alice bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. "data.protocol" `shouldMatch` "mls" - for_ wss $ \ws -> do - n <- awaitMatch isNewMLSMessageNotif ws - msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 - let leafIndexCharlie = 2 - msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie - msg %. "message.content.sender.External" `shouldMatchInt` 0 + resp.json %. "protocol" `shouldMatch` "mixed" - bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "protocol" `shouldMatch` "mls" - -- read receipts should be disabled by default for MLS conversations for now - resp.json %. "receipt_mode" `shouldMatchInt` 0 + -- With manual migration enabled, the migration can be forced even though not + -- all actual members of the Mixed conversation support MLS yet. + withModifiedBackend + ( def + { galleyCfg = + setField + "settings.featureFlags.mlsMigration.defaults.config.allowManualMigration" + True + } + ) + $ \domain -> do + (alice, tid, _) <- createTeam domain 0 + bob <- randomUser domain def + connectUsers [alice, bob] + conv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId + bindResponse (putConversationProtocol bob conv "mixed") $ \resp -> + resp.status `shouldMatchInt` 200 + convId <- getConversation alice (convIdToQidObject conv) >>= getJSON 200 >>= objConvId + updateReceiptMode alice convId (9 :: Int) >>= assertSuccess + charlie <- randomUser domain def + + -- alice creates MLS group and bob joins + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] + createGroup def alice1 convId + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + + void $ withWebSocket bob $ \ws -> do + -- charlie is added to the MLS group, but he does not become part of the + -- conversation, since the Proteus part of the conversation is the source of + -- truth for membership of mixed conversations + void $ uploadNewKeyPackage def charlie1 + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + awaitMatch isNewMLSMessageNotif ws + + supportMLS alice + -- neither bob nor charlie support MLS, but manual migration bypasses the + -- criteria check, so the migration succeeds regardless. + withWebSockets [alice1, bob1] $ \wss -> do + bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "data.protocol" `shouldMatch` "mls" + for_ wss $ \ws -> do + n <- awaitMatch isNewMLSMessageNotif ws + msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 + let leafIndexCharlie = 2 + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie + msg %. "message.content.sender.External" `shouldMatchInt` 0 + + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mls" + -- read receipts should be disabled by default for MLS conversations for now + resp.json %. "receipt_mode" `shouldMatchInt` 0 testAddUserSimple :: (HasCallStack) => Ciphersuite -> CredentialType -> App () testAddUserSimple suite ctype = do From 75971e4a758f2f31929610f34dd396460a4b74e8 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 9 Jul 2026 10:06:26 +0000 Subject: [PATCH 4/4] fixup! add test for manually migrating Mixed->MLS --- integration/test/Test/MLS.hs | 138 +++++++++++++++++------------------ 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index d9a63682d22..fb8f81a1ef9 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -428,82 +428,82 @@ testMLSProtocolUpgrade secondDomain = do resp.json %. "protocol" `shouldMatch` "mls" resp.json %. "receipt_mode" `shouldMatchInt` 0 -testMLSProtocolUpgradeManual :: (HasCallStack) => App () -testMLSProtocolUpgradeManual = do +testMLSProtocolUpgradeManual :: (HasCallStack) => Domain -> App () +testMLSProtocolUpgradeManual secondDomain = do + (alice, tid, _) <- createTeam OwnDomain 1 + I.patchTeamFeature OwnDomain tid "mls" (object ["status" .= "enabled"]) + >>= assertSuccess + bob <- randomUser secondDomain def + connectUsers [alice, bob] + + conv <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId + + bindResponse (putConversationProtocol bob conv "mixed") $ \resp -> + resp.status `shouldMatchInt` 200 + + convId <- + getConversation alice (convIdToQidObject conv) + >>= getJSON 200 + >>= objConvId + + updateReceiptMode alice convId (9 :: Int) >>= assertSuccess + charlie <- randomUser OwnDomain def + [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] + + -- alice creates MLS group and bob joins + createGroup def alice1 convId + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + + void $ withWebSocket bob $ \ws -> do + -- charlie is added to the group, but he does not become part of the + -- conversation, since the Proteus part of the conversation is the source of + -- truth in mixed conversations + void $ uploadNewKeyPackage def charlie1 + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + awaitMatch isNewMLSMessageNotif ws + -- Without manual migration, the migration criteria must be satisfied. Since -- bob does not support MLS, the migration from Mixed to MLS is rejected. - do - (alice, bob, convId) <- simpleMixedConversationSetup OwnDomain - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - createGroup def alice1 convId - void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - - supportMLS alice - bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do - resp.status `shouldMatchInt` 400 - resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" - bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "protocol" `shouldMatch` "mixed" + supportMLS alice + bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mixed" -- With manual migration enabled, the migration can be forced even though not -- all actual members of the Mixed conversation support MLS yet. - withModifiedBackend - ( def - { galleyCfg = - setField - "settings.featureFlags.mlsMigration.defaults.config.allowManualMigration" - True - } + -- I.setTeamFeatureLockStatus alice tid "mlsMigration" "unlocked" + I.patchTeamFeature + OwnDomain + tid + "mlsMigration" + ( object + [ "lockStatus" .= "unlocked", + "config" .= object ["allowManualMigration" .= True] + ] ) - $ \domain -> do - (alice, tid, _) <- createTeam domain 0 - bob <- randomUser domain def - connectUsers [alice, bob] - conv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 - >>= objConvId - bindResponse (putConversationProtocol bob conv "mixed") $ \resp -> - resp.status `shouldMatchInt` 200 - convId <- getConversation alice (convIdToQidObject conv) >>= getJSON 200 >>= objConvId - updateReceiptMode alice convId (9 :: Int) >>= assertSuccess - charlie <- randomUser domain def + >>= assertSuccess + withWebSockets [alice1, bob1] $ \wss -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "data.protocol" `shouldMatch` "mls" + for_ wss $ \ws -> do + n <- awaitMatch isNewMLSMessageNotif ws + msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 + let leafIndexCharlie = 2 + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie + msg %. "message.content.sender.External" `shouldMatchInt` 0 - -- alice creates MLS group and bob joins - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - createGroup def alice1 convId - void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - - void $ withWebSocket bob $ \ws -> do - -- charlie is added to the MLS group, but he does not become part of the - -- conversation, since the Proteus part of the conversation is the source of - -- truth for membership of mixed conversations - void $ uploadNewKeyPackage def charlie1 - void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - awaitMatch isNewMLSMessageNotif ws - - supportMLS alice - -- neither bob nor charlie support MLS, but manual migration bypasses the - -- criteria check, so the migration succeeds regardless. - withWebSockets [alice1, bob1] $ \wss -> do - bindResponse (putConversationProtocol alice convId "mls") $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "data.protocol" `shouldMatch` "mls" - for_ wss $ \ws -> do - n <- awaitMatch isNewMLSMessageNotif ws - msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 - let leafIndexCharlie = 2 - msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie - msg %. "message.content.sender.External" `shouldMatchInt` 0 - - bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "protocol" `shouldMatch` "mls" - -- read receipts should be disabled by default for MLS conversations for now - resp.json %. "receipt_mode" `shouldMatchInt` 0 + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "protocol" `shouldMatch` "mls" + resp.json %. "receipt_mode" `shouldMatchInt` 0 testAddUserSimple :: (HasCallStack) => Ciphersuite -> CredentialType -> App () testAddUserSimple suite ctype = do