feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run

This commit is contained in:
CodexPi
2026-11-14 20:23:59 +01:00
commit 995b17a6e2
295 changed files with 36803 additions and 0 deletions

View File

@@ -0,0 +1,769 @@
import json
import fcntl
import io
import os
import socket
import sqlite3
import subprocess
import sys
import threading
from contextlib import ExitStack
from pathlib import Path
from codexpi.codex_service import CodexManager, is_host_agent_project
from codexpi.database import Database, utcnow
from codexpi.session_worker import (
codex_command,
engine_command,
engine_output,
engine_execution_slot,
extract_conversation_title,
main as session_worker_main,
missing_rollout_thread,
rate_limit_retry_delay,
thread_writer_retry_delay,
wait_for_retry,
watch_for_cancellation,
)
def test_missing_rollout_recovery_only_matches_the_codex_resume_failure():
message = "no rollout found for thread id 01a17c67-0e63-7751-a96c-19821f7f5f0e"
assert missing_rollout_thread(message, "codex", "thread-id") is True
assert missing_rollout_thread("Authentication failed", "codex", "thread-id") is False
assert missing_rollout_thread(message, "codex", None) is False
assert missing_rollout_thread(message, "claude", "thread-id") is False
def test_worker_replays_message_in_a_new_thread_when_rollout_is_missing(tmp_path: Path, monkeypatch):
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), stamp))
db.execute(
"INSERT INTO conversations(id,project_id,title,codex_thread_id,status,created_at,updated_at) "
"VALUES(?,?,?,?,?,?,?)",
("c", "p", "Task", "missing-thread", "running", stamp, stamp),
)
db.execute(
"INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)",
("q", "c", "Relance ce message", "running", stamp),
)
launches: list[list[str]] = []
outputs = [
(
'{"type":"error","message":"thread/resume failed: no rollout found for thread id '
'missing-thread (code -32600)"}\n',
1,
),
(
'{"type":"thread.started","thread_id":"replacement-thread"}\n'
'{"type":"item.completed","item":{"type":"agent_message","text":"Terminé"}}\n',
0,
),
]
class FakeProcess:
pid = 12345
def __init__(self, output: str, returncode: int):
self.stdout = io.StringIO(output)
self.returncode = returncode
def wait(self):
return self.returncode
def poll(self):
return self.returncode
def fake_popen(command, **_kwargs):
launches.append(command)
return FakeProcess(*outputs[len(launches) - 1])
monkeypatch.setattr("codexpi.session_worker.subprocess.Popen", fake_popen)
monkeypatch.setattr("codexpi.session_worker.workspace_snapshot", lambda _cwd: None)
events = tmp_path / "events.jsonl"
monkeypatch.setattr(sys, "argv", [
"session-worker",
"--database", str(db.path),
"--events", str(events),
"--conversation", "c",
"--cwd", str(tmp_path),
"--codex", "codex",
"--queue", "q",
])
assert session_worker_main() == 0
assert db.one("SELECT status FROM prompt_queue WHERE id='q'")["status"] == "done"
conversation = db.one("SELECT status,codex_thread_id FROM conversations WHERE id='c'")
assert conversation == {"status": "completed", "codex_thread_id": "replacement-thread"}
assert launches[0][-3:-1] == ["resume", "missing-thread"]
assert launches[0][-1].startswith("Relance ce message")
assert "insère chaque image directement" in launches[0][-1]
assert "![description](output/image.png)" in launches[0][-1]
assert "WebP" in launches[0][-1]
assert "largeur maximale de 1280 px" in launches[0][-1]
assert "resume" not in launches[1]
assert launches[1][-1].startswith("Relance ce message")
assert "insère chaque image directement" in launches[1][-1]
assert '"type": "queue.recovering"' in events.read_text(encoding="utf-8")
def test_rate_limit_errors_use_bounded_progressive_delays():
assert rate_limit_retry_delay("exceeded retry limit, last status: 429 Too Many Requests", 0) == 15
assert rate_limit_retry_delay("rate limit exceeded", 1) == 45
assert rate_limit_retry_delay("HTTP 429", 2) == 120
assert rate_limit_retry_delay("HTTP 429", 3) is None
assert rate_limit_retry_delay("Authentication failed", 0) is None
def test_active_thread_writer_conflicts_use_short_bounded_retries():
message = "thread-store conflict: thread abc already has an active writer"
assert [thread_writer_retry_delay(message, "codex", "abc", index) for index in range(5)] == [2, 5, 15, 30, None]
assert thread_writer_retry_delay(message, "codex", None, 0) is None
assert thread_writer_retry_delay(message, "claude", "abc", 0) is None
assert thread_writer_retry_delay("Authentication failed", "codex", "abc", 0) is None
def test_worker_retries_the_same_thread_after_active_writer_conflict(tmp_path: Path, monkeypatch):
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), stamp))
db.execute(
"INSERT INTO conversations(id,project_id,title,codex_thread_id,status,created_at,updated_at) "
"VALUES(?,?,?,?,?,?,?)",
("c", "p", "Task", "busy-thread", "running", stamp, stamp),
)
db.execute(
"INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)",
("q", "c", "Reprendre sans doublon", "running", stamp),
)
launches: list[list[str]] = []
outputs = [
('{"type":"error","message":"thread-store conflict: thread busy-thread already has an active writer"}\n', 1),
(
'{"type":"thread.started","thread_id":"busy-thread"}\n'
'{"type":"item.completed","item":{"type":"agent_message","text":"Terminé"}}\n',
0,
),
]
class FakeProcess:
pid = 12345
def __init__(self, output: str, returncode: int):
self.stdout = io.StringIO(output)
self.returncode = returncode
def wait(self):
return self.returncode
def poll(self):
return self.returncode
def fake_popen(command, **_kwargs):
launches.append(command)
return FakeProcess(*outputs[len(launches) - 1])
monkeypatch.setattr("codexpi.session_worker.subprocess.Popen", fake_popen)
monkeypatch.setattr("codexpi.session_worker.wait_for_retry", lambda *_args, **_kwargs: True)
monkeypatch.setattr("codexpi.session_worker.workspace_snapshot", lambda _cwd: None)
events = tmp_path / "events.jsonl"
monkeypatch.setattr(sys, "argv", [
"session-worker", "--database", str(db.path), "--events", str(events),
"--conversation", "c", "--cwd", str(tmp_path), "--codex", "codex", "--queue", "q",
])
assert session_worker_main() == 0
assert db.one("SELECT status FROM prompt_queue WHERE id='q'")["status"] == "done"
assert len(launches) == 2
assert launches[0][-3:-1] == ["resume", "busy-thread"]
assert launches[1][-3:-1] == ["resume", "busy-thread"]
event_log = events.read_text(encoding="utf-8")
assert '"reason": "thread_writer"' in event_log
assert "reprise automatique après libération" in event_log
def test_two_api_processes_launch_only_one_worker_for_a_conversation(tmp_path: Path):
path = tmp_path / "db.sqlite"
setup = Database(path)
setup.initialize()
stamp = utcnow()
setup.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), stamp))
setup.execute(
"INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)",
("c", "p", "Task", "idle", stamp, stamp),
)
conversation = setup.one("SELECT * FROM conversations WHERE id='c'")
project = setup.one("SELECT * FROM projects WHERE id='p'")
managers = [
CodexManager(Database(path), tmp_path / "data-a", "/bin/true"),
CodexManager(Database(path), tmp_path / "data-b", "/bin/true"),
]
launches: list[str] = []
launch_lock = threading.Lock()
for manager in managers:
def fake_spawn(_conversation, _project, queue_id, _event_file, _binary):
with launch_lock:
launches.append(queue_id)
return 4200 + len(launches)
manager._spawn = fake_spawn
barrier = threading.Barrier(3)
results: list[dict] = []
def submit(manager: CodexManager, prompt: str):
barrier.wait()
results.append(manager.enqueue(conversation, project, prompt))
workers = [
threading.Thread(target=submit, args=(managers[0], "Premier")),
threading.Thread(target=submit, args=(managers[1], "Second")),
]
for worker in workers:
worker.start()
barrier.wait()
for worker in workers:
worker.join(timeout=10)
assert all(not worker.is_alive() for worker in workers)
assert sorted(result["status"] for result in results) == ["queued", "running"]
assert len(launches) == 1
assert setup.one("SELECT COUNT(*) AS total FROM prompt_queue WHERE status='running'")["total"] == 1
assert setup.one("SELECT COUNT(*) AS total FROM prompt_queue WHERE status='queued'")["total"] == 1
def test_rate_limit_wait_can_be_cancelled(tmp_path: Path):
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), stamp))
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", ("c", "p", "Task", "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", ("q", "c", "Build", "cancelled", stamp))
assert wait_for_retry(str(db.path), "q", 10, interval=0.01) is False
def test_codex_engine_slot_wait_is_cancellable(tmp_path: Path, monkeypatch):
monkeypatch.setenv("CODEXPI_MAX_CONCURRENT_AGENTS", "1")
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), stamp))
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", ("c", "p", "Task", "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", ("q", "c", "Build", "cancelled", stamp))
lock_path = tmp_path / "codex-engine-slot-0.lock"
lock_path.touch()
events = io.StringIO()
with lock_path.open("a+") as held:
fcntl.flock(held.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
with engine_execution_slot(str(db.path), "codex", "q", events, project_id="p", interval=0.01) as acquired:
assert acquired is False
def test_three_projects_can_run_codex_while_a_fourth_waits(tmp_path: Path, monkeypatch):
monkeypatch.setenv("CODEXPI_MAX_CONCURRENT_AGENTS", "3")
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
for index in range(4):
project_id = f"p{index}"
conversation_id = f"c{index}"
queue_id = f"q{index}"
project_path = tmp_path / project_id
project_path.mkdir()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", (project_id, project_id, str(project_path), stamp))
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", (conversation_id, project_id, project_id, "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", (queue_id, conversation_id, project_id, "running", stamp))
events = io.StringIO()
with ExitStack() as stack:
acquired = [
stack.enter_context(engine_execution_slot(str(db.path), "codex", f"q{index}", events, project_id=f"p{index}", interval=0.01))
for index in range(3)
]
assert acquired == [True, True, True]
db.execute("UPDATE prompt_queue SET status='cancelled' WHERE id='q3'")
with engine_execution_slot(str(db.path), "codex", "q3", events, project_id="p3", interval=0.01) as fourth:
assert fourth is False
def test_two_conversations_from_the_same_project_stay_serialized(tmp_path: Path, monkeypatch):
monkeypatch.setenv("CODEXPI_MAX_CONCURRENT_AGENTS", "3")
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), stamp))
for index in range(2):
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", (f"c{index}", "p", "Task", "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", (f"q{index}", f"c{index}", "Build", "running", stamp))
events = io.StringIO()
with engine_execution_slot(str(db.path), "codex", "q0", events, project_id="p", interval=0.01) as first:
assert first is True
db.execute("UPDATE prompt_queue SET status='cancelled' WHERE id='q1'")
with engine_execution_slot(str(db.path), "codex", "q1", events, project_id="p", interval=0.01) as second:
assert second is False
def test_two_projects_in_the_same_execution_group_stay_serialized(tmp_path: Path, monkeypatch):
monkeypatch.setenv("CODEXPI_MAX_CONCURRENT_AGENTS", "3")
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
for index in range(2):
project_id = f"p{index}"
project_path = tmp_path / project_id
project_path.mkdir()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", (project_id, project_id, str(project_path), stamp))
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", (f"c{index}", project_id, "TV", "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", (f"q{index}", f"c{index}", "Build", "running", stamp))
events = io.StringIO()
with engine_execution_slot(str(db.path), "codex", "q0", events, project_id="p0", execution_group="tv", interval=0.01) as first:
assert first is True
db.execute("UPDATE prompt_queue SET status='cancelled' WHERE id='q1'")
with engine_execution_slot(str(db.path), "codex", "q1", events, project_id="p1", execution_group="tv", interval=0.01) as second:
assert second is False
def test_global_agents_do_not_share_the_reserved_workspace_marker(tmp_path: Path, monkeypatch):
monkeypatch.setenv("CODEXPI_MAX_CONCURRENT_AGENTS", "3")
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
for index in range(2):
project_id = f"global-p{index}"
project_path = tmp_path / project_id
project_path.mkdir()
db.execute(
"INSERT INTO projects(id,name,path,execution_group,created_at) VALUES(?,?,?,?,?)",
(project_id, project_id, str(project_path), "__global_agent__", stamp),
)
db.execute(
"INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)",
(f"global-c{index}", project_id, "Global", "running", stamp, stamp),
)
db.execute(
"INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)",
(f"global-q{index}", f"global-c{index}", "Build", "running", stamp),
)
events = io.StringIO()
with engine_execution_slot(
str(db.path), "codex", "global-q0", events,
project_id="global-p0", execution_group="__global_agent__", interval=0.01,
) as first:
assert first is True
# Avoid a hanging regression test: the old behavior observes this
# cancellation while waiting on the shared reserved-marker lock.
db.execute("UPDATE prompt_queue SET status='cancelled' WHERE id='global-q1'")
with engine_execution_slot(
str(db.path), "codex", "global-q1", events,
project_id="global-p1", execution_group="__global_agent__", interval=0.01,
) as second:
assert second is True
def test_projects_with_any_overlapping_group_stay_serialized(tmp_path: Path, monkeypatch):
monkeypatch.setenv("CODEXPI_MAX_CONCURRENT_AGENTS", "3")
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
for index in range(2):
project_id = f"overlap-p{index}"
project_path = tmp_path / project_id
project_path.mkdir()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", (project_id, project_id, str(project_path), stamp))
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", (f"overlap-c{index}", project_id, "Shared", "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", (f"overlap-q{index}", f"overlap-c{index}", "Build", "running", stamp))
events = io.StringIO()
with engine_execution_slot(
str(db.path), "codex", "overlap-q0", events,
project_id="overlap-p0", execution_groups=["salon", "tv"], interval=0.01,
) as first:
assert first is True
db.execute("UPDATE prompt_queue SET status='cancelled' WHERE id='overlap-q1'")
with engine_execution_slot(
str(db.path), "codex", "overlap-q1", events,
project_id="overlap-p1", execution_groups=["media", "tv"], interval=0.01,
) as second:
assert second is False
def test_other_engines_do_not_share_the_codex_execution_slot(tmp_path: Path):
with engine_execution_slot(str(tmp_path / "db.sqlite"), "claude", "q", io.StringIO()) as acquired:
assert acquired is True
def test_all_engines_share_project_group_locks(tmp_path: Path):
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
for index in range(2):
project_id = f"engine-p{index}"
project_path = tmp_path / project_id
project_path.mkdir()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", (project_id, project_id, str(project_path), stamp))
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", (f"engine-c{index}", project_id, "Shared", "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", (f"engine-q{index}", f"engine-c{index}", "Build", "running", stamp))
events = io.StringIO()
with engine_execution_slot(
str(db.path), "codex", "engine-q0", events,
project_id="engine-p0", execution_groups=["tv"], interval=0.01,
) as first:
assert first is True
db.execute("UPDATE prompt_queue SET status='cancelled' WHERE id='engine-q1'")
with engine_execution_slot(
str(db.path), "claude", "engine-q1", events,
project_id="engine-p1", execution_groups=["tv"], interval=0.01,
) as second:
assert second is False
def test_cancellation_watcher_terminates_the_active_engine_process(tmp_path: Path):
db = Database(tmp_path / "db.sqlite")
db.initialize()
stamp = utcnow()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), stamp))
db.execute("INSERT INTO conversations(id,project_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?)", ("c", "p", "Task", "running", stamp, stamp))
db.execute("INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", ("q", "c", "Stop", "cancelled", stamp))
process = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"], start_new_session=True)
stopped = threading.Event()
try:
watch_for_cancellation(str(db.path), "q", process, stopped, poll_interval=0.01)
assert process.wait(timeout=2) != 0
finally:
if process.poll() is None:
process.kill()
def test_conversation_and_event_recovery(tmp_path: Path):
db = Database(tmp_path / "db.sqlite")
db.initialize()
project_id = "11111111-1111-1111-1111-111111111111"
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", (project_id, "Demo", str(tmp_path), utcnow()))
manager = CodexManager(db, tmp_path / "data", "missing-codex")
conversation = manager.create_conversation(project_id, "Persistent task")
event_file = tmp_path / "data" / "sessions" / f"{conversation['id']}.jsonl"
event_file.write_text(json.dumps({"type": "worker.started"}) + "\n")
db.execute("UPDATE conversations SET event_file=? WHERE id=?", (str(event_file), conversation["id"]))
current = db.one("SELECT * FROM conversations WHERE id=?", (conversation["id"],))
events, offset = manager.read_events(current, 0)
assert events == [{"type": "worker.started"}]
assert offset > 0
assert manager.read_events(current, offset)[0] == []
def test_full_access_is_disabled_by_default(tmp_path: Path):
db = Database(tmp_path / "db.sqlite")
db.initialize()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), utcnow()))
assert db.one("SELECT full_access FROM projects WHERE id='p'")["full_access"] == 0
assert db.one("SELECT archived FROM projects WHERE id='p'")["archived"] == 0
def test_automatic_context_compaction_preference_is_persisted(logged_in):
client, _app, _projects, _forbidden, headers = logged_in
assert client.get("/api/application/preferences").json() == {
"auto_compact_context": False,
"interface_type": "dashboard",
"theme": "system",
}
response = client.put("/api/application/preferences", headers=headers, json={"auto_compact_context": True})
assert response.status_code == 200
assert response.json() == {
"auto_compact_context": True,
"interface_type": "dashboard",
"theme": "system",
}
assert client.get("/api/application/preferences").json() == response.json()
def test_interface_and_theme_preferences_are_independent_and_validated(logged_in):
client, _app, _projects, _forbidden, headers = logged_in
selected = client.put(
"/api/application/preferences",
headers=headers,
json={"interface_type": "atelier", "theme": "dark"},
)
assert selected.status_code == 200
assert selected.json() == {
"auto_compact_context": False,
"interface_type": "atelier",
"theme": "dark",
}
compression = client.put(
"/api/application/preferences",
headers=headers,
json={"auto_compact_context": True},
)
assert compression.json()["interface_type"] == "atelier"
assert compression.json()["theme"] == "dark"
assert client.put(
"/api/application/preferences",
headers=headers,
json={"interface_type": "unknown"},
).status_code == 422
def test_safe_codex_command_writes_without_requiring_git():
command = codex_command("codex", "Modify app.py", None, False)
assert command == [
"codex",
"--sandbox", "workspace-write",
"--ask-for-approval", "never",
"exec", "--json", "--skip-git-repo-check",
"Modify app.py",
]
def test_resumed_non_git_session_keeps_safe_write_mode():
command = codex_command("codex", "Continue", "thread-id", False)
assert command[-3:] == ["resume", "thread-id", "Continue"]
assert "--skip-git-repo-check" in command
assert "workspace-write" in command
def test_images_are_attached_to_initial_and_resumed_prompts():
initial = codex_command("codex", "Inspect", None, False, ["/data/a.png", "/data/b.jpg"])
resumed = codex_command("codex", "Continue", "thread-id", False, ["/data/a.png"])
assert initial[-6:] == ["--image", "/data/a.png", "--image", "/data/b.jpg", "--", "Inspect"]
assert resumed[-5:] == ["resume", "--image", "/data/a.png", "thread-id", "Continue"]
def test_full_access_command_is_explicit_and_project_scoped_by_cwd():
command = codex_command("codex", "Run task", None, True)
assert "--dangerously-bypass-approvals-and-sandbox" in command
assert "workspace-write" not in command
def test_privileged_pi_doctor_uses_the_private_host_worker(tmp_path: Path, monkeypatch):
db = Database(tmp_path / "db.sqlite")
db.initialize()
project_root = tmp_path / "pi-doctor"
project_root.mkdir()
project = {
"id": "pi-doctor",
"name": "Pi Doctor",
"path": str(project_root),
"kind": "local",
"full_access": 1,
"execution_group": "__global_agent__",
}
db.execute(
"INSERT INTO projects(id,name,path,kind,full_access,execution_group,created_at) VALUES(?,?,?,?,?,?,?)",
(project["id"], project["name"], project["path"], project["kind"], 1, project["execution_group"], utcnow()),
)
manager = CodexManager(db, tmp_path / "data", "codex")
conversation = manager.create_conversation(project["id"], "Host audit")
socket_path = tmp_path / "agent.sock"
token_file = tmp_path / "agent.token"
token_file.write_text("x" * 48, encoding="utf-8")
os.chmod(token_file, 0o600)
channel = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
channel.bind(str(socket_path))
captured: dict = {}
class Process:
pid = 4321
def fake_popen(argv, **kwargs):
captured["argv"] = argv
return Process()
monkeypatch.setenv("CODEXPI_HOST_AGENT_SOCKET", str(socket_path))
monkeypatch.setenv("CODEXPI_HOST_AGENT_TOKEN_FILE", str(token_file))
monkeypatch.setattr("codexpi.codex_service.subprocess.Popen", fake_popen)
try:
manager._spawn(conversation, project, "queue", tmp_path / "events.jsonl", "codex")
finally:
channel.close()
assert is_host_agent_project(project) is True
assert "codexpi.host_session_worker" in captured["argv"]
assert "--privileged" in captured["argv"]
assert str(socket_path) in captured["argv"]
def test_remote_consultation_forces_read_only_even_with_full_access():
command = codex_command("codex", "Answer only", None, True, read_only=True)
assert command[command.index("--sandbox") + 1] == "read-only"
assert "--dangerously-bypass-approvals-and-sandbox" not in command
def test_optional_agent_engines_have_non_interactive_commands_and_output_parsers():
claude = engine_command("claude", "claude", "Build", "session-1", True, model="sonnet")
assert claude == [
"claude", "-p", "--verbose", "--output-format", "stream-json",
"--model", "sonnet", "--resume", "session-1", "--dangerously-skip-permissions", "Build",
]
opencode = engine_command("opencode", "opencode", "Inspect", None, False, ["/tmp/screen.png"])
assert opencode == ["opencode", "run", "--format", "json", "--file", "/tmp/screen.png", "Inspect"]
session, text = engine_output({"type": "result", "session_id": "claude-1", "result": "Terminé"}, "claude")
assert (session, text) == ("claude-1", "Terminé")
def test_remote_worker_receives_only_its_project_ipc_socket(tmp_path: Path, monkeypatch):
db = Database(tmp_path / "db.sqlite")
db.initialize()
project_root = tmp_path / "remote"
project_root.mkdir()
project = {
"id": "remote-project",
"name": "Remote",
"path": str(project_root),
"kind": "remote",
"full_access": 0,
}
db.execute(
"INSERT INTO projects(id,name,path,kind,created_at) VALUES(?,?,?,?,?)",
(project["id"], project["name"], project["path"], project["kind"], utcnow()),
)
manager = CodexManager(db, tmp_path / "data", "codex")
conversation = manager.create_conversation(project["id"], "Remote session")
captured: dict = {}
class Process:
pid = 4321
def fake_popen(argv, **kwargs):
captured["argv"] = argv
captured["env"] = kwargs["env"]
return Process()
monkeypatch.setattr("codexpi.codex_service.subprocess.Popen", fake_popen)
manager._spawn(conversation, project, "queue", tmp_path / "events.jsonl", "codex")
assert captured["env"]["CODEXPI_REMOTE_CHANNEL"] == str(project_root / ".codexpi-remote")
assert "--full-access" not in captured["argv"]
assert "--read-only" in captured["argv"]
def test_model_and_reasoning_effort_are_scoped_to_conversation(tmp_path: Path):
db = Database(tmp_path / "db.sqlite")
db.initialize()
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", ("p", "Demo", str(tmp_path), utcnow()))
conversation = CodexManager(db, tmp_path / "data", "codex").create_conversation(
"p", "Configured", "gpt-5.6-sol", "high"
)
assert conversation["model"] == "gpt-5.6-sol"
assert conversation["reasoning_effort"] == "high"
command = codex_command("codex", "Build", None, False, model=conversation["model"], reasoning_effort=conversation["reasoning_effort"])
assert command[:5] == ["codex", "--model", "gpt-5.6-sol", "--config", 'model_reasoning_effort="high"']
def test_existing_database_gets_session_configuration_columns(tmp_path: Path):
path = tmp_path / "old.sqlite"
connection = sqlite3.connect(path)
connection.execute(
"CREATE TABLE conversations (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, title TEXT NOT NULL, "
"codex_thread_id TEXT, status TEXT NOT NULL DEFAULT 'idle', event_file TEXT, worker_pid INTEGER, "
"created_at TEXT NOT NULL, updated_at TEXT NOT NULL)"
)
connection.commit()
connection.close()
Database(path).initialize()
connection = sqlite3.connect(path)
columns = {row[1] for row in connection.execute("PRAGMA table_info(conversations)")}
connection.close()
assert {"model", "reasoning_effort", "parent_conversation_id", "forked_from_message_id", "bootstrap_context", "archived", "auto_title", "compacted_message_count", "compacted_character_count", "compacted_at"} <= columns
def test_native_context_compaction_updates_the_conversation_baseline(tmp_path: Path):
fake = tmp_path / "fake-codex"
fake.write_text(
"""#!/usr/bin/env python3
import json, sys
for line in sys.stdin:
request=json.loads(line)
if request.get('id') == 0:
print(json.dumps({'id':0,'result':{'userAgent':'test'}}), flush=True)
elif request.get('id') == 1:
print(json.dumps({'id':1,'result':{'thread':{'id':request['params']['threadId']}}}), flush=True)
elif request.get('id') == 2:
print(json.dumps({'id':2,'result':{}}), flush=True)
print(json.dumps({'method':'item/completed','params':{'item':{'type':'contextCompaction','id':'compact-1'}}}), flush=True)
""",
encoding="utf-8",
)
fake.chmod(0o755)
db = Database(tmp_path / "db.sqlite")
db.initialize()
project_id = "22222222-2222-4222-8222-222222222222"
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", (project_id, "Demo", str(tmp_path), utcnow()))
manager = CodexManager(db, tmp_path / "data", str(fake))
conversation = manager.create_conversation(project_id, "Long task")
db.execute("UPDATE conversations SET codex_thread_id=? WHERE id=?", ("thread-123", conversation["id"]))
db.add_message(conversation["id"], "user", "First request")
db.add_message(conversation["id"], "assistant", "A sufficiently useful answer")
current = db.one("SELECT * FROM conversations WHERE id=?", (conversation["id"],))
compacted = manager.compact(current, {"id": project_id, "path": str(tmp_path)})
assert compacted["compacted_message_count"] == 1
assert compacted["compacted_character_count"] == len("First requestA sufficiently useful answer")
assert compacted["compacted_at"]
action_message = db.one("SELECT metadata FROM messages WHERE conversation_id=? ORDER BY created_at DESC LIMIT 1", (conversation["id"],))
metadata = json.loads(action_message["metadata"])
assert metadata["context_compaction"] is True
assert metadata["actions"][0]["command"] == "Compression du contexte Codex"
assert metadata["actions"][0]["status"] == "done"
def test_native_context_compaction_resets_a_missing_rollout(tmp_path: Path):
fake = tmp_path / "fake-codex"
fake.write_text(
"""#!/usr/bin/env python3
import json, sys
for line in sys.stdin:
request=json.loads(line)
if request.get('id') == 0:
print(json.dumps({'id':0,'result':{'userAgent':'test'}}), flush=True)
elif request.get('id') == 1:
print(json.dumps({'id':1,'result':{'thread':{'id':'missing-thread'}}}), flush=True)
elif request.get('id') == 2:
print(json.dumps({'id':2,'error':{'message':'no rollout found for thread id missing-thread'}}), flush=True)
""",
encoding="utf-8",
)
fake.chmod(0o755)
db = Database(tmp_path / "db.sqlite")
db.initialize()
project_id = "22222222-2222-4222-8222-222222222222"
db.execute("INSERT INTO projects(id,name,path,created_at) VALUES(?,?,?,?)", (project_id, "Demo", str(tmp_path), utcnow()))
manager = CodexManager(db, tmp_path / "data", str(fake))
conversation = manager.create_conversation(project_id, "Long task")
db.execute("UPDATE conversations SET codex_thread_id=? WHERE id=?", ("missing-thread", conversation["id"]))
db.add_message(conversation["id"], "user", "First request")
db.add_message(conversation["id"], "assistant", "An answer")
current = db.one("SELECT * FROM conversations WHERE id=?", (conversation["id"],))
recovered = manager.compact(current, {"id": project_id, "path": str(tmp_path)})
assert recovered["codex_thread_id"] is None
assert recovered["compacted_message_count"] == 1
assert recovered["compacted_character_count"] == len("First requestAn answer")
metadata = json.loads(db.one(
"SELECT metadata FROM messages WHERE conversation_id=? ORDER BY created_at DESC LIMIT 1",
(conversation["id"],),
)["metadata"])
assert metadata["context_compaction"] is True
assert metadata["context_reset"] is True
assert metadata["actions"][0]["status"] == "done"
assert "nouveau fil" in metadata["actions"][0]["output"]
def test_generated_conversation_title_is_removed_from_the_visible_answer():
title, answer = extract_conversation_title(
"<!-- codexpi-title: Gestionnaire Git élégant -->\n\nVoici les changements."
)
assert title == "Gestionnaire Git élégant"
assert answer == "Voici les changements."