fix(cloud): preserve Core-owned collaboration state

This commit is contained in:
dadachann
2026-07-26 12:27:47 +08:00
parent 97b3e58884
commit 94ff4fcd2d
4 changed files with 58 additions and 1 deletions
+4
View File
@@ -234,6 +234,10 @@ class UserService:
"""Create an invited Account and accept its Membership in one transaction."""
normalized_email = normalize_email(user_email)
if self._uses_control_plane_directory():
raise ControlPlaneDirectoryRequiredError(
'Cloud invitation registration must use a Space account to preserve control-plane identity'
)
invitation, _ = await self.ap.workspace_collaboration_service.inspect_invitation(invitation_token)
if invitation.normalized_email != normalized_email:
from ....workspace.collaboration import InvitationEmailMismatchError
@@ -650,6 +650,11 @@ class DirectoryProjectionService:
)
)
continue
if membership.projection_revision == 0:
# Revision zero is Core-owned collaboration state. Directory
# projection seeds memberships, but must not overwrite later
# invitation, role, or removal decisions made by Core.
continue
if membership.uuid != member.membership_uuid:
raise DirectoryProjectionUnavailableError('Directory membership UUID changed for one account')
if membership.projection_revision > member.projection_revision:
@@ -664,7 +669,7 @@ class DirectoryProjectionService:
membership.projection_revision = member.projection_revision
for account_uuid, membership in existing.items():
if account_uuid not in included_accounts:
if account_uuid not in included_accounts and membership.projection_revision != 0:
membership.status = MembershipStatus.REMOVED.value
membership.projection_revision = max(
int(membership.projection_revision),
@@ -639,6 +639,15 @@ class TestUserServiceCreateOrUpdateSpaceUser:
persistence.execute_async.assert_not_awaited()
async def test_cloud_invitation_registration_requires_space_identity(self):
ap = SimpleNamespace(
workspace_service=SimpleNamespace(policy=SimpleNamespace(multi_workspace_enabled=True)),
)
service = UserService(ap)
with pytest.raises(ControlPlaneDirectoryRequiredError, match='Space account'):
await service.register_invited_account('invite-token', 'member@example.com', 'password')
async def test_create_or_update_new_space_user_first_init(self):
"""Creates new Space user on first initialization."""
# Setup
@@ -815,3 +815,42 @@ async def test_snapshot_for_another_instance_is_rejected(projection_context):
with pytest.raises(DirectoryProjectionUnavailableError, match='another LangBot instance'):
await service.initialize()
async def test_core_owned_membership_survives_directory_updates_and_omission(projection_context):
application, session_factory = projection_context
service = DirectoryProjectionService(application, _Provider([_snapshot(1)]), INSTANCE_UUID)
await service.initialize()
async with session_factory() as session:
async with session.begin():
membership = await session.scalar(sqlalchemy.select(WorkspaceMembership))
membership.role = 'viewer'
membership.status = 'active'
membership.projection_revision = 0
session.add(
WorkspaceMembership(
uuid=SECOND_MEMBERSHIP_UUID,
workspace_uuid=WORKSPACE_UUID,
account_uuid='20000000-0000-0000-0000-000000000099',
role='viewer',
status='active',
joined_at=membership.joined_at,
projection_revision=0,
)
)
projected_member = _member(revision=2).model_copy(update={'role': 'owner', 'membership_status': 'removed'})
projected_workspace = _workspace(revision=2).model_copy(update={'members': (projected_member,)})
await service.apply_snapshot(_snapshot(2, workspaces=[projected_workspace]))
async with session_factory() as session:
memberships = {
membership.uuid: membership
for membership in (await session.scalars(sqlalchemy.select(WorkspaceMembership))).all()
}
assert memberships[MEMBERSHIP_UUID].role == 'viewer'
assert memberships[MEMBERSHIP_UUID].status == 'active'
assert memberships[MEMBERSHIP_UUID].projection_revision == 0
assert memberships[SECOND_MEMBERSHIP_UUID].status == 'active'
assert memberships[SECOND_MEMBERSHIP_UUID].projection_revision == 0