from __future__ import annotations import json import sys PNG = b"\x89PNG\r\n\x1a\n" + b"test-image" def create_plain_conversation(logged_in): client, app, projects, _, headers = logged_in folder = projects / "queue-project" folder.mkdir() project = client.post("/api/projects", headers=headers, json={"name": "Queue", "path": str(folder)}).json() conversation = client.post( f"/api/projects/{project['id']}/conversations", headers=headers, json={"title": "Photos"} ).json() return client, app, project, conversation, headers def test_image_upload_is_authenticated_validated_and_private(logged_in): client, _, _, conversation, headers = create_plain_conversation(logged_in) uploaded = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("screen.png", PNG, "image/png")}, ) assert uploaded.status_code == 201 attachment = uploaded.json() assert attachment["content_type"] == "image/png" assert attachment["url"].startswith("/api/attachments/") fetched = client.get(attachment["url"]) assert fetched.status_code == 200 assert fetched.content == PNG client.cookies.clear() assert client.get(attachment["url"]).status_code == 401 def test_invalid_image_payload_is_rejected(logged_in): client, _, _, conversation, headers = create_plain_conversation(logged_in) response = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("fake.png", b"not-an-image", "image/png")}, ) assert response.status_code == 415 def test_conversation_can_display_an_image_from_its_worktree(logged_in): client, app, _, conversation, _ = create_plain_conversation(logged_in) workspace = app.state.settings.data_dir / "worktrees" / conversation["id"] output = workspace / "output" output.mkdir(parents=True) (output / "result.webp").write_bytes(b"RIFFxxxxWEBPpreview") (output / "notes.txt").write_text("not an image", encoding="utf-8") app.state.db.execute( "UPDATE conversations SET worktree_path=? WHERE id=?", (str(workspace), conversation["id"]), ) response = client.get( f"/api/conversations/{conversation['id']}/files/image", params={"path": "output/result.webp"}, ) assert response.status_code == 200 assert response.headers["content-type"].startswith("image/webp") assert response.headers["content-disposition"].startswith("inline;") assert client.get( f"/api/conversations/{conversation['id']}/files/image", params={"path": "output/notes.txt"}, ).status_code == 415 assert client.get( f"/api/conversations/{conversation['id']}/files/image", params={"path": "../secret.png"}, ).status_code == 404 def test_conversation_can_download_an_artifact_from_its_worktree(logged_in): client, app, _, conversation, _ = create_plain_conversation(logged_in) workspace = app.state.settings.data_dir / "worktrees" / conversation["id"] output = workspace / "output" output.mkdir(parents=True) (output / "boards.zip").write_bytes(b"PK\x03\x04archive") app.state.db.execute( "UPDATE conversations SET worktree_path=? WHERE id=?", (str(workspace), conversation["id"]), ) response = client.get( f"/api/conversations/{conversation['id']}/files/download", params={"path": "output/boards.zip"}, ) assert response.status_code == 200 assert response.content == b"PK\x03\x04archive" assert response.headers["content-disposition"].startswith("attachment;") assert "no-store" in response.headers["cache-control"] assert client.get( f"/api/conversations/{conversation['id']}/files/download", params={"path": "../secret.zip"}, ).status_code == 404 client.cookies.clear() assert client.get( f"/api/conversations/{conversation['id']}/files/download", params={"path": "output/boards.zip"}, ).status_code == 401 def test_regular_file_can_be_uploaded_and_downloaded(logged_in): client, _, _, conversation, headers = create_plain_conversation(logged_in) payload = b"hello from an attached document\n" uploaded = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("notes.txt", payload, "text/plain")}, ) assert uploaded.status_code == 201 attachment = uploaded.json() assert attachment["original_name"] == "notes.txt" assert attachment["content_type"] == "text/plain" fetched = client.get(attachment["url"]) assert fetched.status_code == 200 assert fetched.content == payload assert fetched.headers["content-disposition"].startswith("attachment;") def test_file_larger_than_ten_megabytes_can_be_uploaded(logged_in): client, _, _, conversation, headers = create_plain_conversation(logged_in) payload = b"x" * (10 * 1024 * 1024 + 1) uploaded = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("large.txt", payload, "text/plain")}, ) assert uploaded.status_code == 201 assert uploaded.json()["size"] == len(payload) def test_file_over_configured_limit_is_rejected_and_removed(logged_in, monkeypatch): client, app, _, conversation, headers = create_plain_conversation(logged_in) monkeypatch.setattr("codexpi.main.MAX_ATTACHMENT_BYTES", 4) response = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("too-large.txt", b"12345", "text/plain")}, ) assert response.status_code == 413 assert not app.state.db.one("SELECT id FROM attachments WHERE conversation_id=?", (conversation["id"],)) assert not any((app.state.settings.data_dir / "uploads" / conversation["id"]).iterdir()) def test_text_attachment_can_be_opened_and_edited_by_its_owner(logged_in): client, app, _, conversation, headers = create_plain_conversation(logged_in) uploaded = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("long-message.txt", b"original text", "text/plain")}, ).json() updated = client.put( uploaded["url"], headers={**headers, "Content-Type": "text/plain; charset=utf-8"}, content="edited text with accents: été".encode(), ) assert updated.status_code == 200 assert updated.json()["size"] == len("edited text with accents: été".encode()) fetched = client.get(uploaded["url"]) assert fetched.status_code == 200 assert fetched.content == "edited text with accents: été".encode() assert app.state.db.one("SELECT size FROM attachments WHERE id=?", (uploaded["id"],))["size"] == len(fetched.content) client.cookies.clear() assert client.put(uploaded["url"], content=b"forbidden").status_code == 401 def test_binary_attachment_cannot_be_edited_as_text(logged_in): client, _, _, conversation, headers = create_plain_conversation(logged_in) uploaded = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("archive.bin", b"\x00\x01\x02", "application/octet-stream")}, ).json() response = client.put( uploaded["url"], headers={**headers, "Content-Type": "text/plain"}, content=b"replacement", ) assert response.status_code == 415 def test_oversized_text_edit_keeps_original_file(logged_in, monkeypatch): client, _, _, conversation, headers = create_plain_conversation(logged_in) uploaded = client.post( f"/api/conversations/{conversation['id']}/attachments", headers=headers, files={"image": ("notes.txt", b"keep me", "text/plain")}, ).json() monkeypatch.setattr("codexpi.main.MAX_EDITABLE_TEXT_BYTES", 4) response = client.put(uploaded["url"], headers=headers, content=b"12345") assert response.status_code == 413 assert client.get(uploaded["url"]).content == b"keep me" def test_messages_can_be_queued_while_codex_is_running(logged_in): client, app, _, conversation, headers = create_plain_conversation(logged_in) app.state.codex.codex_bin = sys.executable app.state.db.execute("UPDATE conversations SET status='running' WHERE id=?", (conversation["id"],)) first = client.post( f"/api/conversations/{conversation['id']}/queue", headers=headers, json={"content": "Second task"} ) second = client.post( f"/api/conversations/{conversation['id']}/queue", headers=headers, json={"content": "Third task"} ) assert first.status_code == 202 and first.json()["status"] == "queued" assert second.status_code == 202 and second.json()["position"] == 2 queued = client.get(f"/api/conversations/{conversation['id']}/queue").json() assert [item["content"] for item in queued] == ["Second task", "Third task"] cancelled = client.delete(f"/api/conversations/{conversation['id']}/queue/{first.json()['id']}", headers=headers) assert cancelled.status_code == 200 def test_running_action_can_be_stopped_without_cancelling_waiting_messages(logged_in, monkeypatch): client, app, _, conversation, headers = create_plain_conversation(logged_in) stamp = "2026-09-18T12:00:00+00:00" app.state.db.execute( "INSERT INTO prompt_queue(id,conversation_id,content,status,created_at,started_at) VALUES(?,?,?,?,?,?)", ("running-task", conversation["id"], "wrong task", "running", stamp, stamp), ) app.state.db.execute( "INSERT INTO prompt_queue(id,conversation_id,content,status,created_at) VALUES(?,?,?,?,?)", ("next-task", conversation["id"], "keep this", "queued", stamp), ) app.state.db.execute( "UPDATE conversations SET status='running',worker_pid=4242 WHERE id=?", (conversation["id"],), ) monkeypatch.setattr(app.state.codex, "_worker_is_alive", lambda pid, conversation_id: True) assert client.post(f"/api/conversations/{conversation['id']}/stop").status_code == 403 assert app.state.db.one("SELECT status FROM prompt_queue WHERE id='running-task'")["status"] == "running" stopped = client.post(f"/api/conversations/{conversation['id']}/stop", headers=headers) assert stopped.status_code == 200 assert stopped.json() == {"ok": True, "queue_id": "running-task"} assert app.state.db.one("SELECT status FROM prompt_queue WHERE id='running-task'")["status"] == "cancelled" assert app.state.db.one("SELECT status FROM prompt_queue WHERE id='next-task'")["status"] == "queued" assert client.post(f"/api/conversations/{conversation['id']}/stop", headers=headers).status_code == 409 def test_orphaned_worker_is_released(logged_in, monkeypatch): client, app, _, conversation, _ = create_plain_conversation(logged_in) stamp = "2026-09-14T00:00:00+00:00" app.state.db.execute( "INSERT INTO prompt_queue(id,conversation_id,content,status,created_at,started_at) VALUES(?,?,?,?,?,?)", ("orphaned-task", conversation["id"], "stuck", "running", stamp, stamp), ) app.state.db.execute( "UPDATE conversations SET status='running',worker_pid=999999 WHERE id=?", (conversation["id"],), ) monkeypatch.setattr(app.state.codex, "_worker_is_alive", lambda pid, conversation_id: False) assert app.state.codex.recover_orphaned_workers() == 1 task = app.state.db.one("SELECT status,finished_at,error FROM prompt_queue WHERE id='orphaned-task'") restored = app.state.db.one("SELECT status,worker_pid FROM conversations WHERE id=?", (conversation["id"],)) assert task["status"] == "failed" and task["finished_at"] and "libérée" in task["error"] assert restored == {"status": "failed", "worker_pid": None} def test_waiting_messages_can_be_edited_and_cleared(logged_in): client, app, _, conversation, headers = create_plain_conversation(logged_in) app.state.codex.codex_bin = sys.executable app.state.db.execute("UPDATE conversations SET status='running' WHERE id=?", (conversation["id"],)) first = client.post( f"/api/conversations/{conversation['id']}/queue", headers=headers, json={"content": "Initial text"} ).json() second = client.post( f"/api/conversations/{conversation['id']}/queue", headers=headers, json={"content": "Another task"} ).json() edited = client.put( f"/api/conversations/{conversation['id']}/queue/{first['id']}", headers=headers, json={"content": "Edited text"}, ) assert edited.status_code == 200 queued = client.get(f"/api/conversations/{conversation['id']}/queue").json() assert queued[0]["content"] == "Edited text" messages = client.get(f"/api/conversations/{conversation['id']}/messages").json() assert messages[0]["content"] == "Edited text" assert messages[0]["metadata"]["queue_status"] == "queued" cleared = client.delete(f"/api/conversations/{conversation['id']}/queue", headers=headers) assert cleared.status_code == 200 assert cleared.json()["cancelled"] == 2 assert client.get(f"/api/conversations/{conversation['id']}/queue").json() == [] messages = client.get(f"/api/conversations/{conversation['id']}/messages").json() assert all(message["metadata"]["queue_status"] == "cancelled" for message in messages) def test_failed_message_exposes_copyable_error_details(logged_in): client, app, _, conversation, _ = create_plain_conversation(logged_in) stamp = "2026-10-15T08:44:00+00:00" app.state.db.execute( "INSERT INTO prompt_queue(id,conversation_id,content,status,created_at,started_at,finished_at,error) " "VALUES(?,?,?,?,?,?,?,?)", ("failed-task", conversation["id"], "Build", "failed", stamp, stamp, stamp, "Selected model is at capacity."), ) app.state.db.add_message( conversation["id"], "user", "Build", {"queue_id": "failed-task"} ) message = client.get(f"/api/conversations/{conversation['id']}/messages").json()[0] assert message["metadata"]["queue_status"] == "failed" assert message["metadata"]["queue_error"] == "Selected model is at capacity." def test_large_action_outputs_are_compacted_only_in_the_messages_response(logged_in): client, app, _, conversation, _ = create_plain_conversation(logged_in) original_output = "first useful line\n" + ("x" * 20_000) + "\nlast useful line" message_id = app.state.db.add_message( conversation["id"], "assistant", "Done", { "actions": [ { "id": "large-output", "command": "build", "output": original_output, "status": "done", } ] }, ) for index in range(4): app.state.db.add_message(conversation["id"], "assistant", f"Recent answer {index}") message = client.get(f"/api/conversations/{conversation['id']}/messages").json()[0] action = message["metadata"]["actions"][0] assert action["output"].startswith("first useful line") assert action["output"].endswith("last useful line") assert len(action["output"].encode("utf-8")) <= 2_048 assert action["output_truncated"] is True assert action["output_original_bytes"] == len(original_output.encode("utf-8")) stored = app.state.db.one("SELECT metadata FROM messages WHERE id=?", (message_id,)) assert json.loads(stored["metadata"])["actions"][0]["output"] == original_output def test_conversation_can_branch_from_an_earlier_message(logged_in): client, app, project, conversation, headers = create_plain_conversation(logged_in) app.state.db.execute( "UPDATE conversations SET model=?,reasoning_effort=? WHERE id=?", ("gpt-5.6-sol", "high", conversation["id"]), ) first_id = app.state.db.add_message(conversation["id"], "user", "First request") app.state.db.add_message(conversation["id"], "assistant", "First answer") app.state.db.add_message(conversation["id"], "user", "Later request") branched = client.post( f"/api/conversations/{conversation['id']}/branch", headers=headers, json={"message_id": first_id}, ) assert branched.status_code == 201 branch = branched.json() assert "bootstrap_context" not in branch assert branch["parent_conversation_id"] == conversation["id"] assert branch["forked_from_message_id"] == first_id assert branch["model"] == "gpt-5.6-sol" and branch["reasoning_effort"] == "high" copied = client.get(f"/api/conversations/{branch['id']}/messages").json() assert [message["content"] for message in copied] == ["First request"] stored = app.state.db.one("SELECT bootstrap_context FROM conversations WHERE id=?", (branch["id"],)) assert "First request" in stored["bootstrap_context"] listed = client.get(f"/api/projects/{project['id']}/conversations").json() assert all("bootstrap_context" not in item for item in listed)