mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
Merge pull request #2679 from langbot-app/test/quality-api-observability-20261017
test(api): verify monitoring payloads and embed side effects
This commit is contained in:
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, Mock
|
||||
@@ -148,6 +149,11 @@ class TestEmbedWidgetEndpoint:
|
||||
assert 'javascript' in response.content_type
|
||||
fake_embed_app.platform_mgr.resolve_public_bot.assert_any_await('a1b2c3d4-5678-90ab-cdef-123456789abc')
|
||||
|
||||
body = await response.get_data(as_text=True)
|
||||
assert 'a1b2c3d4-5678-90ab-cdef-123456789abc' in body
|
||||
assert 'baseUrl: "http://localhost"' in body
|
||||
assert '__LANGBOT_' not in body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
('query', 'expected_base'),
|
||||
@@ -208,14 +214,21 @@ class TestEmbedLogoEndpoint:
|
||||
assert response.status_code == 200
|
||||
assert 'image/webp' in response.content_type
|
||||
|
||||
body = await response.get_data()
|
||||
assert body == (Path(__file__).parents[3] / 'src/langbot/templates/embed/logo.webp').read_bytes()
|
||||
assert body[:4] == b'RIFF' and body[8:12] == b'WEBP'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestEmbedTurnstileVerifyEndpoint:
|
||||
"""Tests for Turnstile verification endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_turnstile_verify_no_secret(self, quart_test_client):
|
||||
async def test_turnstile_verify_no_secret(self, quart_test_client, monkeypatch):
|
||||
"""POST turnstile verify without secret returns dummy token."""
|
||||
import langbot.pkg.api.http.controller.groups.pipelines.embed as embed
|
||||
|
||||
monkeypatch.setattr(embed.time, 'time', lambda: 1720000000.25)
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/turnstile/verify', json={'token': 'test-token'}
|
||||
)
|
||||
@@ -225,6 +238,8 @@ class TestEmbedTurnstileVerifyEndpoint:
|
||||
assert data['code'] == 0
|
||||
assert 'token' in data['data']
|
||||
|
||||
assert data['data']['token'] == '1720000000.25.dummy'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_turnstile_verify_invalid_uuid(self, quart_test_client):
|
||||
"""POST turnstile verify with invalid UUID returns 400."""
|
||||
@@ -264,7 +279,7 @@ class TestEmbedMessagesEndpoint:
|
||||
proxy_bot.adapter.get_websocket_messages.assert_called_with('test-pipeline-uuid', 'person', SESSION_ID)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_group_success(self, quart_test_client):
|
||||
async def test_get_messages_group_success(self, quart_test_client, fake_embed_app):
|
||||
"""GET messages/group returns messages."""
|
||||
response = await quart_test_client.get(
|
||||
f'/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/messages/group?session_id={SESSION_ID}',
|
||||
@@ -273,6 +288,12 @@ class TestEmbedMessagesEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data']['messages'] == [{'id': 'msg-1', 'content': 'test message'}]
|
||||
adapter = fake_embed_app.platform_mgr.get_websocket_proxy_bot.return_value.adapter
|
||||
adapter.get_websocket_messages.assert_called_with('test-pipeline-uuid', 'group', SESSION_ID)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_requires_session_id(self, quart_test_client):
|
||||
"""GET messages without a client session identifier returns 400."""
|
||||
@@ -287,12 +308,14 @@ class TestEmbedMessagesEndpoint:
|
||||
async def test_get_messages_invalid_session_type(self, quart_test_client):
|
||||
"""GET messages with invalid session_type returns 400."""
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/messages/invalid',
|
||||
f'/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/messages/invalid?session_id={SESSION_ID}',
|
||||
headers={'Authorization': 'Bearer 1234567890.dummy'},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
assert (await response.get_json())['msg'] == 'session_type must be person or group'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestEmbedResetEndpoint:
|
||||
@@ -337,8 +360,12 @@ class TestEmbedFeedbackEndpoint:
|
||||
"""Tests for feedback submission endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submit_feedback_like(self, quart_test_client):
|
||||
async def test_submit_feedback_like(self, quart_test_client, fake_embed_app, monkeypatch):
|
||||
"""POST feedback with type=1 (like) succeeds."""
|
||||
import langbot.pkg.api.http.controller.groups.pipelines.embed as embed
|
||||
|
||||
monkeypatch.setattr(embed.uuid, 'uuid4', lambda: SimpleNamespace(hex='123456789abcdef0123456789abcdef0'))
|
||||
fake_embed_app.monitoring_service.record_feedback.reset_mock()
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/feedback',
|
||||
headers={'Authorization': 'Bearer 1234567890.dummy'},
|
||||
@@ -350,9 +377,29 @@ class TestEmbedFeedbackEndpoint:
|
||||
assert data['code'] == 0
|
||||
assert 'feedback_id' in data['data']
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data']['feedback_id'] == 'embed_123456789abc'
|
||||
called = fake_embed_app.monitoring_service.record_feedback
|
||||
assert called.await_count == 1
|
||||
assert called.await_args.args[0].workspace_uuid == 'workspace-test'
|
||||
assert called.await_args.kwargs == {
|
||||
'feedback_id': 'embed_123456789abc',
|
||||
'feedback_type': 1,
|
||||
'bot_id': 'a1b2c3d4-5678-90ab-cdef-123456789abc',
|
||||
'bot_name': 'Test Web Bot',
|
||||
'pipeline_id': 'test-pipeline-uuid',
|
||||
'message_id': 'msg-123',
|
||||
'platform': 'web_page_bot',
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submit_feedback_dislike(self, quart_test_client):
|
||||
async def test_submit_feedback_dislike(self, quart_test_client, fake_embed_app, monkeypatch):
|
||||
"""POST feedback with type=2 (dislike) succeeds."""
|
||||
import langbot.pkg.api.http.controller.groups.pipelines.embed as embed
|
||||
|
||||
monkeypatch.setattr(embed.uuid, 'uuid4', lambda: SimpleNamespace(hex='123456789abcdef0123456789abcdef0'))
|
||||
fake_embed_app.monitoring_service.record_feedback.reset_mock()
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/feedback',
|
||||
headers={'Authorization': 'Bearer 1234567890.dummy'},
|
||||
@@ -361,6 +408,22 @@ class TestEmbedFeedbackEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data']['feedback_id'] == 'embed_123456789abc'
|
||||
called = fake_embed_app.monitoring_service.record_feedback
|
||||
assert called.await_count == 1
|
||||
assert called.await_args.args[0].workspace_uuid == 'workspace-test'
|
||||
assert called.await_args.kwargs == {
|
||||
'feedback_id': 'embed_123456789abc',
|
||||
'feedback_type': 2,
|
||||
'bot_id': 'a1b2c3d4-5678-90ab-cdef-123456789abc',
|
||||
'bot_name': 'Test Web Bot',
|
||||
'pipeline_id': 'test-pipeline-uuid',
|
||||
'message_id': 'msg-123',
|
||||
'platform': 'web_page_bot',
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submit_feedback_invalid_type(self, quart_test_client):
|
||||
"""POST feedback with invalid type returns 400."""
|
||||
@@ -378,7 +441,30 @@ class TestEmbedWebSocketEndpoint:
|
||||
"""The public socket authenticates before resolving shared runtime state."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticates_before_connecting(self, quart_test_client, fake_embed_app):
|
||||
async def test_authenticates_before_connecting(self, quart_test_client, fake_embed_app, monkeypatch):
|
||||
import langbot.pkg.api.http.controller.groups.pipelines.embed as embed
|
||||
|
||||
authenticated = False
|
||||
original_authenticate = embed.EmbedRouterGroup._authenticate_websocket
|
||||
original_add = embed.ws_connection_manager.add_connection
|
||||
proxy = fake_embed_app.platform_mgr.get_websocket_proxy_bot
|
||||
|
||||
async def authenticate(router, runtime_bot):
|
||||
nonlocal authenticated
|
||||
await original_authenticate(router, runtime_bot)
|
||||
authenticated = True
|
||||
|
||||
async def get_proxy(*_args, **_kwargs):
|
||||
assert authenticated, 'Runtime lookup must follow successful authentication'
|
||||
return proxy.return_value
|
||||
|
||||
async def add_connection(*args, **kwargs):
|
||||
assert authenticated, 'Connection registration must follow successful authentication'
|
||||
return await original_add(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(embed.EmbedRouterGroup, '_authenticate_websocket', authenticate)
|
||||
monkeypatch.setattr(proxy, 'side_effect', get_proxy)
|
||||
monkeypatch.setattr(embed.ws_connection_manager, 'add_connection', add_connection)
|
||||
async with quart_test_client.websocket(
|
||||
f'/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/ws/connect'
|
||||
f'?session_type=person&session_id={SESSION_ID}',
|
||||
|
||||
@@ -8,6 +8,9 @@ Run: uv run pytest tests/integration/api/test_monitoring.py -q
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, Mock, patch
|
||||
from types import SimpleNamespace
|
||||
@@ -122,17 +125,24 @@ def fake_monitoring_app():
|
||||
)
|
||||
app.monitoring_service.get_feedback_stats = AsyncMock(return_value={'like_count': 10})
|
||||
app.monitoring_service.get_feedback_list = AsyncMock(return_value=([{'feedback_id': 'fb-1'}], 12))
|
||||
app.monitoring_service.export_messages = AsyncMock(return_value=[{'id': 'msg-1'}])
|
||||
app.monitoring_service.export_messages = AsyncMock(
|
||||
return_value=[{'id': 'msg-1', 'message_content': 'hello, "quoted"'}]
|
||||
)
|
||||
app.monitoring_service.export_llm_calls = AsyncMock(return_value=[{'id': 'llm-1'}])
|
||||
app.monitoring_service.export_errors = AsyncMock(return_value=[{'id': 'err-1'}])
|
||||
app.monitoring_service.export_sessions = AsyncMock(return_value=[{'session_id': 'sess-1'}])
|
||||
app.monitoring_service.export_feedback = AsyncMock(return_value=[{'id': 'fb-1'}])
|
||||
app.monitoring_service.export_embedding_calls = AsyncMock(return_value=[{'id': 'emb-1'}])
|
||||
app.monitoring_service._escape_csv_field = Mock(return_value='escaped')
|
||||
app.monitoring_service._escape_csv_field = MonitoringService(app)._escape_csv_field
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_monitoring_calls(fake_monitoring_app):
|
||||
fake_monitoring_app.monitoring_service.reset_mock()
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
async def quart_test_client(fake_monitoring_app, http_controller_cls):
|
||||
"""Create Quart test client (module scope)."""
|
||||
@@ -158,6 +168,14 @@ class TestMonitoringOverviewEndpoint:
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
|
||||
assert data['data'] == {
|
||||
'total_messages': 100,
|
||||
'total_llm_calls': 50,
|
||||
'total_sessions': 20,
|
||||
'active_sessions': 5,
|
||||
'total_errors': 2,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_viewer_can_read_monitoring_but_cannot_export(
|
||||
self,
|
||||
@@ -203,6 +221,15 @@ class TestMonitoringMessagesEndpoint:
|
||||
assert data['code'] == 0
|
||||
assert 'messages' in data['data']
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {
|
||||
'messages': [{'id': 'msg-1', 'content': 'test'}],
|
||||
'total': 100,
|
||||
'limit': 100,
|
||||
'offset': 0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringLLMCallsEndpoint:
|
||||
@@ -217,6 +244,10 @@ class TestMonitoringLLMCallsEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {'llm_calls': [{'id': 'llm-1'}], 'total': 50, 'limit': 100, 'offset': 0}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringEmbeddingCallsEndpoint:
|
||||
@@ -231,6 +262,10 @@ class TestMonitoringEmbeddingCallsEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {'embedding_calls': [{'id': 'emb-1'}], 'total': 10, 'limit': 100, 'offset': 0}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringSessionsEndpoint:
|
||||
@@ -245,6 +280,10 @@ class TestMonitoringSessionsEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {'sessions': [{'session_id': 'sess-1'}], 'total': 20, 'limit': 100, 'offset': 0}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sessions_forwards_user_search_and_page_window(self, quart_test_client, fake_monitoring_app):
|
||||
fake_monitoring_app.monitoring_service.get_sessions.reset_mock()
|
||||
@@ -275,6 +314,10 @@ class TestMonitoringErrorsEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {'errors': [{'id': 'err-1'}], 'total': 2, 'limit': 100, 'offset': 0}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringAllDataEndpoint:
|
||||
@@ -298,6 +341,29 @@ class TestMonitoringAllDataEndpoint:
|
||||
assert 'overview' in data['data']
|
||||
assert data['data']['traffic'] == traffic
|
||||
|
||||
assert data['code'] == 0
|
||||
assert data['data']['overview'] == {
|
||||
'total_messages': 100,
|
||||
'total_llm_calls': 50,
|
||||
'total_sessions': 20,
|
||||
'active_sessions': 5,
|
||||
'total_errors': 2,
|
||||
}
|
||||
assert data['data']['messages'] == [{'id': 'msg-1', 'content': 'test'}]
|
||||
assert data['data']['llmCalls'] == [{'id': 'llm-1'}]
|
||||
assert data['data']['toolCalls'] == [{'id': 'tool-1'}]
|
||||
assert data['data']['embeddingCalls'] == [{'id': 'emb-1'}]
|
||||
assert data['data']['sessions'] == [{'session_id': 'sess-1'}]
|
||||
assert data['data']['errors'] == [{'id': 'err-1'}]
|
||||
assert data['data']['totalCount'] == {
|
||||
'messages': 100,
|
||||
'llmCalls': 50,
|
||||
'toolCalls': 5,
|
||||
'embeddingCalls': 10,
|
||||
'sessions': 20,
|
||||
'errors': 2,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringDetailsEndpoints:
|
||||
@@ -327,6 +393,10 @@ class TestMonitoringDetailsEndpoints:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {'found': True, 'message_id': 'msg-1'}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringFeedbackEndpoints:
|
||||
@@ -341,6 +411,10 @@ class TestMonitoringFeedbackEndpoints:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {'like_count': 10}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_feedback_list(self, quart_test_client):
|
||||
"""GET /api/v1/monitoring/feedback."""
|
||||
@@ -350,13 +424,17 @@ class TestMonitoringFeedbackEndpoints:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data'] == {'feedback': [{'feedback_id': 'fb-1'}], 'total': 12, 'limit': 100, 'offset': 0}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringExportEndpoint:
|
||||
"""Tests for /api/v1/monitoring/export endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_messages(self, quart_test_client):
|
||||
async def test_export_messages(self, quart_test_client, fake_monitoring_app):
|
||||
"""GET export?type=messages returns CSV."""
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/export?type=messages', headers={'Authorization': 'Bearer test_token'}
|
||||
@@ -365,8 +443,20 @@ class TestMonitoringExportEndpoint:
|
||||
assert response.status_code == 200
|
||||
assert 'text/csv' in response.content_type
|
||||
|
||||
assert 'text/csv' in response.content_type
|
||||
assert 'monitoring-messages-' in response.headers['Content-Disposition']
|
||||
body = (await response.get_data()).decode('utf-8-sig')
|
||||
records = list(csv.DictReader(io.StringIO(body)))
|
||||
assert len(records) == 1
|
||||
assert records[0]['id'] == 'msg-1'
|
||||
called = fake_monitoring_app.monitoring_service.export_messages
|
||||
assert called.await_count == 1
|
||||
assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a'
|
||||
assert called.await_args.kwargs['limit'] == 100000
|
||||
assert records[0]['message_content'] == 'hello, "quoted"'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_llm_calls(self, quart_test_client):
|
||||
async def test_export_llm_calls(self, quart_test_client, fake_monitoring_app):
|
||||
"""GET export?type=llm-calls returns CSV."""
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/export?type=llm-calls', headers={'Authorization': 'Bearer test_token'}
|
||||
@@ -374,8 +464,19 @@ class TestMonitoringExportEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert 'text/csv' in response.content_type
|
||||
assert 'monitoring-llm-calls-' in response.headers['Content-Disposition']
|
||||
body = (await response.get_data()).decode('utf-8-sig')
|
||||
records = list(csv.DictReader(io.StringIO(body)))
|
||||
assert len(records) == 1
|
||||
assert records[0]['id'] == 'llm-1'
|
||||
called = fake_monitoring_app.monitoring_service.export_llm_calls
|
||||
assert called.await_count == 1
|
||||
assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a'
|
||||
assert called.await_args.kwargs['limit'] == 100000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_sessions(self, quart_test_client):
|
||||
async def test_export_sessions(self, quart_test_client, fake_monitoring_app):
|
||||
"""GET export?type=sessions returns CSV."""
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/export?type=sessions', headers={'Authorization': 'Bearer test_token'}
|
||||
@@ -383,11 +484,33 @@ class TestMonitoringExportEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert 'text/csv' in response.content_type
|
||||
assert 'monitoring-sessions-' in response.headers['Content-Disposition']
|
||||
body = (await response.get_data()).decode('utf-8-sig')
|
||||
records = list(csv.DictReader(io.StringIO(body)))
|
||||
assert len(records) == 1
|
||||
assert records[0]['session_id'] == 'sess-1'
|
||||
called = fake_monitoring_app.monitoring_service.export_sessions
|
||||
assert called.await_count == 1
|
||||
assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a'
|
||||
assert called.await_args.kwargs['limit'] == 100000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_feedback(self, quart_test_client):
|
||||
async def test_export_feedback(self, quart_test_client, fake_monitoring_app):
|
||||
"""GET export?type=feedback returns CSV."""
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/export?type=feedback', headers={'Authorization': 'Bearer test_token'}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert 'text/csv' in response.content_type
|
||||
assert 'monitoring-feedback-' in response.headers['Content-Disposition']
|
||||
body = (await response.get_data()).decode('utf-8-sig')
|
||||
records = list(csv.DictReader(io.StringIO(body)))
|
||||
assert len(records) == 1
|
||||
assert records[0]['id'] == 'fb-1'
|
||||
called = fake_monitoring_app.monitoring_service.export_feedback
|
||||
assert called.await_count == 1
|
||||
assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a'
|
||||
assert called.await_args.kwargs['limit'] == 100000
|
||||
|
||||
Reference in New Issue
Block a user