mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 20:50:58 +00:00
fix(cloud): converge legacy revision-zero members
This commit is contained in:
@@ -865,6 +865,12 @@ class DirectoryProjectionService:
|
|||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
}
|
}
|
||||||
|
existing_account_sources: dict[str, str] = {}
|
||||||
|
if existing:
|
||||||
|
existing_account_sources = {
|
||||||
|
account.uuid: account.source
|
||||||
|
for account in (await session.scalars(sqlalchemy.select(User).where(User.uuid.in_(existing)))).all()
|
||||||
|
}
|
||||||
included_accounts: set[str] = set()
|
included_accounts: set[str] = set()
|
||||||
for member in candidate.members:
|
for member in candidate.members:
|
||||||
included_accounts.add(member.account_uuid)
|
included_accounts.add(member.account_uuid)
|
||||||
@@ -891,10 +897,13 @@ class DirectoryProjectionService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if membership.projection_revision == 0:
|
if (
|
||||||
# Revision zero is Core-owned collaboration state. Directory
|
membership.projection_revision == 0
|
||||||
# projection seeds memberships, but must not overwrite later
|
and existing_account_sources.get(member.account_uuid) != AccountSource.CLOUD_PROJECTION.value
|
||||||
# invitation, role, or removal decisions made by Core.
|
):
|
||||||
|
# Keep genuinely local collaboration state. Historical Cloud
|
||||||
|
# memberships also used revision zero; once Space includes the
|
||||||
|
# account in its authoritative directory, adopt that row.
|
||||||
continue
|
continue
|
||||||
if membership.uuid != member.membership_uuid:
|
if membership.uuid != member.membership_uuid:
|
||||||
raise DirectoryProjectionUnavailableError('Directory membership UUID changed for one account')
|
raise DirectoryProjectionUnavailableError('Directory membership UUID changed for one account')
|
||||||
@@ -910,7 +919,8 @@ class DirectoryProjectionService:
|
|||||||
membership.projection_revision = member.projection_revision
|
membership.projection_revision = member.projection_revision
|
||||||
|
|
||||||
for account_uuid, membership in existing.items():
|
for account_uuid, membership in existing.items():
|
||||||
if account_uuid not in included_accounts and membership.projection_revision != 0:
|
space_owned = existing_account_sources.get(account_uuid) == AccountSource.CLOUD_PROJECTION.value
|
||||||
|
if account_uuid not in included_accounts and (membership.projection_revision != 0 or space_owned):
|
||||||
membership.status = MembershipStatus.REMOVED.value
|
membership.status = MembershipStatus.REMOVED.value
|
||||||
membership.projection_revision = max(
|
membership.projection_revision = max(
|
||||||
int(membership.projection_revision),
|
int(membership.projection_revision),
|
||||||
|
|||||||
@@ -1056,7 +1056,7 @@ async def test_snapshot_for_another_instance_is_rejected(projection_context):
|
|||||||
await service.initialize()
|
await service.initialize()
|
||||||
|
|
||||||
|
|
||||||
async def test_core_owned_membership_survives_directory_updates_and_omission(projection_context):
|
async def test_revision_zero_membership_in_authoritative_directory_is_adopted(projection_context):
|
||||||
application, session_factory = projection_context
|
application, session_factory = projection_context
|
||||||
service = DirectoryProjectionService(application, _Provider([_snapshot(1)]), INSTANCE_UUID)
|
service = DirectoryProjectionService(application, _Provider([_snapshot(1)]), INSTANCE_UUID)
|
||||||
await service.initialize()
|
await service.initialize()
|
||||||
@@ -1067,11 +1067,45 @@ async def test_core_owned_membership_survives_directory_updates_and_omission(pro
|
|||||||
membership.role = 'viewer'
|
membership.role = 'viewer'
|
||||||
membership.status = 'active'
|
membership.status = 'active'
|
||||||
membership.projection_revision = 0
|
membership.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:
|
||||||
|
membership = await session.scalar(sqlalchemy.select(WorkspaceMembership))
|
||||||
|
assert membership.role == 'owner'
|
||||||
|
assert membership.status == 'removed'
|
||||||
|
assert membership.projection_revision == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_revision_zero_membership_absent_from_authoritative_directory_is_removed(projection_context):
|
||||||
|
application, session_factory = projection_context
|
||||||
|
service = DirectoryProjectionService(application, _Provider([_snapshot(1)]), INSTANCE_UUID)
|
||||||
|
await service.initialize()
|
||||||
|
|
||||||
|
historical_account_uuid = '20000000-0000-0000-0000-000000000099'
|
||||||
|
async with session_factory() as session:
|
||||||
|
async with session.begin():
|
||||||
|
membership = await session.scalar(sqlalchemy.select(WorkspaceMembership))
|
||||||
|
session.add(
|
||||||
|
User(
|
||||||
|
uuid=historical_account_uuid,
|
||||||
|
user='Historical Space Member',
|
||||||
|
normalized_email='historical@example.com',
|
||||||
|
password='',
|
||||||
|
status='active',
|
||||||
|
source='cloud_projection',
|
||||||
|
projection_revision=1,
|
||||||
|
account_type='space',
|
||||||
|
space_account_uuid=historical_account_uuid,
|
||||||
|
)
|
||||||
|
)
|
||||||
session.add(
|
session.add(
|
||||||
WorkspaceMembership(
|
WorkspaceMembership(
|
||||||
uuid=SECOND_MEMBERSHIP_UUID,
|
uuid=SECOND_MEMBERSHIP_UUID,
|
||||||
workspace_uuid=WORKSPACE_UUID,
|
workspace_uuid=WORKSPACE_UUID,
|
||||||
account_uuid='20000000-0000-0000-0000-000000000099',
|
account_uuid=historical_account_uuid,
|
||||||
role='viewer',
|
role='viewer',
|
||||||
status='active',
|
status='active',
|
||||||
joined_at=membership.joined_at,
|
joined_at=membership.joined_at,
|
||||||
@@ -1079,17 +1113,50 @@ async def test_core_owned_membership_survives_directory_updates_and_omission(pro
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
projected_member = _member(revision=2).model_copy(update={'role': 'owner', 'membership_status': 'removed'})
|
await service.apply_snapshot(_snapshot(2))
|
||||||
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:
|
async with session_factory() as session:
|
||||||
memberships = {
|
historical = await session.get(WorkspaceMembership, SECOND_MEMBERSHIP_UUID)
|
||||||
membership.uuid: membership
|
assert historical.status == 'removed'
|
||||||
for membership in (await session.scalars(sqlalchemy.select(WorkspaceMembership))).all()
|
assert historical.projection_revision == 2
|
||||||
}
|
|
||||||
assert memberships[MEMBERSHIP_UUID].role == 'viewer'
|
|
||||||
assert memberships[MEMBERSHIP_UUID].status == 'active'
|
async def test_revision_zero_local_collaboration_survives_directory_omission(projection_context):
|
||||||
assert memberships[MEMBERSHIP_UUID].projection_revision == 0
|
application, session_factory = projection_context
|
||||||
assert memberships[SECOND_MEMBERSHIP_UUID].status == 'active'
|
service = DirectoryProjectionService(application, _Provider([_snapshot(1)]), INSTANCE_UUID)
|
||||||
assert memberships[SECOND_MEMBERSHIP_UUID].projection_revision == 0
|
await service.initialize()
|
||||||
|
|
||||||
|
local_account_uuid = '20000000-0000-0000-0000-000000000098'
|
||||||
|
async with session_factory() as session:
|
||||||
|
async with session.begin():
|
||||||
|
membership = await session.scalar(sqlalchemy.select(WorkspaceMembership))
|
||||||
|
session.add(
|
||||||
|
User(
|
||||||
|
uuid=local_account_uuid,
|
||||||
|
user='Local Collaborator',
|
||||||
|
normalized_email='local@example.com',
|
||||||
|
password='',
|
||||||
|
status='active',
|
||||||
|
source='local',
|
||||||
|
projection_revision=0,
|
||||||
|
account_type='local',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
WorkspaceMembership(
|
||||||
|
uuid=SECOND_MEMBERSHIP_UUID,
|
||||||
|
workspace_uuid=WORKSPACE_UUID,
|
||||||
|
account_uuid=local_account_uuid,
|
||||||
|
role='viewer',
|
||||||
|
status='active',
|
||||||
|
joined_at=membership.joined_at,
|
||||||
|
projection_revision=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.apply_snapshot(_snapshot(2))
|
||||||
|
|
||||||
|
async with session_factory() as session:
|
||||||
|
local = await session.get(WorkspaceMembership, SECOND_MEMBERSHIP_UUID)
|
||||||
|
assert local.status == 'active'
|
||||||
|
assert local.projection_revision == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user