mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
Merge pull request #2677 from langbot-app/test/quality-support-services-oracles-20261017
test: verify committed workspace and service outcomes
This commit is contained in:
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
@@ -184,7 +186,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 closing(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 +220,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 +260,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 +581,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 +599,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 +761,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 +778,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 +791,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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user