mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 23:07:14 +00:00
164 lines
6.6 KiB
Python
164 lines
6.6 KiB
Python
"""HTTP streaming termination, fail-open behavior and real producer privacy."""
|
|
|
|
import asyncio
|
|
import json
|
|
import re
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
import quart
|
|
|
|
from langbot.pkg.agent.runner import errors as runner_errors
|
|
from langbot.pkg.api import management_diagnostics as md
|
|
from langbot.pkg.api.http.controller.group import AuthType
|
|
from langbot.pkg.api.http.controller.groups.agent_debug_stream import debug_stream_response
|
|
from langbot.pkg.api.mcp.mount import MCPMount
|
|
from langbot.pkg.telemetry import diagnostics as d
|
|
from tests.unit_tests.api.test_diagnostics_management_http import Recorder, setup
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('termination', ['close', 'cancel', 'error'])
|
|
async def test_stream_termination_preserves_cleanup_and_error(termination):
|
|
app, ap, routes = await setup()
|
|
closed = []
|
|
|
|
@routes.route('/end', auth_type=AuthType.NONE)
|
|
async def end():
|
|
async def body():
|
|
try:
|
|
yield b'one'
|
|
if termination == 'error':
|
|
raise ValueError('private-error')
|
|
if termination == 'cancel':
|
|
raise asyncio.CancelledError('private-cancel')
|
|
yield b'two'
|
|
finally:
|
|
closed.append(d.current_span())
|
|
|
|
return quart.Response(body())
|
|
|
|
async with app.test_request_context('/management/end'):
|
|
response = await app.full_dispatch_request()
|
|
if termination == 'close':
|
|
async with response.response as body:
|
|
assert await anext(body.__aiter__()) == b'one'
|
|
else:
|
|
error = ValueError if termination == 'error' else asyncio.CancelledError
|
|
with pytest.raises(error):
|
|
async with response.response as body:
|
|
iterator = body.__aiter__()
|
|
assert await anext(iterator) == b'one'
|
|
await anext(iterator)
|
|
assert len(closed) == 1 and closed[0] is not None
|
|
expected = 'failed' if termination == 'error' else 'cancelled'
|
|
assert [e['outcome'] for e in ap.diagnostics.events] == ['started', expected]
|
|
assert d.current_span() is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_debug_ndjson_error_marks_http_business_failure():
|
|
app, ap, routes = await setup()
|
|
service = SimpleNamespace(debug_agent=AsyncMock(side_effect=runner_errors.RunnerNotFoundError('private-error')))
|
|
|
|
@routes.route('/ndjson', auth_type=AuthType.NONE)
|
|
async def ndjson():
|
|
return debug_stream_response(service, object(), 'private-agent', {'text': 'private-text'})
|
|
|
|
response = await app.test_client().get('/management/ndjson')
|
|
assert response.status_code == 200
|
|
assert json.loads(await response.get_data())['kind'] == 'error'
|
|
assert ap.diagnostics.events[-1]['outcome'] == 'failed'
|
|
assert 'private-' not in json.dumps(ap.diagnostics.events)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_terminal_diagnostic_failure_preserves_success_and_stream():
|
|
class BrokenFinish(Recorder):
|
|
def emit(self, kind, operation, outcome, **fields):
|
|
if outcome != 'started':
|
|
raise RuntimeError('private-diagnostic-error')
|
|
super().emit(kind, operation, outcome, **fields)
|
|
|
|
app, _, _ = await setup(BrokenFinish())
|
|
response = await app.test_client().get('/management/stream')
|
|
assert await response.get_data() == b'private-stream-chunk'
|
|
assert d.current_span() is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_real_manager_status_privacy_and_no_network_in_request():
|
|
app, ap, _ = await setup(None)
|
|
ap.instance_config = SimpleNamespace(data={'space': {'url': 'https://example.invalid'}})
|
|
ap.diagnostics = d.DiagnosticsManager(ap, version='4.11.0-beta.2', instance_id='instance-test', capacity=20)
|
|
ap.diagnostics.credentials = AsyncMock(side_effect=AssertionError('must not await credentials'))
|
|
for path in ['ok/private-identifier', 'business', 'auth', 'cancel']:
|
|
async with app.test_request_context('/management/' + path):
|
|
try:
|
|
await app.full_dispatch_request()
|
|
except asyncio.CancelledError:
|
|
pass
|
|
events = list(ap.diagnostics.pending)
|
|
assert len(events) == 8
|
|
assert {e['outcome'] for e in events} == {'started', 'succeeded', 'failed', 'rejected', 'cancelled'}
|
|
assert all(re.fullmatch(r'[A-Za-z_][A-Za-z0-9_.:-]{0,127}', e['operation']) for e in events)
|
|
assert 'private-' not in json.dumps(events)
|
|
ap.diagnostics.credentials.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
'config,version',
|
|
[
|
|
({}, '4.11.0'),
|
|
({'disable_telemetry': True}, '4.11.0-beta.2'),
|
|
({'disable_beta_diagnostics': True}, '4.11.0-beta.2'),
|
|
],
|
|
)
|
|
async def test_real_manager_disabled_modes_do_not_install_context(config, version):
|
|
app, ap, _ = await setup(None)
|
|
ap.instance_config = SimpleNamespace(data={'space': config})
|
|
ap.diagnostics = d.DiagnosticsManager(ap, version=version, instance_id='instance-test')
|
|
response = await app.test_client().get('/management/ok/private-id')
|
|
assert response.status_code == 200
|
|
assert ap.seen == [None]
|
|
assert not ap.diagnostics.pending
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_mount_observes_auth_without_headers_or_body():
|
|
ap = SimpleNamespace(
|
|
diagnostics=Recorder(), apikey_service=SimpleNamespace(authenticate_api_key=AsyncMock(return_value=None))
|
|
)
|
|
mount = MCPMount(ap)
|
|
send = AsyncMock()
|
|
fallback = AsyncMock()
|
|
await mount.wrap(fallback)(
|
|
{'type': 'http', 'path': '/mcp/private-path', 'headers': [(b'x-api-key', b'private-token')]}, AsyncMock(), send
|
|
)
|
|
assert send.call_args_list[0].args[0]['status'] == 401
|
|
assert [e['outcome'] for e in ap.diagnostics.events] == ['started', 'rejected']
|
|
assert ap.diagnostics.events[-1]['operation'] == 'mcp.request'
|
|
assert 'private-' not in json.dumps(ap.diagnostics.events)
|
|
fallback.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_debug_boundary_source_inherits_without_duplicate_run():
|
|
recorder = Recorder()
|
|
ap = SimpleNamespace(diagnostics=recorder)
|
|
|
|
@md.observe('http.test.debug', source='webui_debug', ap=ap)
|
|
async def debug():
|
|
@d.observe('run', 'runner.run', source='agent', ap=ap)
|
|
async def run():
|
|
return 'private-content'
|
|
|
|
return await run()
|
|
|
|
assert await debug() == 'private-content'
|
|
assert len([e for e in recorder.events if e['kind'] == 'run' and e['outcome'] == 'started']) == 1
|
|
assert all(e['source'] == 'webui_debug' and e['attributes']['synthetic'] for e in recorder.events)
|
|
assert 'private-' not in json.dumps(recorder.events)
|