import json import subprocess import sys from pathlib import Path from codexpi.database import Database, utcnow from codexpi.message_results import collect_event, recover_results, snapshot_changes, workspace_snapshot from codexpi.session_worker import main def test_commands_are_deduplicated_and_failures_preserved(): results = {} collect_event(results, {'type': 'item.started', 'item': {'id': 'c', 'type': 'command_execution', 'command': 'false'}}, '2026-09-13T10:00:00Z') collect_event(results, {'type': 'item.completed', 'item': {'id': 'c', 'type': 'command_execution', 'command': 'false', 'exit_code': 1, 'aggregated_output': 'failure'}}, '2026-09-13T10:00:02Z') assert len(results['actions']) == 1 assert results['actions'][0]['status'] == 'error' assert results['actions'][0]['duration'] == 2000 assert results['actions'][0]['output'] == 'failure' def test_snapshot_excludes_preexisting_changes(tmp_path): subprocess.run(['git', 'init', str(tmp_path)], check=True, capture_output=True) (tmp_path / 'existing.txt').write_text('already changed') (tmp_path / 'edited.txt').write_text('before') (tmp_path / 'deleted.txt').write_text('before') before = workspace_snapshot(str(tmp_path)) (tmp_path / 'edited.txt').write_text('after') (tmp_path / 'deleted.txt').unlink() (tmp_path / 'new.txt').write_text('new') assert snapshot_changes(before, workspace_snapshot(str(tmp_path))) == [ {'path': 'deleted.txt', 'status': 'D'}, {'path': 'edited.txt', 'status': 'M'}, {'path': 'new.txt', 'status': 'A'}] def test_recovery_does_not_mix_queued_prompts(tmp_path): path = tmp_path / 'events.jsonl' events = [ {'type': 'queue.started', 'data': {'id': 'first'}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:00Z', 'data': {'type': 'item.completed', 'item': {'id': 'c', 'type': 'command_execution', 'command': 'ls', 'exit_code': 0}}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:01Z', 'data': {'type': 'item.completed', 'item': {'type': 'agent_message', 'text': 'Checking files'}}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:02Z', 'data': {'type': 'item.completed', 'item': {'type': 'agent_message', 'text': 'First answer'}}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:02Z', 'data': {'type': 'error', 'message': 'Temporary transport error'}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:02Z', 'data': {'type': 'turn.failed', 'error': {'message': 'Selected model is at capacity.'}}}, {'type': 'queue.started', 'data': {'id': 'second'}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:00Z', 'data': {'type': 'item.completed', 'item': {'type': 'file_change', 'changes': [{'path': 'app.py', 'kind': 'update'}]}}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:03Z', 'data': {'type': 'item.completed', 'item': {'type': 'agent_message', 'text': 'Second answer'}}}, {'type': 'queue.started', 'data': {'id': 'third'}}, {'type': 'codex.event', 'at': '2026-09-13T10:00:04Z', 'data': {'type': 'output', 'text': 'CLI authentication failed'}}, {'type': 'queue.finished', 'data': {'id': 'third', 'returncode': 1, 'status': 'failed'}}, ] path.write_text('\n'.join(map(json.dumps, events)) + '\n{broken') recovered = recover_results([path]) assert len(recovered['first']['actions']) == 1 assert recovered['first']['files'] == [] assert recovered['first']['progress'] == ['Checking files'] assert recovered['first']['answer'] == 'First answer' assert recovered['first']['error'] == 'Selected model is at capacity.' assert recovered['second']['actions'] == [] assert recovered['second']['files'] == [{'path': 'app.py', 'status': 'update'}] assert recovered['second']['progress'] == [] assert recovered['second']['answer'] == 'Second answer' assert recovered['third']['error'] == 'CLI authentication failed' def test_worker_persists_structured_engine_error(tmp_path, monkeypatch): repo = tmp_path / 'repo' repo.mkdir() executable = tmp_path / 'fake-codex' executable.write_text('''#!/usr/bin/env python3 import json, sys print(json.dumps({'type':'error','message':'Selected model is at capacity.'})) print(json.dumps({'type':'turn.failed','error':{'message':'Selected model is at capacity.'}})) sys.exit(1) ''') executable.chmod(0o755) db = Database(tmp_path / 'db.sqlite') db.initialize() stamp = utcnow() db.execute('INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)', ('p', 'Demo', str(repo), stamp)) db.execute('INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)', ('c', 'p', 'Demo', 'running', stamp, stamp)) db.execute('INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)', ('q', 'c', 'Build', 'running', stamp)) monkeypatch.setattr(sys, 'argv', ['worker', '--database', str(db.path), '--events', str(tmp_path / 'events.jsonl'), '--conversation', 'c', '--cwd', str(repo), '--codex', str(executable), '--queue', 'q']) assert main() == 1 assert db.one('SELECT status,error FROM prompt_queue WHERE id=?', ('q',)) == { 'status': 'failed', 'error': 'Selected model is at capacity.', } def test_worker_persists_results_per_message(tmp_path, monkeypatch): repo = tmp_path / 'repo' repo.mkdir() subprocess.run(['git', 'init', str(repo)], check=True, capture_output=True) (repo / 'preexisting.txt').write_text('preexisting dirty file') executable = tmp_path / 'fake-codex' executable.write_text('''#!/usr/bin/env python3 import json, sys from pathlib import Path if sys.argv[-1].startswith('first'): Path('changed.txt').write_text('new content') print(json.dumps({'type':'item.completed','item':{'id':'c','type':'command_execution','command':'write file','exit_code':0}})) print(json.dumps({'type':'item.completed','item':{'type':'agent_message','text':'Progress'}})) print(json.dumps({'type':'item.completed','item':{'type':'agent_message','text':'Answer'}})) ''') executable.chmod(0o755) db = Database(tmp_path / 'db.sqlite') db.initialize() stamp = utcnow() db.execute('INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)', ('p', 'Demo', str(repo), stamp)) db.execute('INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)', ('c', 'p', 'Demo', 'running', stamp, stamp)) for identifier, status in [('first', 'running'), ('second', 'queued')]: db.execute('INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)', (identifier, 'c', identifier, status, stamp)) monkeypatch.setattr(sys, 'argv', ['worker', '--database', str(db.path), '--events', str(tmp_path / 'events.jsonl'), '--conversation', 'c', '--cwd', str(repo), '--codex', str(executable), '--queue', 'first']) started_before = utcnow() assert main() == 0 events = [json.loads(line) for line in (tmp_path / 'events.jsonl').read_text().splitlines()] starts = [event['data'] for event in events if event['type'] == 'queue.started'] assert len(starts) == 2 for start in starts: stored = db.one('SELECT started_at FROM prompt_queue WHERE id=?', (start['id'],)) assert stored['started_at'] == start['started_at'] assert start['started_at'] >= started_before stored_messages = db.all('SELECT * FROM messages') rows = {json.loads(row['metadata'])['queue_id']: json.loads(row['metadata']) for row in stored_messages} answers = {json.loads(row['metadata'])['queue_id']: row['content'] for row in stored_messages} assert len(rows['first']['actions']) == 1 assert rows['first']['files'] == [{'path': 'changed.txt', 'status': 'A'}] assert rows['second']['actions'] == [] assert rows['second']['files'] == [] assert rows['first']['progress'] == ['Progress'] assert rows['second']['progress'] == ['Progress'] assert answers == {'first': 'Answer', 'second': 'Answer'}