test: update adapter and monitoring regression fixtures

This commit is contained in:
Hyu
2026-10-11 01:10:40 +08:00
parent 3ff849d243
commit 33ea03701a
15 changed files with 87 additions and 39 deletions
@@ -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)