From c1ab2d1110c1b64754d64faf1181c4fc13a648a0 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <60681390+huanghuoguoguo@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:13:08 +0800 Subject: [PATCH 1/2] test(services): verify persisted state and delivery boundaries --- .../cloud/test_directory_projection.py | 61 ++++++++++++++----- tests/unit_tests/command/test_cmdmgr.py | 6 +- tests/unit_tests/rag/test_file_storage.py | 8 ++- tests/unit_tests/rag/test_kbmgr.py | 4 +- .../unit_tests/survey/test_survey_manager.py | 11 +++- tests/unit_tests/telemetry/test_telemetry.py | 13 ++-- .../workspace/test_invitation_delivery.py | 28 ++++++--- 7 files changed, 96 insertions(+), 35 deletions(-) diff --git a/tests/unit_tests/cloud/test_directory_projection.py b/tests/unit_tests/cloud/test_directory_projection.py index 756baa7be..22fbc5804 100644 --- a/tests/unit_tests/cloud/test_directory_projection.py +++ b/tests/unit_tests/cloud/test_directory_projection.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import datetime import logging +import sqlite3 from types import SimpleNamespace from unittest.mock import AsyncMock, Mock @@ -184,7 +185,16 @@ def _delta( async def test_directory_delta_requests_model_catalog_sync_after_commit(projection_context): application, _session_factory = projection_context - request_sync = Mock() + observed_revisions = [] + + def observe_committed_revision(): + with sqlite3.connect(application.persistence_mgr.engine.url.database) as connection: + row = connection.execute( + 'SELECT projection_revision FROM workspaces WHERE uuid = ?', (WORKSPACE_UUID,) + ).fetchone() + observed_revisions.append(row[0]) + + request_sync = Mock(side_effect=observe_committed_revision) application.cloud_model_catalog_service = SimpleNamespace(request_sync=request_sync) event = DirectoryEvent( cursor=2, @@ -209,10 +219,12 @@ async def test_directory_delta_requests_model_catalog_sync_after_commit(projecti ) await service.initialize() request_sync.reset_mock() + observed_revisions.clear() await service.sync_once() request_sync.assert_called_once_with() + assert observed_revisions == [2] async def test_targeted_reconciliation_projects_new_workspace_without_advancing_event_cursor(projection_context): @@ -247,11 +259,7 @@ async def test_targeted_reconciliation_projects_new_workspace_without_advancing_ async def test_targeted_reconciliation_preserves_existing_account_until_ordered_event_projection(projection_context): application, session_factory = projection_context targeted_workspace = _workspace(revision=8, name='Renamed Workspace').model_copy( - update={ - 'members': [ - _member(revision=8).model_copy(update={'display_name': 'Changed Account Name'}) - ] - } + update={'members': [_member(revision=8).model_copy(update={'display_name': 'Changed Account Name'})]} ) provider = _Provider( [_snapshot(7)], @@ -572,7 +580,13 @@ async def test_account_projection_reads_large_directory_in_bounded_batches(proje assert await session.scalar(sqlalchemy.select(sqlalchemy.func.count()).select_from(User)) == 501 -async def test_archived_and_absent_workspaces_are_execution_fenced(projection_context): +@pytest.mark.parametrize( + ('status', 'execution_state', 'write_fenced', 'membership_status'), + [('archived', 'inactive', True, 'removed'), ('active', 'active', False, 'active')], +) +async def test_archived_and_absent_workspaces_are_execution_fenced( + projection_context, status, execution_state, write_fenced, membership_status +): application, session_factory = projection_context service = DirectoryProjectionService( application, @@ -584,17 +598,17 @@ async def test_archived_and_absent_workspaces_are_execution_fenced(projection_co await service.apply_snapshot( _snapshot( 2, - workspaces=[_workspace(revision=2, status='archived')], + workspaces=[_workspace(revision=2, status=status)], ) ) async with session_factory() as session: workspace = await session.get(Workspace, WORKSPACE_UUID) execution = await session.get(WorkspaceExecutionState, WORKSPACE_UUID) membership = await session.scalar(sqlalchemy.select(WorkspaceMembership)) - assert workspace.status == 'archived' - assert execution.state == 'inactive' - assert execution.write_fenced is True - assert membership.status == 'removed' + assert workspace.status == status + assert execution.state == execution_state + assert execution.write_fenced is write_fenced + assert membership.status == membership_status await service.apply_snapshot(_snapshot(3, workspaces=[])) async with session_factory() as session: @@ -746,7 +760,15 @@ async def test_missing_workspace_in_delta_fences_only_requested_workspace(projec high_water_cursor=2, events=[event], ) - provider = _Provider([_snapshot(1)], [batch], [_delta(workspaces=[])]) + second_workspace = _workspace().model_copy( + update={ + 'uuid': SECOND_WORKSPACE_UUID, + 'slug': 'second-workspace', + 'type': 'team', + 'members': [_member().model_copy(update={'membership_uuid': SECOND_MEMBERSHIP_UUID})], + } + ) + provider = _Provider([_snapshot(1, workspaces=[_workspace(), second_workspace])], [batch], [_delta(workspaces=[])]) service = DirectoryProjectionService(application, provider, INSTANCE_UUID) await service.initialize() @@ -755,7 +777,12 @@ async def test_missing_workspace_in_delta_fences_only_requested_workspace(projec async with session_factory() as session: workspace = await session.get(Workspace, WORKSPACE_UUID) execution = await session.get(WorkspaceExecutionState, WORKSPACE_UUID) - membership = await session.scalar(sqlalchemy.select(WorkspaceMembership)) + membership = await session.scalar( + sqlalchemy.select(WorkspaceMembership).where(WorkspaceMembership.workspace_uuid == WORKSPACE_UUID) + ) + untouched_workspace = await session.get(Workspace, SECOND_WORKSPACE_UUID) + untouched_execution = await session.get(WorkspaceExecutionState, SECOND_WORKSPACE_UUID) + untouched_membership = await session.get(WorkspaceMembership, SECOND_MEMBERSHIP_UUID) assert workspace.status == 'archived' assert workspace.projection_revision == 2 assert execution.state == 'inactive' @@ -763,6 +790,12 @@ async def test_missing_workspace_in_delta_fences_only_requested_workspace(projec assert execution.desired_state_revision == 2 assert membership.status == 'removed' assert membership.projection_revision == 2 + assert untouched_workspace.status == 'active' + assert untouched_workspace.projection_revision == 1 + assert untouched_execution.state == 'active' + assert untouched_execution.write_fenced is False + assert untouched_membership.status == 'active' + assert untouched_membership.projection_revision == 1 stale_same_cursor_snapshot = _snapshot( 2, diff --git a/tests/unit_tests/command/test_cmdmgr.py b/tests/unit_tests/command/test_cmdmgr.py index c4c9156a6..9510a84b2 100644 --- a/tests/unit_tests/command/test_cmdmgr.py +++ b/tests/unit_tests/command/test_cmdmgr.py @@ -437,8 +437,10 @@ class TestEmptyAndEdgeInputs: async for ret in mgr.execute(' ', '/ ', query, session): results.append(ret) - # Should yield error - assert len(results) >= 1 + assert len(results) == 1 + assert results[0].error is not None + assert results[0].error.message == '未知命令: ' + assert results[0].text is None @pytest.mark.asyncio async def test_initialize_with_deep_nesting(self): diff --git a/tests/unit_tests/rag/test_file_storage.py b/tests/unit_tests/rag/test_file_storage.py index 8886e221a..f658218fe 100644 --- a/tests/unit_tests/rag/test_file_storage.py +++ b/tests/unit_tests/rag/test_file_storage.py @@ -300,7 +300,9 @@ class TestStoreFileTask: task_context.set_current_action.assert_called_once_with('Processing file') kb.ap.storage_mgr.storage_provider.size.assert_awaited_once_with(object_key) kb._ingest_document.assert_awaited_once() - assert kb.ap.persistence_mgr.execute_async.await_count == 2 + statements = [call.args[0].compile().params for call in kb.ap.persistence_mgr.execute_async.await_args_list] + assert [params['status'] for params in statements] == ['processing', 'completed'] + assert statements[-1]['engine_document_id'] == 'file-uuid' kb.ap.storage_mgr.storage_provider.delete.assert_awaited_once_with(object_key) @pytest.mark.asyncio @@ -316,7 +318,9 @@ class TestStoreFileTask: with pytest.raises(Exception, match='parser failed'): await kb._store_file_task(CONTEXT, file_obj, task_context) - assert kb.ap.persistence_mgr.execute_async.await_count == 2 + statements = [call.args[0].compile().params for call in kb.ap.persistence_mgr.execute_async.await_args_list] + assert [params['status'] for params in statements] == ['processing', 'failed'] + assert statements[-1]['engine_document_id'] == 'file-uuid' kb.ap.storage_mgr.storage_provider.delete.assert_awaited_once_with(object_key) diff --git a/tests/unit_tests/rag/test_kbmgr.py b/tests/unit_tests/rag/test_kbmgr.py index 9ea0c54c9..01f9a9225 100644 --- a/tests/unit_tests/rag/test_kbmgr.py +++ b/tests/unit_tests/rag/test_kbmgr.py @@ -662,7 +662,9 @@ class TestRuntimeKnowledgeBaseRetrieve: assert len(results) == 1 assert results[0].id == 'doc1' - assert hasattr(results[0], 'content') + assert results[0].content[0].text == 'test content' + assert results[0].metadata == {'source': 'file.pdf'} + assert results[0].distance == 0.15 class TestRuntimeKnowledgeBaseDispose: diff --git a/tests/unit_tests/survey/test_survey_manager.py b/tests/unit_tests/survey/test_survey_manager.py index da67e8219..aac76e3ed 100644 --- a/tests/unit_tests/survey/test_survey_manager.py +++ b/tests/unit_tests/survey/test_survey_manager.py @@ -234,6 +234,12 @@ class TestTriggerEvent: await manager.trigger_event('new_event') assert 'new_event' in manager._triggered_events + statements = mock_app.persistence_mgr.execute_async.await_args_list + assert len(statements) == 2 + assert statements[-1].args[0].compile().params == { + 'key': 'survey_triggered_events', + 'value': '["new_event"]', + } class TestRecordBotResponseSuccess: @@ -255,8 +261,9 @@ class TestRecordBotResponseSuccess: await manager.record_bot_response_success() assert manager._bot_response_count == 1 - # select + insert for the count key - assert mock_app.persistence_mgr.execute_async.call_count >= 2 + statements = mock_app.persistence_mgr.execute_async.await_args_list + assert len(statements) == 2 + assert statements[-1].args[0].compile().params == {'key': 'survey_bot_response_count', 'value': '1'} @pytest.mark.asyncio async def test_fires_milestone_event_at_threshold(self): diff --git a/tests/unit_tests/telemetry/test_telemetry.py b/tests/unit_tests/telemetry/test_telemetry.py index 87699a18d..230710b31 100644 --- a/tests/unit_tests/telemetry/test_telemetry.py +++ b/tests/unit_tests/telemetry/test_telemetry.py @@ -83,7 +83,9 @@ class TestTelemetrySendEarlyReturn: manager.telemetry_config = {} # Should return without making HTTP calls - await manager.send({'query_id': 'test'}) + with patch.object(httpx, 'AsyncClient') as client: + await manager.send({'query_id': 'test'}) + client.assert_not_called() # No HTTP client should be created, no logs should be written mock_app.logger.debug.assert_not_called() @@ -99,7 +101,9 @@ class TestTelemetrySendEarlyReturn: manager = telemetry.TelemetryManager(mock_app) manager.telemetry_config = {'disable_telemetry': True, 'url': 'https://example.com'} - await manager.send({'query_id': 'test'}) + with patch.object(httpx, 'AsyncClient') as client: + await manager.send({'query_id': 'test'}) + client.assert_not_called() mock_app.logger.debug.assert_not_called() @@ -113,7 +117,9 @@ class TestTelemetrySendEarlyReturn: manager = telemetry.TelemetryManager(mock_app) manager.telemetry_config = {'url': ''} - await manager.send({'query_id': 'test'}) + with patch.object(httpx, 'AsyncClient') as client: + await manager.send({'query_id': 'test'}) + client.assert_not_called() mock_app.logger.debug.assert_not_called() @@ -627,4 +633,3 @@ class TestAuthenticatedWorkspaceReporter: assert mock_client.post.call_args.kwargs['headers'] == { 'Authorization': 'Bearer refreshed-workspace-owner-token' } - diff --git a/tests/unit_tests/workspace/test_invitation_delivery.py b/tests/unit_tests/workspace/test_invitation_delivery.py index 347524d07..ff070282c 100644 --- a/tests/unit_tests/workspace/test_invitation_delivery.py +++ b/tests/unit_tests/workspace/test_invitation_delivery.py @@ -1,7 +1,7 @@ from __future__ import annotations from types import SimpleNamespace -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch import pytest @@ -143,16 +143,24 @@ async def test_oss_smtp_configuration_delivers_the_generic_invitation_email(): } ) ) - service._send_smtp = AsyncMock(return_value=True) link = 'https://self-hosted.example/invitations/accept#token=lbi_secret' - result = await service.deliver_invitation( - recipient_email='member@example.com', - workspace_name='Self-hosted Workspace', - invitation_link=link, - ) + with patch('langbot.pkg.workspace.invitation_delivery.smtplib.SMTP') as smtp: + result = await service.deliver_invitation( + recipient_email='member@example.com', + workspace_name='Self-hosted Workspace', + invitation_link=link, + ) assert result == InvitationDeliveryResult(status='sent', provider='smtp') - service._send_smtp.assert_awaited_once() - assert 'LangBot Cloud' not in service._plain_text('Self-hosted Workspace', link) - assert 'LangBot Cloud' not in service._html('Self-hosted Workspace', link) + send_message = smtp.return_value.__enter__.return_value.send_message + send_message.assert_called_once() + message = send_message.call_args.args[0] + assert message['From'] == 'LangBot ' + assert message['To'] == 'member@example.com' + assert message['Subject'] == 'You were invited to Self-hosted Workspace' + for subtype in ('plain', 'html'): + content = message.get_body(preferencelist=(subtype,)).get_content() + assert 'Self-hosted Workspace' in content + assert link in content + assert 'LangBot Cloud' not in content From 89ef102238411623a449a1928d8e17a780ba146e Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <60681390+huanghuoguoguo@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:25:24 +0800 Subject: [PATCH 2/2] test(cloud): close directory observer database connections --- tests/unit_tests/cloud/test_directory_projection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/cloud/test_directory_projection.py b/tests/unit_tests/cloud/test_directory_projection.py index 22fbc5804..cc2a28e88 100644 --- a/tests/unit_tests/cloud/test_directory_projection.py +++ b/tests/unit_tests/cloud/test_directory_projection.py @@ -4,6 +4,7 @@ import asyncio import datetime import logging import sqlite3 +from contextlib import closing from types import SimpleNamespace from unittest.mock import AsyncMock, Mock @@ -188,7 +189,7 @@ async def test_directory_delta_requests_model_catalog_sync_after_commit(projecti observed_revisions = [] def observe_committed_revision(): - with sqlite3.connect(application.persistence_mgr.engine.url.database) as connection: + with closing(sqlite3.connect(application.persistence_mgr.engine.url.database)) as connection: row = connection.execute( 'SELECT projection_revision FROM workspaces WHERE uuid = ?', (WORKSPACE_UUID,) ).fetchone()