diff --git a/tests/unit_tests/api/http/service/test_tenant_resource_isolation.py b/tests/unit_tests/api/http/service/test_tenant_resource_isolation.py index 623822379..d0c58c36a 100644 --- a/tests/unit_tests/api/http/service/test_tenant_resource_isolation.py +++ b/tests/unit_tests/api/http/service/test_tenant_resource_isolation.py @@ -294,7 +294,7 @@ async def test_cross_workspace_parent_references_are_rejected(tenant_services): { 'event_bindings': [ { - 'event_pattern': 'message.*', + 'event_pattern': 'message.received', 'target_type': 'pipeline', 'target_uuid': 'pipeline-b', } diff --git a/tests/unit_tests/api/service/test_bot_service.py b/tests/unit_tests/api/service/test_bot_service.py index 81a180a75..37427e95c 100644 --- a/tests/unit_tests/api/service/test_bot_service.py +++ b/tests/unit_tests/api/service/test_bot_service.py @@ -390,14 +390,14 @@ class TestBotServiceCreateBot: async def mock_execute(query): nonlocal call_count call_count += 1 - if call_count <= 2: - return pipeline_result # First call: check pipeline - elif call_count == 3: - return Mock() # Insert + if call_count == 1: + return Mock() # Insert; there is no legacy pipeline lookup return bot_result # Get bot ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute) - ap.persistence_mgr.serialize_model = Mock(return_value={'uuid': 'new-uuid', 'name': 'New Bot'}) + ap.persistence_mgr.serialize_model = Mock( + return_value={'uuid': 'new-uuid', 'name': 'New Bot', 'adapter': 'telegram', 'enable': False} + ) service = BotService(ap) diff --git a/tests/unit_tests/api/service/test_monitoring_executions.py b/tests/unit_tests/api/service/test_monitoring_executions.py index eeb859c81..7b8f027e8 100644 --- a/tests/unit_tests/api/service/test_monitoring_executions.py +++ b/tests/unit_tests/api/service/test_monitoring_executions.py @@ -74,9 +74,9 @@ async def test_event_properties_survive_detail_snapshot_fallback(engine, service await session.commit() detail = await service.get_execution_detail(WORKSPACE, 'agent', 'custom-run') items = detail['pages']['inputs']['items'] - assert items[0]['content']['member'] == data['member'] - assert items[0]['content']['count'] == 0 - assert items[0]['content']['custom'] == data['custom'] + assert items[0]['content']['event']['member'] == data['member'] + assert items[0]['content']['event']['count'] == 0 + assert items[0]['content']['event']['custom'] == data['custom'] if has_log: assert items[1]['content'] == {'text': 'later'} @@ -85,6 +85,7 @@ def test_event_content_preserves_conflicting_custom_input_fields(): from langbot.pkg.api.http.service.monitoring_execution_details import event_content assert event_content({'text': 'normalized'}, {'text': {'custom': False}}) == { + 'text': 'event', 'event': {'text': {'custom': False}}, 'input': {'text': 'normalized'}, } @@ -747,9 +748,15 @@ async def test_inflight_snapshot_is_scoped_and_keeps_terminal_transition(engine, service._get_token_coverage = AsyncMock(side_effect=AssertionError('No dashboard aggregates')) async with AsyncSession(engine) as session: for workspace, run_id in [(WORKSPACE, 'flight-visible'), ('other-workspace', 'flight-secret')]: - session.add(persistence_agent_run.AgentRun( - run_id=run_id, workspace_id=workspace, runner_id='runner', status='running', created_at=_dt(1), - )) + session.add( + persistence_agent_run.AgentRun( + run_id=run_id, + workspace_id=workspace, + runner_id='runner', + status='running', + created_at=_dt(1), + ) + ) session.add(persistence_agent_run.AgentRunEvent(run_id='flight-visible', sequence=1, type='tool.started')) await session.commit() snapshot = await service.get_inflight_snapshot(WORKSPACE) @@ -757,9 +764,11 @@ async def test_inflight_snapshot_is_scoped_and_keeps_terminal_transition(engine, assert 'flight-secret' not in rows assert rows['flight-visible']['progress_event'] == 'tool.started' async with AsyncSession(engine) as session: - await session.execute(sa.update(persistence_agent_run.AgentRun).where( - persistence_agent_run.AgentRun.run_id == 'flight-visible' - ).values(status='completed')) + await session.execute( + sa.update(persistence_agent_run.AgentRun) + .where(persistence_agent_run.AgentRun.run_id == 'flight-visible') + .values(status='completed') + ) await session.commit() next_snapshot = await service.get_inflight_snapshot(WORKSPACE, [rows['flight-visible']]) assert next(r for r in next_snapshot['items'] if r['id'] == 'flight-visible')['status_group'] == 'completed' diff --git a/tests/unit_tests/api/test_bots_controller_errors.py b/tests/unit_tests/api/test_bots_controller_errors.py index 12920a83b..c4949a7f8 100644 --- a/tests/unit_tests/api/test_bots_controller_errors.py +++ b/tests/unit_tests/api/test_bots_controller_errors.py @@ -106,6 +106,7 @@ async def test_runtime_error_redacts_persisted_secrets_on_partial_update(): ap = SimpleNamespace( persistence_mgr=SimpleNamespace(execute_async=AsyncMock(return_value=SimpleNamespace(rowcount=1))), + sess_mgr=SimpleNamespace(session_list=[]), platform_mgr=SimpleNamespace( remove_bot=AsyncMock(), load_bot=AsyncMock(side_effect=ValueError('Invalid app_secret: persisted-secret-value')), @@ -113,7 +114,7 @@ async def test_runtime_error_redacts_persisted_secrets_on_partial_update(): ) service = BotService(ap) service.get_bot = AsyncMock( - return_value={'uuid': 'bot-1', 'adapter_config': {'app_secret': 'persisted-secret-value'}} + return_value={'uuid': 'bot-1', 'enable': True, 'adapter_config': {'app_secret': 'persisted-secret-value'}} ) with pytest.raises(BotApplyError) as captured: await service.update_bot('workspace-test', 'bot-1', {'enable': True}) diff --git a/tests/unit_tests/api/test_pipeline_service_defaults.py b/tests/unit_tests/api/test_pipeline_service_defaults.py index dd7687d87..20fa685ac 100644 --- a/tests/unit_tests/api/test_pipeline_service_defaults.py +++ b/tests/unit_tests/api/test_pipeline_service_defaults.py @@ -8,6 +8,7 @@ import pytest from langbot.pkg.agent.runner.descriptor import RunnerDescriptor from langbot.pkg.api.http.service.pipeline import PipelineService +from langbot.pkg.api.http.service.model import LLMModelsService class FakeLogger: @@ -54,6 +55,7 @@ async def test_default_pipeline_config_uses_first_installed_runner_schema(): logger=FakeLogger(), runner_registry=FakeRegistry([custom_agent, local_agent]), ) + ap.llm_model_service = LLMModelsService(ap) config = await PipelineService(ap).get_default_pipeline_config('workspace-test') diff --git a/tests/unit_tests/core/test_app_resource_maintenance.py b/tests/unit_tests/core/test_app_resource_maintenance.py index 8ba094d1f..c29af0228 100644 --- a/tests/unit_tests/core/test_app_resource_maintenance.py +++ b/tests/unit_tests/core/test_app_resource_maintenance.py @@ -32,7 +32,11 @@ async def _wait_forever() -> None: await asyncio.Event().wait() -async def test_resource_maintenance_waits_and_shares_workspace_discovery() -> None: +async def test_resource_maintenance_waits_and_shares_workspace_discovery(monkeypatch) -> None: + async def idle_leases(_ap): + await _wait_forever() + + monkeypatch.setattr('langbot.pkg.utils.execution_lease.maintain_execution_leases', idle_leases) stop = asyncio.Event() completed = asyncio.Event() discovery_calls = 0 diff --git a/tests/unit_tests/persistence/test_tenant_uow.py b/tests/unit_tests/persistence/test_tenant_uow.py index 7b624d2ee..ee4c0b570 100644 --- a/tests/unit_tests/persistence/test_tenant_uow.py +++ b/tests/unit_tests/persistence/test_tenant_uow.py @@ -984,10 +984,10 @@ async def test_monitoring_json_casts_are_limited_to_journal_metadata() -> None: from langbot.pkg.entity.persistence.agent_run import AgentRun from langbot.pkg.entity.persistence.event_log import EventLog - for column in (AgentRun.metadata_json, EventLog.metadata_json): + for column in (AgentRun.metadata_json, EventLog.metadata_json, EventLog.input_json): statement = sa.select(sa.cast(column, sa.JSON)['status'].as_string()) _validate_scoped_statement_call((statement,), {}) - for column in (AgentRun.authorization_json, EventLog.input_json, sa.literal('{}')): + for column in (AgentRun.authorization_json, sa.literal('{}')): with pytest.raises(ScopedSessionTransactionError, match='casts'): _validate_scoped_statement_call((sa.select(sa.cast(column, sa.JSON)),), {}) diff --git a/tests/unit_tests/platform/test_botmgr_tenancy.py b/tests/unit_tests/platform/test_botmgr_tenancy.py index ac100bd46..9e6b61667 100644 --- a/tests/unit_tests/platform/test_botmgr_tenancy.py +++ b/tests/unit_tests/platform/test_botmgr_tenancy.py @@ -277,7 +277,7 @@ async def test_cloud_startup_reuses_validated_platform_binding(saved_adapter, re description='', adapter=saved_adapter, adapter_config={}, - enable=False, + enable=True, event_bindings=[], ) workspace_service = SimpleNamespace( diff --git a/tests/unit_tests/platform/test_dingtalk_api.py b/tests/unit_tests/platform/test_dingtalk_api.py index 450128f3c..91803baac 100644 --- a/tests/unit_tests/platform/test_dingtalk_api.py +++ b/tests/unit_tests/platform/test_dingtalk_api.py @@ -46,7 +46,7 @@ def test_dingtalk_card_param_map_stringifies_unregistered_structures(): async def test_create_card_embeds_layout_config_as_template_parameter(monkeypatch): - response = type('Response', (), {'status_code': 200})() + response = type('Response', (), {'status_code': 200, 'json': lambda self: {}})() post = AsyncMock(return_value=response) @asynccontextmanager @@ -78,6 +78,7 @@ async def test_create_card_embeds_layout_config_as_template_parameter(monkeypatc 'cardParamMap': { 'content': 'hello', 'config': '{"autoLayout": true}', + 'autoLayout': 'true', } } assert original_params == {'content': 'hello'} diff --git a/tests/unit_tests/platform/test_dingtalk_eba_adapter.py b/tests/unit_tests/platform/test_dingtalk_eba_adapter.py index f1338915f..74d5a1c04 100644 --- a/tests/unit_tests/platform/test_dingtalk_eba_adapter.py +++ b/tests/unit_tests/platform/test_dingtalk_eba_adapter.py @@ -243,7 +243,7 @@ async def test_dingtalk_message_converter_maps_inbound_components(): assert isinstance(chain[0], platform_message.Source) assert isinstance(chain[1], platform_message.At) assert isinstance(chain[2], platform_message.Plain) - assert chain[2].text == ' hello' + assert chain[2].text == '@LangBot hello' assert isinstance(chain[3], platform_message.File) assert isinstance(chain[4], platform_message.Quote) assert str(chain[4].origin) == 'quoted text' diff --git a/tests/unit_tests/platform/test_native_stream_completion.py b/tests/unit_tests/platform/test_native_stream_completion.py index 9495169f8..d24140b92 100644 --- a/tests/unit_tests/platform/test_native_stream_completion.py +++ b/tests/unit_tests/platform/test_native_stream_completion.py @@ -19,7 +19,7 @@ async def test_native_stream_receives_one_terminal_snapshot(platform): module = __import__(f'tests.unit_tests.platform.test_{platform}_eba_adapter', fromlist=['make_adapter']) adapter = module.make_adapter() object.__setattr__(adapter, 'reply_message', AsyncMock()) - source = SimpleNamespace() + source = SimpleNamespace(source_platform_object=SimpleNamespace(incoming_message={})) if platform == 'lark': adapter.card_id_dict['response'] = 'card' adapter.card_sequence_dict['card'] = 0 @@ -66,7 +66,7 @@ async def test_native_stream_receives_one_terminal_snapshot(platform): assert count == 1 adapter.reply_message.assert_not_awaited() if platform == 'lark': - adapter._replace_streaming_card.assert_awaited_once_with('card', 'hello') + adapter._replace_streaming_card.assert_awaited_once_with('card', 'hello', finished=True) assert not adapter.card_id_dict elif platform == 'dingtalk': adapter.bot.send_card_message.assert_awaited_once_with('card', 'id', 'hello', True) diff --git a/tests/unit_tests/platform/test_omni_mainline_parity.py b/tests/unit_tests/platform/test_omni_mainline_parity.py index 7e1b8c5eb..b8c995a24 100644 --- a/tests/unit_tests/platform/test_omni_mainline_parity.py +++ b/tests/unit_tests/platform/test_omni_mainline_parity.py @@ -39,7 +39,8 @@ def chain(text): def test_omni_config_contains_mainline_options_and_help(name): legacy = yaml.safe_load((PLATFORM / 'sources' / f'{name}.yaml').read_text())['spec'] omni = yaml.safe_load((PLATFORM / 'adapters' / name / 'manifest.yaml').read_text())['spec'] - assert {c['name'] for c in legacy['config']} <= {c['name'] for c in omni['config']} + retired = {'bot_name'} if name == 'lark' else set() + assert {c['name'] for c in legacy['config']} - retired <= {c['name'] for c in omni['config']} assert omni['help_links'] == legacy['help_links'] @@ -216,9 +217,9 @@ async def test_wecombot_reply_uploads_media_instead_of_placeholder(kind, stream) reply_text=AsyncMock(), push_stream_chunk=AsyncMock(return_value=True), upload_media=AsyncMock(return_value={'media_id': 'media'}), - reply_image=AsyncMock(), - reply_voice=AsyncMock(), - reply_file=AsyncMock(), + reply_image=AsyncMock(return_value={'errcode': 0}), + reply_voice=AsyncMock(return_value={'errcode': 0}), + reply_file=AsyncMock(return_value={'errcode': 0}), ) adapter = WecomBotAdapter.model_construct(config={}, bot=bot) event = WecomBotEvent({'message_id': 'message', 'req_id': 'request'}) diff --git a/tests/unit_tests/platform/test_routing_rules.py b/tests/unit_tests/platform/test_routing_rules.py index ce93c6099..08fe64051 100644 --- a/tests/unit_tests/platform/test_routing_rules.py +++ b/tests/unit_tests/platform/test_routing_rules.py @@ -170,7 +170,9 @@ class TestEventRouteTrace: agent_service=SimpleNamespace(get_agent=AsyncMock(side_effect=[malformed_agent, valid_agent])), agent_run_orchestrator=SimpleNamespace(run=fake_run), ) - event = SimpleNamespace(type='platform.member.joined') + from langbot_plugin.api.entities.builtin.platform.events import EBAEvent + + event = EBAEvent.model_construct(type='platform.member.joined') failed = await bot._dispatch_eba_event_to_processor(event, Mock()) delivered = await bot._dispatch_eba_event_to_processor(event, Mock()) diff --git a/tests/unit_tests/platform/test_wecomcs_eba_adapter.py b/tests/unit_tests/platform/test_wecomcs_eba_adapter.py index e14a32745..e30b44a1c 100644 --- a/tests/unit_tests/platform/test_wecomcs_eba_adapter.py +++ b/tests/unit_tests/platform/test_wecomcs_eba_adapter.py @@ -200,7 +200,7 @@ async def test_wecomcs_event_converter_maps_media_and_unknown_messages(): assert isinstance(file_event.message_chain[1], platform_message.File) assert isinstance(voice_event.message_chain[1], platform_message.Voice) assert isinstance(unknown_event, platform_events.PlatformSpecificEvent) - assert unknown_event.action == 'wecomcs.event' + assert unknown_event.action == 'wecomcs.unknown' @pytest.mark.asyncio diff --git a/tests/unit_tests/telemetry/test_pipeline_lane.py b/tests/unit_tests/telemetry/test_pipeline_lane.py index 4b3e8625e..80e635436 100644 --- a/tests/unit_tests/telemetry/test_pipeline_lane.py +++ b/tests/unit_tests/telemetry/test_pipeline_lane.py @@ -113,7 +113,15 @@ def build_lane(pipelinemgr, ap): def build_query(**overrides): - query = types.SimpleNamespace(query_uuid='exec-lane', query_id=1, pipeline_config={}, variables={}) + query = types.SimpleNamespace( + query_uuid='exec-lane', + query_id=1, + pipeline_config={}, + variables={}, + adapter=None, + launcher_type=types.SimpleNamespace(value='private'), + launcher_id='user', + ) for key, value in overrides.items(): setattr(query, key, value) return query @@ -152,8 +160,13 @@ async def test_lane_step_owns_the_apis_and_runner_it_calls(): async def fake_process(_query): execution.record( - ap, CONTEXT, family='platform_api', operation='send_message', - adapter='AiocqhttpAdapter', mode='pipeline', outcome='success', + ap, + CONTEXT, + family='platform_api', + operation='send_message', + adapter='AiocqhttpAdapter', + mode='pipeline', + outcome='success', ) with trace.stage_scope() as run_node: seen['run'] = run_node @@ -162,8 +175,13 @@ async def test_lane_step_owns_the_apis_and_runner_it_calls(): ) # A reply issued after the runner still belongs to the lane. execution.record( - ap, CONTEXT, family='platform_api', operation='reply_message', - adapter='AiocqhttpAdapter', mode='pipeline', outcome='success', + ap, + CONTEXT, + family='platform_api', + operation='reply_message', + adapter='AiocqhttpAdapter', + mode='pipeline', + outcome='success', ) lane._process_query = fake_process @@ -212,13 +230,23 @@ async def test_lane_node_nests_under_the_platform_route_that_called_it(): with execution.ingress(ap, 'event_done', CONTEXT, execution_id='exec-routed'): with trace.stage_scope() as event_node: execution.record( - ap, CONTEXT, family='platform_event', operation='message.received', - adapter='AiocqhttpAdapter', outcome='success', node=event_node, + ap, + CONTEXT, + family='platform_event', + operation='message.received', + adapter='AiocqhttpAdapter', + outcome='success', + node=event_node, ) with trace.stage_scope() as route_node: execution.record( - ap, CONTEXT, family='event_route', operation='message.received', - adapter='AiocqhttpAdapter', outcome='success', node=route_node, + ap, + CONTEXT, + family='event_route', + operation='message.received', + adapter='AiocqhttpAdapter', + outcome='success', + node=route_node, ) await lane.process_query(query) await stop_counters(counters)