Files
odysseus/tests/test_headless_completion_consumers.py
T
Alexandre Teixeira 0133de6f0e feat(security): apply audited PR #6738 remediations and test hardening
- Enforce vision probe SSRF safe local host validation, SafeProbeBackend TOCTOU elimination, and session owner authorization
- Authorize session titling endpoints against registered owner configuration
- Sanitize sensitive metadata logging from agent loop debug logs
- Add async non-blocking execution for spec router tools and thinking decisions
- Harden email cache invalidation fencing and thread-safe mutation
- Add regression test suites for legacy browser authority, spec contracts, spec router request scoping, and task MCP delivery
2026-10-27 17:25:25 +00:00

173 lines
6.6 KiB
Python

"""Headless consumers present the completion gate's answer and close its stream."""
import asyncio
import json
import sys
import types
from types import SimpleNamespace
import pytest
from src.agent_runtime.completion import with_completion_gate
from src.agent_runtime.journal import current_journal
from src.teacher_escalation import with_teacher_takeover
CLAIM = 'I created report.md and all tests passed.'
@pytest.fixture(autouse=True)
def registered_scheduler_endpoint(tmp_path, monkeypatch):
import core.database as database
from tests.helpers.database import disposable_database
with disposable_database(tmp_path) as factory:
monkeypatch.setattr(database, 'SessionLocal', factory)
with factory() as db:
db.add(database.ModelEndpoint(id='test-endpoint', name='test', owner='admin',
base_url='http://ep/v1', is_enabled=True))
db.commit()
yield
def _event(payload):
return 'data: ' + json.dumps(payload) + '\n\n'
def _task():
return SimpleNamespace(
crew_member_id=None, endpoint_url='http://ep/v1', model='m',
session_id='s', owner='admin', prompt='create report.md and run the tests',
name='job', max_steps=5, character_id=None,
)
def _gated_loop(released):
"""Real gate and takeover adapters around a loop that over-claims."""
@with_teacher_takeover
@with_completion_gate
async def stream_agent_loop(*args, messages=None, client_runtime_context=None, **kwargs):
yield _event({'delta': 'Inspected the layout. ' + CLAIM})
yield _event({'type': 'metrics', 'data': {}})
yield 'data: [DONE]\n\n'
async def recording(*args, **kwargs):
async for chunk in stream_agent_loop(*args, **kwargs):
if chunk.startswith('data: {') and '"final_response"' in chunk:
released.append(json.loads(chunk[6:])['content'])
yield chunk
return recording
async def test_scheduler_result_is_the_gated_replacement_without_grace_call(monkeypatch):
from src.task_scheduler import TaskScheduler
released = []
grace_calls = []
async def grace(*args, **kwargs):
grace_calls.append(kwargs)
return 'ungated summary: all tests passed'
monkeypatch.setattr('src.agent_loop.stream_agent_loop', _gated_loop(released))
monkeypatch.setattr('src.task_endpoint.resolve_task_candidates', lambda **kwargs: [])
monkeypatch.setattr('src.task_endpoint.task_llm_call_async', grace)
result = await TaskScheduler(session_manager=None)._run_agent_loop(
'http://ep/v1', 'model', _task(), 's')
assert len(released) == 1
assert result == released[0].strip()
assert 'Inspected the layout.' in result
assert 'tests passed' not in result
assert grace_calls == []
def test_background_followup_prose_is_the_gated_replacement(monkeypatch):
from src import bg_monitor
released = []
agent_loop = types.ModuleType('src.agent_loop')
agent_loop.stream_agent_loop = _gated_loop(released)
monkeypatch.setitem(sys.modules, 'src.agent_loop', agent_loop)
sess = SimpleNamespace(endpoint_url='http://example.test', model='model',
headers=None, context_length=0, id='s1', owner='owner')
full, _ = asyncio.run(bg_monitor._drain_agent(
sess, [{'role': 'user', 'content': 'create report.md and run the tests'}]))
assert len(released) == 1
assert full == released[0]
assert 'tests passed' not in full
@pytest.mark.parametrize('consumer', ['scheduler', 'background'])
def test_later_answer_supersedes_earlier_replacement(monkeypatch, consumer):
async def stream_agent_loop(*args, **kwargs):
yield _event({'type': 'final_response', 'content': 'Earlier summary.'})
yield _event({'delta': 'Final '})
yield _event({'delta': 'answer.'})
yield 'data: [DONE]\n\n'
if consumer == 'scheduler':
from src.task_scheduler import TaskScheduler
monkeypatch.setattr('src.agent_loop.stream_agent_loop', stream_agent_loop)
monkeypatch.setattr('src.task_endpoint.resolve_task_candidates', lambda **kwargs: [])
result = asyncio.run(TaskScheduler(session_manager=None)._run_agent_loop(
'http://ep/v1', 'model', _task(), 's'))
else:
from src import bg_monitor
agent_loop = types.ModuleType('src.agent_loop')
agent_loop.stream_agent_loop = stream_agent_loop
monkeypatch.setitem(sys.modules, 'src.agent_loop', agent_loop)
sess = SimpleNamespace(endpoint_url='http://example.test', model='model',
headers=None, context_length=0, id='s1')
result, _ = asyncio.run(bg_monitor._drain_agent(sess, []))
assert result == 'Final answer.'
async def test_scheduler_approval_pause_closes_gated_stream_in_its_own_context(monkeypatch):
from src.task_scheduler import TaskScheduler
closed = []
lineage = []
@with_teacher_takeover
@with_completion_gate
async def paused_loop(*args, messages=None, client_runtime_context=None, **kwargs):
try:
yield _event({'type': 'tool_output', 'tool': 'bash', 'output': 'Waiting for an exact user approval.',
'ask_user': {'kind': 'tool_approval', 'approval_id': 'missing'}})
yield _event({'delta': 'not reached'})
finally:
closed.append(current_journal() is not None)
@with_teacher_takeover
@with_completion_gate
async def later_loop(*args, messages=None, client_runtime_context=None, **kwargs):
yield _event({'delta': 'Later run.'})
yield _event({'type': 'metrics', 'data': {}})
yield 'data: [DONE]\n\n'
async def later_run():
async for chunk in later_loop(messages=[{'role': 'user', 'content': 'x'}]):
if chunk.startswith('data: {') and '"metrics"' in chunk:
lineage.append(json.loads(chunk[6:])['data']['parent_run_id'])
monkeypatch.setattr('src.agent_loop.stream_agent_loop', paused_loop)
monkeypatch.setattr('src.task_endpoint.resolve_task_candidates', lambda **kwargs: [])
result = await TaskScheduler(session_manager=None)._run_agent_loop(
'http://ep/v1', 'model', _task(), 's')
assert 'paused safely' in result
# Closed during the pause, while its own journal was still bound.
assert closed == [True]
assert current_journal() is None
# Neither a chained task (which copies this context) nor a later run in
# this task inherits the paused run's journal as its parent.
chained = asyncio.create_task(later_run())
await chained
await later_run()
assert lineage == [None, None]