feat(workspace): add in-product collaboration and direct Cloud launch

This commit is contained in:
dadachann
2026-07-26 10:40:36 +08:00
parent 276791e7af
commit 8a445bfb22
29 changed files with 1258 additions and 124 deletions
@@ -0,0 +1,88 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from langbot.pkg.workspace.invitation_delivery import (
InvitationDeliveryResult,
InvitationDeliveryService,
)
pytestmark = pytest.mark.asyncio
def _app(config: dict) -> SimpleNamespace:
return SimpleNamespace(
instance_config=SimpleNamespace(data=config),
logger=SimpleNamespace(warning=lambda *args, **kwargs: None),
)
async def test_link_only_delivery_builds_public_web_link_without_secret_capability():
service = InvitationDeliveryService(
_app(
{
'api': {
'webui_url': 'https://public.langbot.example/',
'webhook_prefix': 'http://internal:5300',
}
}
)
)
assert service.build_invitation_link('lbi_secret') == (
'https://public.langbot.example/invitations/accept#token=lbi_secret'
)
assert service.capability() == {'enabled': False, 'provider': None}
result = await service.deliver_invitation(
recipient_email='member@example.com',
workspace_name='Workspace',
invitation_link='https://public.langbot.example/invitations/accept#token=lbi_secret',
)
assert result == InvitationDeliveryResult(status='link_only', provider=None)
async def test_configured_provider_failure_returns_failed_without_raising():
service = InvitationDeliveryService(
_app(
{
'workspace': {
'invitations': {
'email': {
'provider': 'resend',
'from': 'LangBot <noreply@example.com>',
'resend': {'api_key': 'resend-secret'},
}
}
}
}
)
)
service._send_resend = AsyncMock(return_value=False)
assert service.capability() == {'enabled': True, 'provider': 'resend'}
result = await service.deliver_invitation(
recipient_email='member@example.com',
workspace_name='Workspace',
invitation_link='https://public.langbot.example/invitations/accept#token=lbi_secret',
)
assert result == InvitationDeliveryResult(status='failed', provider='resend')
service._send_resend.assert_awaited_once()
async def test_environment_mapping_enables_provider_without_leaking_secret(monkeypatch):
monkeypatch.setenv('WORKSPACE__INVITATIONS__PUBLIC_WEB_URL', 'https://env.langbot.example/')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__PROVIDER', 'smtp')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__FROM', 'LangBot <noreply@example.com>')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__SMTP__HOST', 'smtp.example.com')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__SMTP__PASSWORD', 'smtp-secret')
service = InvitationDeliveryService(_app({'api': {'webui_url': 'https://config.example'}}))
assert service.build_invitation_link('lbi_secret') == (
'https://env.langbot.example/invitations/accept#token=lbi_secret'
)
assert service.capability() == {'enabled': True, 'provider': 'smtp'}
@@ -240,7 +240,7 @@ async def test_cloud_member_listing_opens_workspace_uow_when_request_scope_has_c
]
async def test_cloud_directory_requires_explicit_selector_and_rejects_local_mutation(
async def test_cloud_directory_requires_explicit_selector_and_allows_core_membership_management(
collaboration_context,
):
_service, workspace_service, session_factory, owner, _workspace, _membership = collaboration_context
@@ -292,13 +292,34 @@ async def test_cloud_directory_requires_explicit_selector_and_rejects_local_muta
assert access.workspace.uuid == cloud_workspace_uuid
assert access.execution.placement_generation == 8
with pytest.raises(MembershipPermissionError, match='control plane'):
await cloud_service.create_invitation(
cloud_workspace_uuid,
cloud_membership,
'member@example.com',
'viewer',
)
created = await cloud_service.create_invitation(
cloud_workspace_uuid,
cloud_membership,
'member@example.com',
'viewer',
)
assert created.invitation.workspace_uuid == cloud_workspace_uuid
assert created.token.startswith('lbi_')
member = await _add_account(session_factory, 'member@example.com')
membership = await cloud_service.accept_invitation(created.token, member.uuid)
assert membership.workspace_uuid == cloud_workspace_uuid
assert membership.role == 'viewer'
updated = await cloud_service.update_member_role(
cloud_workspace_uuid,
member.uuid,
'developer',
cloud_membership,
)
assert updated.role == 'developer'
removed = await cloud_service.remove_member(
cloud_workspace_uuid,
member.uuid,
cloud_membership,
)
assert removed.status == 'removed'
async def test_invitation_lock_registry_does_not_retain_sequential_tokens(collaboration_context):