Files
odysseus/tests/tools/organizer_smoke.py
T
pewdiepie-archdaemon 2e8413a54a Preserve preview harness, editor, email and task improvements
Snapshot current maintainer-preview application changes and regression fixtures for integration into lab. Excludes local runtime data, evaluation outputs and source backups. Focused Python regression selection: 140 passed; full suite not certified.
2026-10-01 01:34:26 +00:00

405 lines
27 KiB
Python

"""Live model + real organizer handlers with disposable SQLite storage.
No scheduler runs; calendar fixtures are local, with no external sync.
Run with PYTHONPATH=. ODYSSEUS_EDITOR_TEST_ENDPOINT=<chat completions URL>.
"""
import asyncio
import json
import os
import re
import tempfile
from types import SimpleNamespace
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from core import database
from src.tools.notes import do_manage_notes
from src.tools.system import do_manage_tasks
from src.tools.calendar import do_manage_calendar
import jsonschema
from src import clean_agent_preview as runner
from src.tool_policy import ToolPolicy
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
from src.turn_contract import requested_capabilities, selected_tools_for_request, resolve_turn_contract
CASES = [
('checklist', 'Make a todo list: buy milk, return the keys, call Sam.', 'manage_notes'),
('plain-note', 'Save a note titled Project ideas with the text: Build a small weather display.', 'manage_notes'),
('daily', 'Create a task to summarize technology news every day at 07:30 UTC.', 'manage_tasks'),
('weekly', 'Every Monday at 09:00 UTC research new battery technology for me.', 'manage_tasks'),
('calendar', 'Add a calendar event named Design review on October 5, 2026 at 14:00 UTC for one hour.', 'manage_calendar'),
('calendar-all-day', 'Add an all-day calendar event named Office closed on October 8, 2026. Only that day.', 'manage_calendar'),
('calendar-offset', 'Add a calendar event named Tokyo call on October 6, 2026 at 18:00 Japan time (UTC+09:00) for 30 minutes.', 'manage_calendar'),
('calendar-tokyo-simple', 'Add a calendar event named Tokyo call on October 6, 2026 at 6pm Tokyo time for 30 minutes.', 'manage_calendar'),
('calendar-half-offset', 'Create a calendar event called Early call on October 6, 2026 at 00:15 UTC+05:30, lasting 30 minutes.', 'manage_calendar'),
('calendar-negative', 'Create a calendar event called Late call on October 6, 2026 at 22:00 UTC-04:00, lasting 30 minutes.', 'manage_calendar'),
('calendar-dst', 'Add Summer call to my calendar on July 6, 2027 at 10am America/New_York time for 30 minutes.', 'manage_calendar'),
('calendar-move', 'Move it to 16:30 UTC on the same day. Keep its duration.', 'manage_calendar'),
('calendar-rename', 'Rename it to Planning review. Keep its date and time unchanged.', 'manage_calendar'),
('calendar-shorten', 'Make that event 30 minutes long. Keep its start time and title.', 'manage_calendar'),
('calendar-midnight', 'Move that event to October 5, 2026 at 23:30 UTC. Keep its one-hour duration and title.', 'manage_calendar'),
('calendar-move-zone', 'Move that event to October 6, 2026 at 18:00 Tokyo time. Keep its one-hour duration and title.', 'manage_calendar'),
('calendar-relative-hours', 'Push that event back by two hours.', 'manage_calendar'),
('calendar-relative-day', 'Move that event to the following day at the same time.', 'manage_calendar'),
('calendar-hypothetical', 'What time would that event start if it were pushed back by two hours? Do not change it.', 'manage_calendar'),
('calendar-hypothetical-later', 'What time would that event start if it began two hours later? Do not change it.', 'manage_calendar'),
('calendar-hypothetical-earlier', 'What time would that event start if it began two hours earlier? Do not change it.', 'manage_calendar'),
('calendar-no-change', 'Do not move that event. Just remind me of its current start time.', 'manage_calendar'),
('calendar-wording', 'Suggest a short message asking to postpone that event by two hours. Do not modify my calendar.', 'manage_calendar'),
('note-append', 'Add "Check the battery life." to the end of that note. Keep what is already there.', 'manage_notes'),
('checklist-remove', 'Remove return the keys from that checklist. Leave the other items and their checked states alone.', 'manage_notes'),
('checklist-check', 'Mark call Sam as done in that checklist. Keep everything else.', 'manage_notes'),
('checklist-rename', 'Rename that checklist to Weekend errands. Keep all items and their checked states.', 'manage_notes'),
('checklist-uncheck', 'Uncheck buy milk in that checklist. Keep the other items unchanged.', 'manage_notes'),
('checklist-uncheck-last', 'Uncheck call Sam in that checklist. Keep the other items unchanged.', 'manage_notes'),
('checklist-reorder', 'Move call Sam to the top of that checklist. Keep the other items in order and preserve their checked states.', 'manage_notes'),
('checklist-append', 'Add pick up parcel at the end of that checklist, unchecked. Keep the existing items and their checked states.', 'manage_notes'),
('checklist-unfinished', 'List only the unfinished items in that checklist. Do not change anything.', 'manage_notes'),
('checklist-finished', 'Which items in that checklist are already done? List only those. Do not change anything.', 'manage_notes'),
('task-pause', 'Pause that task.', 'manage_tasks'),
('task-retime', 'Change that task to 08:45 UTC every day. Keep its name and prompt.', 'manage_tasks'),
('task-rename', 'Rename that task to Morning briefing. Keep everything else.', 'manage_tasks'),
('task-offset', 'Create a task to summarize technology news every day at 07:30 UTC+09:00.', 'manage_tasks'),
('task-weekdays', 'Create one task to summarize technology news every Monday, Wednesday and Friday at 09:15 UTC.', 'manage_tasks'),
('task-monthly', 'Create a task to review my budget on the 15th of every month at 08:00 UTC.', 'manage_tasks'),
('task-cron-retime', 'Move that task to 10:30 UTC. Keep the same weekdays.', 'manage_tasks'),
('task-cron-days', 'Run that task on Tuesdays and Thursdays instead. Keep the same time.', 'manage_tasks'),
('task-cron-pause', 'Pause that task.', 'manage_tasks'),
('task-cron-resume', 'Resume that task with its existing schedule.', 'manage_tasks'),
('task-monthly-day', 'Move that task to the 20th of each month. Keep the same time and instructions.', 'manage_tasks'),
('task-monthly-time', 'Run that task at 11:45 UTC instead. Keep the same day of the month and instructions.', 'manage_tasks'),
]
CALENDAR_EXPECTED = {
'calendar-offset': ('Tokyo call', '2026-10-06T09:00:00', '2026-10-06T09:30:00'),
'calendar-tokyo-simple': ('Tokyo call', '2026-10-06T09:00:00', '2026-10-06T09:30:00'),
'calendar-half-offset': ('Early call', '2026-10-05T18:45:00', '2026-10-05T19:15:00'),
'calendar-negative': ('Late call', '2026-10-07T02:00:00', '2026-10-07T02:30:00'),
'calendar-dst': ('Summer call', '2027-07-06T14:00:00', '2027-07-06T14:30:00'),
'calendar-move': ('Design review', '2026-10-05T16:30:00', '2026-10-05T17:30:00'),
'calendar-rename': ('Planning review', '2026-10-05T14:00:00', '2026-10-05T15:00:00'),
'calendar-shorten': ('Design review', '2026-10-05T14:00:00', '2026-10-05T14:30:00'),
'calendar-midnight': ('Design review', '2026-10-05T23:30:00', '2026-10-06T00:30:00'),
'calendar-move-zone': ('Design review', '2026-10-06T09:00:00', '2026-10-06T10:00:00'),
'calendar-relative-hours': ('Design review', '2026-10-05T16:00:00', '2026-10-05T17:00:00'),
'calendar-relative-day': ('Design review', '2026-10-06T14:00:00', '2026-10-06T15:00:00'),
'calendar-hypothetical': ('Design review', '2026-10-05T14:00:00', '2026-10-05T15:00:00'),
'calendar-hypothetical-later': ('Design review', '2026-10-05T14:00:00', '2026-10-05T15:00:00'),
'calendar-hypothetical-earlier': ('Design review', '2026-10-05T14:00:00', '2026-10-05T15:00:00'),
'calendar-no-change': ('Design review', '2026-10-05T14:00:00', '2026-10-05T15:00:00'),
'calendar-wording': ('Design review', '2026-10-05T14:00:00', '2026-10-05T15:00:00'),
}
async def seed_followup(case, owner):
if case in {'calendar-move', 'calendar-rename', 'calendar-shorten', 'calendar-midnight', 'calendar-move-zone', 'calendar-relative-hours', 'calendar-relative-day', 'calendar-no-change', 'calendar-wording'} or case.startswith('calendar-hypothetical'):
tool = 'manage_calendar'
prompt = 'Create Design review on my calendar, October 5, 2026, 14:00 UTC for one hour.'
args = dict(action='create_event', summary='Design review', dtstart='2026-10-05T14:00:00Z', dtend='2026-10-05T15:00:00Z')
result = await do_manage_calendar(json.dumps(args), owner=owner)
elif case == 'note-append':
tool = 'manage_notes'
prompt = 'Save a note titled Project ideas with the text: Build a small weather display.'
args = dict(action='add', title='Project ideas', content='Build a small weather display.')
result = await do_manage_notes(json.dumps(args), owner=owner)
elif case.startswith('checklist-'):
tool = 'manage_notes'
prompt = 'Save a checklist called Errands: buy milk (done), return the keys, call Sam.'
args = dict(action='add', title='Errands', note_type='checklist', checklist_items=[
{'text': 'buy milk', 'done': True}, {'text': 'return the keys', 'done': False},
{'text': 'call Sam', 'done': False}])
if case == 'checklist-uncheck-last':
prompt = 'Save a checklist called Errands: buy milk (done), return the keys, call Sam (done).'
args['checklist_items'][2]['done'] = True
result = await do_manage_notes(json.dumps(args), owner=owner)
elif case in {'task-monthly-day', 'task-monthly-time'}:
tool = 'manage_tasks'
prompt = 'Create a task called Budget review to review my budget on the 15th of each month at 08:00 UTC.'
args = dict(action='create', name='Budget review', prompt='Review my budget.',
day_of_month=15, scheduled_time='08:00', task_type='llm')
result = await do_manage_tasks(json.dumps(args), owner=owner)
elif case.startswith('task-cron-'):
tool = 'manage_tasks'
prompt = 'Create one task called Tech digest to summarize technology news every Monday, Wednesday and Friday at 09:15 UTC.'
args = dict(action='create', name='Tech digest', prompt='Summarize technology news.',
weekdays=['monday', 'wednesday', 'friday'], scheduled_time='09:15', task_type='llm')
result = await do_manage_tasks(json.dumps(args), owner=owner)
elif case in {'task-pause', 'task-retime', 'task-rename'}:
tool = 'manage_tasks'
prompt = 'Create a task called Tech digest to summarize technology news every day at 07:30 UTC.'
args = dict(action='create', name='Tech digest', prompt='Summarize technology news.',
schedule='daily', scheduled_time='07:30', task_type='llm')
result = await do_manage_tasks(json.dumps(args), owner=owner)
else:
return [], None
assert result.get('exit_code', 0) == 0, result
history = [{'role': 'user', 'content': prompt}, {'role': 'assistant',
'content': result['response'], 'metadata': {
'tool_events': [{'tool': tool, 'exit_code': 0}],
'clean_v3_turn': [
{'role': 'assistant', 'content': None, 'tool_calls': [{'id': 'seed1', 'type': 'function',
'function': {'name': tool, 'arguments': json.dumps(args)}}]},
{'role': 'tool', 'tool_call_id': 'seed1', 'content': json.dumps(result)},
],
}}]
if case == 'task-cron-resume':
paused = await do_manage_tasks(json.dumps({'action': 'pause', 'task_id': result['task_id']}), owner=owner)
assert paused.get('exit_code') == 0, paused
history.extend([{'role': 'user', 'content': 'Pause that task.'},
{'role': 'assistant', 'content': paused['response']}])
return history, result.get('uid') or result.get('note_id') or result.get('task_id')
def validate_case(case, args):
if case == 'checklist':
assert args['action'] == 'add' and args['note_type'] == 'checklist'
texts = [item['text'].lower() for item in args['checklist_items']]
assert len(texts) == 3
assert all(any(word in text for text in texts) for word in ('milk', 'keys', 'sam'))
assert all(not item.get('done') for item in args['checklist_items'])
elif case == 'plain-note':
assert args['action'] == 'add' and args['title'] == 'Project ideas'
assert 'weather display' in args.get('content', '').lower()
elif case in {'daily', 'weekly'}:
assert args['action'] == 'create'
assert args.get('name') and args.get('prompt')
assert args['schedule'] == case
assert args['scheduled_time'] == ('07:30' if case == 'daily' else '09:00')
if case == 'weekly': assert args['scheduled_day'] == 0
else:
assert args['action'] == 'create_event'
assert args['summary'] == 'Design review'
assert args['dtstart'].startswith('2026-10-05T14:00')
assert args['dtend'].startswith('2026-10-05T15:00')
def visible_answer(events):
answer = ''
for item in events:
if item.get('type') == 'final_response':
answer = item.get('content') or ''
elif isinstance(item.get('delta'), str):
if item.get('replacement_scope') == 'turn':
answer = ''
answer += item['delta']
return answer
async def main():
if os.environ.get('ODYSSEUS_ORGANIZER_TRACE'):
original_choice = runner.provider_compatible_tool_choice_request
original_lines = runner.preview_lines_until_finish
def capture_request(request, model):
request = original_choice(request, model)
if os.environ.get('ODYSSEUS_ORGANIZER_AUTO_CHOICE') and request.get('tools'):
request = {**request, 'tool_choice': 'auto'}
print('WIRE_REQUEST', json.dumps({key: request.get(key) for key in
('model', 'tool_choice', 'tools', 'max_tokens', 'stop')}), flush=True)
if os.environ.get('ODYSSEUS_ORGANIZER_TRACE_MESSAGES'):
print('WIRE_MESSAGES', json.dumps(request.get('messages', [])), flush=True)
return request
async def capture_lines(response, finish_event):
arguments = {}
async for line in original_lines(response, finish_event):
if line.startswith('data: ') and line[6:] != '[DONE]':
payload = json.loads(line[6:])
for choice in payload.get('choices', []):
for call in choice.get('delta', {}).get('tool_calls', []):
index = call['index']
arguments[index] = arguments.get(index, '') + call.get('function', {}).get('arguments', '')
yield line
print('WIRE_ARGUMENTS', json.dumps(arguments), flush=True)
runner.provider_compatible_tool_choice_request = capture_request
runner.preview_lines_until_finish = capture_lines
failures = []
for case, prompt, expected in CASES:
if os.environ.get('ODYSSEUS_ORGANIZER_CASES') and case not in os.environ['ODYSSEUS_ORGANIZER_CASES'].split(','):
continue
calls = []
owner = 'fixture-' + case
history, original_id = await seed_followup(case, owner)
async def execute(block, **kwargs):
print('PROPOSED', case, block.tool_type, block.content[:2000], flush=True)
if block.tool_type != expected:
return block.tool_type, {'exit_code': 1, 'error': 'This recording fixture only implements the requested organizer tool.'}
args = json.loads(block.content)
schema = next(s['function']['parameters'] for s in FUNCTION_TOOL_SCHEMAS
if s['function']['name'] == expected)
jsonschema.validate(args, schema)
calls.append(args)
if expected == 'manage_notes':
return expected, await do_manage_notes(block.content, owner=owner)
if expected == 'manage_tasks':
return expected, await do_manage_tasks(block.content, owner=owner)
return expected, await do_manage_calendar(block.content, owner=owner)
runner.execute_tool_block = execute
policy = ToolPolicy()
contract = resolve_turn_contract(
capabilities=requested_capabilities(prompt, history), schemas=FUNCTION_TOOL_SCHEMAS,
policy=policy, selected_tools=selected_tools_for_request(prompt),
required_tools=selected_tools_for_request(prompt) or (), message=prompt, history=history,
)
events = []
try:
async for chunk in runner.stream_preview(
endpoint_url=os.environ['ODYSSEUS_EDITOR_TEST_ENDPOINT'], model='Ajax', headers={},
messages=[{'role': 'user', 'content': prompt}], turn_contract=contract,
history_session=SimpleNamespace(history=history),
session_id='fixture-organizer-' + case, owner=owner,
disabled_tools=set(), tool_policy=policy, thinking_mode='off', max_rounds=4,
):
if chunk.startswith('data: ') and '[DONE]' not in chunk:
events.append(json.loads(chunk[6:]))
answer = visible_answer(events)
if case in {'checklist-unfinished', 'checklist-finished'}:
assert all(args['action'] in {'view', 'list', 'search'} for args in calls), calls
expected_words = ('keys', 'sam') if case == 'checklist-unfinished' else ('milk',)
excluded_words = ('milk',) if case == 'checklist-unfinished' else ('keys', 'sam')
assert all(word in answer.lower() for word in expected_words), answer
assert not any(word in answer.lower() for word in excluded_words), answer
if case in {'calendar-no-change', 'calendar-wording'} or case.startswith('calendar-hypothetical'):
assert all(args['action'] in {'list_events', 'list_calendars'} for args in calls), calls
assert answer.strip(), f'Empty answer; events: {events}'
assert 'Open the document you want reviewed' not in answer, answer
if case.startswith('calendar-hypothetical'):
pattern = (r'\b(?:12:00|12(?::00)?\s*p\.?m\.?|noon)\b'
if case == 'calendar-hypothetical-earlier'
else r'\b(?:16:00|4(?::00)?\s*p\.?m\.?)\b')
assert re.search(pattern, answer, re.I), answer
with database.SessionLocal() as db:
if expected == 'manage_notes':
rows = db.query(database.Note).filter_by(owner=owner).all()
assert len(rows) == 1, f'Expected one note, got {len(rows)}'
row = rows[0]
if case == 'note-append':
assert row.id == original_id
assert row.title == 'Project ideas'
assert 'Build a small weather display.' in row.content
assert row.content.rstrip().endswith('Check the battery life.')
elif case.startswith('checklist-'):
assert row.id == original_id
assert row.title == ('Weekend errands' if case == 'checklist-rename' else 'Errands')
assert row.note_type == 'checklist'
wanted = [{'text': 'buy milk', 'done': case != 'checklist-uncheck'}]
if case != 'checklist-remove':
wanted.append({'text': 'return the keys', 'done': False})
wanted.append({'text': 'call Sam', 'done': case == 'checklist-check'})
if case == 'checklist-reorder':
wanted.insert(0, wanted.pop())
elif case == 'checklist-append':
wanted.append({'text': 'pick up parcel', 'done': False})
assert json.loads(row.items) == wanted, f'Expected {wanted}, got {row.items}'
else:
validate_case(case, {'action': 'add', 'title': row.title, 'content': row.content or '',
'note_type': row.note_type, 'checklist_items': json.loads(row.items or '[]')})
elif expected == 'manage_tasks':
rows = db.query(database.ScheduledTask).filter_by(owner=owner).all()
assert len(rows) == 1, f'Expected one task, got {len(rows)}'
row = rows[0]
if case.startswith('task-cron-'):
from croniter import croniter
from datetime import datetime
assert row.id == original_id and row.name == 'Tech digest'
assert row.prompt == 'Summarize technology news.'
assert row.status == ('paused' if case == 'task-cron-pause' else 'active')
cron = croniter(row.cron_expression, datetime(2026, 10, 4))
days = (6, 8, 13) if case == 'task-cron-days' else (5, 7, 9)
hour, minute = (10, 30) if case == 'task-cron-retime' else (9, 15)
assert [cron.get_next(datetime) for _ in range(3)] == [
datetime(2026, 10, day, hour, minute) for day in days]
assert row.next_run is not None
elif case in {'task-pause', 'task-retime', 'task-rename'}:
assert row.id == original_id
assert row.name == ('Morning briefing' if case == 'task-rename' else 'Tech digest')
assert row.prompt == 'Summarize technology news.'
assert row.schedule == 'daily'
assert row.scheduled_time == ('08:45' if case == 'task-retime' else '07:30')
assert row.status == ('paused' if case == 'task-pause' else 'active')
elif case == 'task-offset':
assert row.schedule == 'daily' and row.scheduled_time == '22:30'
assert 'technology' in row.prompt.lower()
elif case == 'task-weekdays':
from croniter import croniter
from datetime import datetime
assert row.schedule == 'cron' and row.cron_expression
cron = croniter(row.cron_expression, datetime(2026, 10, 4, 0, 0))
assert [cron.get_next(datetime) for _ in range(3)] == [
datetime(2026, 10, 5, 9, 15), datetime(2026, 10, 7, 9, 15),
datetime(2026, 10, 9, 9, 15)]
assert row.next_run is not None
elif case in {'weekly', 'task-monthly', 'task-monthly-day', 'task-monthly-time'}:
from datetime import datetime
from src.task_scheduler import compute_next_run
after = datetime(2026, 10, 4)
actual = []
for _ in range(3):
after = compute_next_run(row.schedule, row.scheduled_time,
row.scheduled_day, after=after, cron_expression=row.cron_expression)
actual.append(after)
wanted = ([datetime(2026, 10, day, 9) for day in (5, 12, 19)] if case == 'weekly'
else [datetime(2026, month, 15, 8) for month in (10, 11, 12)])
if original_id:
assert row.id == original_id and row.name == 'Budget review'
assert row.prompt == 'Review my budget.' and row.status == 'active'
day = 20 if case == 'task-monthly-day' else 15
hour, minute = (11, 45) if case == 'task-monthly-time' else (8, 0)
wanted = [datetime(2026, month, day, hour, minute) for month in (10, 11, 12)]
assert actual == wanted, f'Expected {wanted}, got {actual}'
else:
validate_case(case, {'action': 'create', 'name': row.name, 'prompt': row.prompt,
'schedule': row.schedule, 'scheduled_time': row.scheduled_time, 'scheduled_day': row.scheduled_day})
else:
rows = db.query(database.CalendarEvent).join(database.CalendarCal).filter(
database.CalendarCal.owner == owner).all()
assert len(rows) == 1, f'Expected one calendar event, got {len(rows)}'
row = rows[0]
if original_id:
assert row.uid == original_id, 'Follow-up must update the existing event'
print('SAVED_CALENDAR', json.dumps({'summary': row.summary,
'start': row.dtstart.isoformat(), 'end': row.dtend.isoformat(),
'is_utc': row.is_utc, 'all_day': row.all_day}), flush=True)
assert not row.rrule, 'One-off event unexpectedly recurs'
assert not row.caldav_sync_pending, 'Fixture must not sync externally'
if case == 'calendar-all-day':
assert row.summary == 'Office closed' and row.all_day
assert row.dtstart.isoformat() == '2026-10-08T00:00:00'
assert row.dtend.isoformat() == '2026-10-09T00:00:00'
elif case in CALENDAR_EXPECTED:
title, start, end = CALENDAR_EXPECTED[case]
assert row.summary == title and row.is_utc and not row.all_day
assert row.dtstart.isoformat() == start, f'Expected UTC start {start}, got {row.dtstart}'
assert row.dtend.isoformat() == end, f'Expected UTC end {end}, got {row.dtend}'
else:
assert row.is_utc and not row.all_day
validate_case(case, {'action': 'create_event', 'summary': row.summary,
'dtstart': row.dtstart.isoformat(), 'dtend': row.dtend.isoformat()})
passed = True
except Exception as exc:
passed = False
failures.append(case)
print(case, type(exc).__name__, str(exc), flush=True)
outputs = [e for e in events if e.get('type') == 'tool_output']
print(json.dumps({'case': case, 'passed': passed, 'calls': calls,
'outputs': outputs, 'answer': visible_answer(events)}, ensure_ascii=False), flush=True)
if failures:
raise SystemExit('Failed: ' + ', '.join(failures))
if __name__ == '__main__':
with tempfile.TemporaryDirectory(prefix='organizer-fixture-') as tmp:
engine = create_engine('sqlite:///' + tmp + '/test.db')
database.Base.metadata.create_all(engine)
database.SessionLocal = sessionmaker(bind=engine)
try:
asyncio.run(main())
finally:
engine.dispose()