test(services): verify persisted state and delivery boundaries

This commit is contained in:
huanghuoguoguo
2026-10-17 15:13:08 +08:00
parent bbf517edbc
commit c1ab2d1110
7 changed files with 96 additions and 35 deletions
@@ -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,
+4 -2
View File
@@ -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):
+6 -2
View File
@@ -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)
+3 -1
View File
@@ -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:
@@ -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):
+9 -4
View File
@@ -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'
}
@@ -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='[email protected]',
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='[email protected]',
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 <[email protected]>'
assert message['To'] == '[email protected]'
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