mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 15:02:20 +02:00
Integrate lab fff55a78 into PR #40 (cc25d5ba). Lab's modular email backend/frontend, modular settings, split stylesheets (static/style.css stays deleted), procfs compatibility, and request-scoped TurnContract authority win; PR #40's routing classifiers, editor/email/task features, and style.css changes are ported into lab's module and stylesheet homes. Integration fixes: - settings/api.js imports ui.js under its canonical versioned URL - browser observations keep legacy CAPTCHA/access-block evidence - artifact turns do not re-trigger broad-web research recovery - env reference documents PR test-tool variables; page regenerated PR #40 defects surfaced by lab gates and fixed here: - web_fetch generic schema drops top-level anyOf (OpenAI contract); the compact preview contract still requires url or urls - get_weather registered as a brokered network read - new lazy editor modules precached for offline use - SearXNG pin mirrored into GPU standalone compose files - image model picker again skips offline endpoints Tests updated where PR #40 changed behaviour on purpose, and PR tests moved onto lab's document_source helpers.
114 lines
5.0 KiB
Python
114 lines
5.0 KiB
Python
import os
|
|
|
|
import pytest
|
|
|
|
from src.email_attachment_text import attachment_text
|
|
from src.clean_agent_preview import compact_schemas
|
|
|
|
|
|
def test_text_and_truncation(tmp_path):
|
|
path = tmp_path / 'invoice.csv'
|
|
path.write_text('Item,Amount\nServices,12345\n', encoding='utf-8')
|
|
assert '12345' in attachment_text(path)['content']
|
|
result = attachment_text(path, max_chars=10)
|
|
assert len(result['content']) == 10
|
|
assert 'truncated' in result['content_note']
|
|
|
|
|
|
def test_real_pdf_text(tmp_path):
|
|
fitz = pytest.importorskip('fitz') # PyMuPDF is in requirements-optional.txt
|
|
path = tmp_path / 'payslip.pdf'
|
|
with fitz.open() as doc:
|
|
page = doc.new_page()
|
|
page.insert_text((72, 72), 'Gross pay: 150000\nNet pay: 120000')
|
|
doc.save(path)
|
|
result = attachment_text(path)
|
|
assert result['content_status'] == 'read'
|
|
assert '120000' in result['content']
|
|
assert 'Page 1' in result['content']
|
|
|
|
|
|
def test_blank_pdf_reports_ocr(tmp_path):
|
|
from pypdf import PdfWriter
|
|
path = tmp_path / 'scan.pdf'
|
|
writer = PdfWriter()
|
|
writer.add_blank_page(width=100, height=100)
|
|
writer.write(path)
|
|
assert attachment_text(path)['content_status'] == 'needs_ocr'
|
|
|
|
|
|
def test_xlsx(tmp_path):
|
|
Workbook = pytest.importorskip('openpyxl').Workbook # requirements-optional.txt
|
|
path = tmp_path / 'expenses.xlsx'
|
|
book = Workbook()
|
|
book.active.append(['Expenses', 12500])
|
|
book.save(path)
|
|
result = attachment_text(path)
|
|
assert 'Expenses\t12500' in result['content']
|
|
|
|
|
|
def test_bad_pdf_reports_failure(tmp_path):
|
|
path = tmp_path / 'bad.pdf'
|
|
path.write_bytes(b'not a pdf')
|
|
assert attachment_text(path)['content_status'] == 'failed'
|
|
|
|
|
|
def test_compact_live_alias_explains_reading():
|
|
schema = {'type': 'function', 'function': {'name': 'mcp__email__download_attachment',
|
|
'description': 'Download', 'parameters': {'type': 'object', 'properties': {}}}}
|
|
description = compact_schemas([schema], model='Ajax')[0]['function']['description']
|
|
assert 'before answering' in description
|
|
assert 'contents inline' in description
|
|
|
|
|
|
def test_discovered_attachment_read_does_not_need_explicit_download_request():
|
|
from src.clean_agent_preview import evaluate_preview_call
|
|
decision = evaluate_preview_call('mcp__email__download_attachment',
|
|
{'uid': '42', 'index': 0, 'account': 'fixture@example.test'},
|
|
'What email had my latest payslip and how much')
|
|
assert decision.allowed
|
|
assert not evaluate_preview_call('write_file', {'path': '/tmp/test', 'content': 'x'},
|
|
'What email had my latest payslip and how much').allowed
|
|
|
|
|
|
async def _live_attachment_check(monkeypatch):
|
|
import json
|
|
import os
|
|
import src.clean_agent_preview as module
|
|
from src.tool_policy import ToolPolicy
|
|
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
|
|
from src.turn_contract import resolve_full_inventory_contract
|
|
executions = []
|
|
async def execute(block, **kwargs):
|
|
name = block.tool_type.removeprefix('mcp__email__')
|
|
executions.append(name)
|
|
outputs = {
|
|
'list_email_accounts': 'Account: fixture@example.test',
|
|
'search_emails': 'UID: 42\nFolder: INBOX\nAccount: fixture@example.test\nSubject: September 2026 Payslip\nDate: 2026-09-25',
|
|
'read_email': 'UID: 42\nFolder: INBOX\nAccount: fixture@example.test\nSubject: September 2026 Payslip\nBody: Your payslip is attached.\nAttachments: [0] payslip.pdf (application/pdf)',
|
|
'download_attachment': 'Attachment content:\nPage 1:\nSeptember 2026 payslip\nGross pay: JPY 150000\nNet pay: JPY 120000',
|
|
}
|
|
return block.tool_type, {'exit_code': 0, 'stdout': outputs[name]}
|
|
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
|
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in {
|
|
'list_email_accounts', 'search_emails', 'read_email', 'download_attachment'}]
|
|
policy = ToolPolicy()
|
|
contract = resolve_full_inventory_contract(schemas=schemas, policy=policy)
|
|
chunks = [c async for c in module.stream_preview(
|
|
endpoint_url=os.environ['ODYSSEUS_AJAX_TEST_URL'], model='Ajax', headers={},
|
|
messages=[{'role': 'user', 'content': 'What email had my latest payslip and how much'}],
|
|
turn_contract=contract, owner='fixture', session_id='fixture-attachment',
|
|
disabled_tools=set(), tool_policy=policy, thinking_mode='off', max_rounds=6,
|
|
)]
|
|
events = [json.loads(c[6:]) for c in chunks if c.startswith('data: ') and '[DONE]' not in c]
|
|
answer = next((e['content'] for e in reversed(events) if e.get('type') == 'final_response'),
|
|
''.join(e.get('delta', '') for e in events))
|
|
assert 'download_attachment' in executions, (executions, answer)
|
|
assert '120000' in answer.replace(',', ''), answer
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(not os.environ.get('ODYSSEUS_AJAX_TEST_URL'), reason='Opt-in Ajax fixture test')
|
|
async def test_live_ajax_reads_attachment(monkeypatch):
|
|
await _live_attachment_check(monkeypatch)
|