mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 23:12:22 +02:00
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.
405 lines
27 KiB
Python
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()
|