mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-21 07:55:18 +08:00
test: update adapter and monitoring regression fixtures
This commit is contained in:
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),), {})
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user