mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 12:18:39 +00:00
Reject malformed and negative depth at every managed ingress, preserve admitted depth authority, and quarantine bypassed pending rows before assignment. Add focused API, queue, restore, and budget regressions plus architecture rationale. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2824 lines
116 KiB
Python
2824 lines
116 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import importlib.util
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from starlette.applications import Starlette
|
|
from starlette.routing import Route
|
|
from starlette.testclient import TestClient
|
|
|
|
from ouroboros.gateway.tasks import (
|
|
_compose_task_text,
|
|
_resolve_workspace_root,
|
|
api_task_artifact,
|
|
api_task_events,
|
|
api_task_get,
|
|
api_tasks_create,
|
|
api_tasks_list,
|
|
iter_task_events,
|
|
)
|
|
from ouroboros.headless import (
|
|
ARTIFACT_STATUS_FAILED,
|
|
ARTIFACT_STATUS_FINALIZING,
|
|
ARTIFACT_STATUS_READY,
|
|
ARTIFACT_STATUS_READY_WITH_CHANGES,
|
|
_incidental_lockfile_excludes,
|
|
build_memory_export,
|
|
build_workspace_patch,
|
|
finalize_task_artifacts,
|
|
prune_headless_task_drives,
|
|
prune_task_drives,
|
|
task_artifacts_dir,
|
|
write_workspace_patch_artifacts,
|
|
)
|
|
from ouroboros.task_results import write_task_result
|
|
from ouroboros.tools.core import _repo_read
|
|
from ouroboros.tools.registry import ToolContext, ToolRegistry
|
|
from ouroboros.utils import utc_now_iso
|
|
from ouroboros.workspace_preflight import _infer_tools_from_manifests
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _managed_worker_pool_available(monkeypatch):
|
|
"""HTTP task tests model a ready server unless a case overrides the pool."""
|
|
import supervisor.workers as workers
|
|
|
|
monkeypatch.setattr(workers, "WORKERS", {0: SimpleNamespace()})
|
|
monkeypatch.setattr(workers, "_WORKER_POOL_DISABLED_REASON", "")
|
|
|
|
|
|
def _init_repo_with_file(repo, name="tracked.txt", content="old\n"):
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
(repo / name).write_text(content, encoding="utf-8")
|
|
subprocess.run(["git", "add", name], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
|
|
def test_task_api_enqueue_workspace_creates_child_drive(tmp_path, monkeypatch):
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
data = tmp_path / "data"
|
|
(data / "memory").mkdir(parents=True)
|
|
(data / "memory" / "identity.md").write_text("seed identity", encoding="utf-8")
|
|
|
|
captured = []
|
|
bootstrapped = []
|
|
|
|
def fake_enqueue(task):
|
|
captured.append(dict(task))
|
|
return task
|
|
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", fake_enqueue)
|
|
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
|
|
monkeypatch.setattr("ouroboros.workspace_admission.bootstrap_process_path", lambda: bootstrapped.append(True) or [])
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
response = TestClient(app).post(
|
|
"/api/tasks",
|
|
json={
|
|
"description": "fix it",
|
|
"workspace_root": str(workspace),
|
|
"memory_mode": "forked",
|
|
"expected_output": "A workspace patch and concise handoff.",
|
|
"constraints": "No network.",
|
|
"allowed_resources": {"web": False, "network": False},
|
|
"resource_policy": {
|
|
"protected_artifacts": [
|
|
{
|
|
"id": "reference",
|
|
"role": "black_box_reference",
|
|
"paths": ["reference.bin"],
|
|
"allow": ["execute"],
|
|
}
|
|
]
|
|
},
|
|
"deadline_at": "2026-06-04T12:00:00Z",
|
|
"service_teardown": "keep",
|
|
"context_requires_self_body_docs": "false",
|
|
"metadata": {
|
|
"root_task_id": "forged-root",
|
|
"parent_task_id": "forged-parent",
|
|
"delegation_role": "root",
|
|
"child_drive_root": "/tmp/forged-child",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["task_id"]
|
|
assert bootstrapped
|
|
assert captured and captured[0]["workspace_root"] == str(workspace.resolve(strict=False))
|
|
assert captured[0]["deadline_at"] == "2026-06-04T12:00:00Z"
|
|
assert captured[0]["metadata"]["service_teardown"] == "keep"
|
|
assert captured[0]["allowed_resources"] == {"web": False, "network": False}
|
|
assert captured[0]["context_requires_self_body_docs"] is False
|
|
assert captured[0]["task_contract"]["expected_output"] == "A workspace patch and concise handoff."
|
|
assert captured[0]["task_contract"]["constraints"] == "No network."
|
|
assert captured[0]["task_contract"]["context_requires_self_body_docs"] is False
|
|
assert captured[0]["task_contract"]["resource_policy"]["protected_artifacts"][0]["paths"] == ["reference.bin"]
|
|
child_drive = captured[0]["drive_root"]
|
|
assert child_drive
|
|
assert (tmp_path / "data" / "task_results" / f"{payload['task_id']}.json").is_file()
|
|
assert "seed identity" in (data / "state" / "headless_tasks" / payload["task_id"] / "data" / "memory" / "identity.md").read_text(encoding="utf-8")
|
|
result = json.loads((data / "task_results" / f"{payload['task_id']}.json").read_text(encoding="utf-8"))
|
|
assert result["artifact_status"] == "pending"
|
|
assert captured[0]["root_task_id"] == payload["task_id"]
|
|
assert captured[0]["parent_task_id"] is None
|
|
assert captured[0]["delegation_role"] == "root"
|
|
assert result["metadata"]["root_task_id"] == payload["task_id"]
|
|
assert result["metadata"]["parent_task_id"] == ""
|
|
assert result["metadata"]["delegation_role"] == "root"
|
|
assert result["task_contract"]["deadline_at"] == "2026-06-04T12:00:00Z"
|
|
assert result["task_contract"]["allowed_resources"] == {"web": False, "network": False}
|
|
assert result["task_contract"]["resource_policy"]["protected_artifacts"][0]["id"] == "reference"
|
|
assert result["metadata"]["child_drive_root"] == captured[0]["child_drive_root"]
|
|
assert "/tmp/forged-child" not in json.dumps(result["metadata"])
|
|
assert result["metadata"]["workspace_preflight"]["git"]["head"] == ""
|
|
assert any(item["kind"] == "workspace_preflight" for item in result["artifacts"])
|
|
assert "workspace_preflight:" in captured[0]["text"]
|
|
assert "target workspace, not the Ouroboros system repo" in captured[0]["text"]
|
|
|
|
|
|
def test_task_api_attachment_admission_is_atomic_by_default(tmp_path, monkeypatch):
|
|
import supervisor.queue as queue
|
|
from ouroboros.task_results import load_task_result
|
|
|
|
data = tmp_path / "data"
|
|
repo = tmp_path / "repo"
|
|
data.mkdir()
|
|
repo.mkdir()
|
|
good = tmp_path / "good.txt"
|
|
good.write_text("ok", encoding="utf-8")
|
|
captured = []
|
|
monkeypatch.setattr(queue, "enqueue_task", lambda task: captured.append(task) or task)
|
|
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
|
|
response = TestClient(app).post(
|
|
"/api/tasks",
|
|
json={
|
|
"task_id": "atomic-attachments",
|
|
"description": "needs both",
|
|
"attachments": [
|
|
{"path": str(good), "label": "good"},
|
|
{"path": str(tmp_path / "missing.txt"), "label": "missing"},
|
|
],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
payload = response.json()
|
|
assert payload["reason_code"] == "attachment_admission_rejected"
|
|
assert [row["status"] for row in payload["attachment_manifest"]] == ["staged", "rejected"]
|
|
assert payload["attachment_manifest"][1]["reason"] == "source_missing"
|
|
assert captured == []
|
|
assert load_task_result(data, "atomic-attachments") is None
|
|
assert "atomic-attachments" not in queue.ADMISSION_RESERVATIONS
|
|
assert not task_artifacts_dir(data, "atomic-attachments", create=False).exists()
|
|
|
|
|
|
def test_task_api_explicit_partial_attachments_reaches_caller_contract_and_actor(
|
|
tmp_path, monkeypatch,
|
|
):
|
|
import supervisor.queue as queue
|
|
|
|
data = tmp_path / "data"
|
|
repo = tmp_path / "repo"
|
|
data.mkdir()
|
|
repo.mkdir()
|
|
good = tmp_path / "good.txt"
|
|
good.write_text("ok", encoding="utf-8")
|
|
captured = []
|
|
monkeypatch.setattr(queue, "enqueue_task", lambda task: captured.append(task) or task)
|
|
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
|
|
response = TestClient(app).post(
|
|
"/api/tasks",
|
|
json={
|
|
"task_id": "partial-attachments",
|
|
"description": "work with what arrived",
|
|
"allow_partial_attachments": True,
|
|
"attachments": [
|
|
{"path": str(good), "label": "good"},
|
|
{"path": str(tmp_path / "missing.txt"), "label": "missing"},
|
|
],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
manifest = response.json()["attachment_manifest"]
|
|
assert [row["status"] for row in manifest] == ["staged", "rejected"]
|
|
task = captured[0]
|
|
assert task["attachments"] == manifest
|
|
assert task["task_contract"]["attachment_manifest"] == manifest
|
|
assert "reason=source_missing" in task["text"]
|
|
result = json.loads(
|
|
(data / "task_results" / "partial-attachments.json").read_text(encoding="utf-8")
|
|
)
|
|
assert result["attachment_manifest"] == manifest
|
|
|
|
|
|
def test_task_api_admission_refusal_is_terminal_not_scheduled_phantom(tmp_path, monkeypatch):
|
|
from ouroboros.task_results import STATUS_FAILED, load_task_result
|
|
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
data = tmp_path / "data"
|
|
(data / "memory").mkdir(parents=True)
|
|
persisted = []
|
|
|
|
monkeypatch.setattr(
|
|
"supervisor.queue.enqueue_task",
|
|
lambda task: {
|
|
**task,
|
|
"_admission_blocked": "project_routing_fence",
|
|
"_project_id": "closed-project",
|
|
"_project_lifecycle": "deleting",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
"supervisor.queue.persist_queue_snapshot",
|
|
lambda reason="": persisted.append(reason),
|
|
)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
response = TestClient(app).post(
|
|
"/api/tasks",
|
|
json={
|
|
"description": "must not run",
|
|
"task_id": "blocked-root",
|
|
"project_id": "closed-project",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
payload = response.json()
|
|
assert payload["task_id"] == "blocked-root"
|
|
assert payload["status"] == STATUS_FAILED
|
|
assert payload["admission"]["reason_code"] == "project_routing_fence"
|
|
assert payload["admission"]["project_lifecycle"] == "deleting"
|
|
assert persisted == []
|
|
result = load_task_result(data, "blocked-root")
|
|
assert result["status"] == STATUS_FAILED
|
|
assert result["reason_code"] == "project_routing_fence"
|
|
assert result["admission_cleanup"] == {"child_drive_removed": True}
|
|
assert not (data / "state" / "headless_tasks" / "blocked-root").exists()
|
|
|
|
|
|
def test_task_api_refuses_when_durable_queue_snapshot_fails(tmp_path, monkeypatch):
|
|
import supervisor.queue as queue
|
|
from ouroboros.task_results import STATUS_FAILED, load_task_result
|
|
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
data = tmp_path / "data"
|
|
(data / "memory").mkdir(parents=True)
|
|
pending = []
|
|
monkeypatch.setattr(queue, "DRIVE_ROOT", data)
|
|
monkeypatch.setattr(queue, "PENDING", pending)
|
|
monkeypatch.setattr(queue, "RUNNING", {})
|
|
calls = []
|
|
|
|
def persist(reason=""):
|
|
calls.append(reason)
|
|
return reason == "api_task_create_rollback"
|
|
|
|
monkeypatch.setattr(queue, "persist_queue_snapshot", persist)
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
response = TestClient(app).post(
|
|
"/api/tasks",
|
|
json={"description": "must be durable", "task_id": "snapshot-fail"},
|
|
)
|
|
|
|
assert response.status_code == 503
|
|
assert response.json()["admission"]["reason_code"] == "queue_snapshot_persist_failed"
|
|
assert pending == []
|
|
assert calls == ["api_task_create", "api_task_create_rollback"]
|
|
assert load_task_result(data, "snapshot-fail")["status"] == STATUS_FAILED
|
|
assert not (data / "state" / "headless_tasks" / "snapshot-fail").exists()
|
|
|
|
|
|
def test_late_api_identity_lookup_failure_preserves_exact_result(tmp_path):
|
|
from ouroboros.gateway.tasks import _admission_rejection_response
|
|
|
|
result_path = tmp_path / "task_results" / "api-corrupt.json"
|
|
result_path.parent.mkdir()
|
|
original = b"{api-corrupt"
|
|
result_path.write_bytes(original)
|
|
response = _admission_rejection_response(
|
|
{"_admission_blocked": "task_id_lookup_failed"},
|
|
drive_root=tmp_path, task_id="api-corrupt", project_id="",
|
|
workspace_root=None, child_drive=None,
|
|
)
|
|
assert response is not None and response.status_code == 409
|
|
assert json.loads(response.body)["admission"]["reason_code"] == "task_id_lookup_failed"
|
|
assert result_path.read_bytes() == original
|
|
|
|
|
|
def test_task_api_releases_reservation_when_payload_composition_fails(
|
|
tmp_path, monkeypatch,
|
|
):
|
|
import supervisor.queue as queue
|
|
from ouroboros.gateway import tasks
|
|
|
|
data = tmp_path / "data"
|
|
repo = tmp_path / "repo"
|
|
data.mkdir()
|
|
repo.mkdir()
|
|
task_id = "compose-failure"
|
|
real_compose = tasks._compose_task_text
|
|
monkeypatch.setattr(
|
|
tasks,
|
|
"_compose_task_text",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("compose failed")),
|
|
)
|
|
monkeypatch.setattr(queue, "enqueue_task", lambda task: task)
|
|
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda **_kwargs: True)
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
client = TestClient(app)
|
|
|
|
failed = client.post(
|
|
"/api/tasks", json={"task_id": task_id, "description": "compose me"}
|
|
)
|
|
assert failed.status_code == 503
|
|
assert task_id not in queue.ADMISSION_RESERVATIONS
|
|
assert not task_artifacts_dir(data, task_id, create=False).exists()
|
|
|
|
monkeypatch.setattr(tasks, "_compose_task_text", real_compose)
|
|
retried = client.post(
|
|
"/api/tasks", json={"task_id": task_id, "description": "compose me"}
|
|
)
|
|
assert retried.status_code == 200, retried.text
|
|
|
|
|
|
def test_api_tasks_create_requires_description_not_legacy_aliases(monkeypatch):
|
|
captured = []
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: captured.append(task) or task)
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
client = TestClient(app)
|
|
|
|
for payload in ({"text": "legacy task"}, {"prompt": "legacy task"}, {"description": ""}):
|
|
response = client.post("/api/tasks", json=payload)
|
|
assert response.status_code == 400, (payload, response.text)
|
|
assert "description is required" in response.json().get("error", "")
|
|
|
|
response = client.post("/api/tasks", json={"description": "x", "service_teardown": "detach"})
|
|
assert response.status_code == 400
|
|
assert "service_teardown" in response.json().get("error", "")
|
|
|
|
assert captured == []
|
|
|
|
|
|
def test_api_tasks_create_rejects_internal_task_types(tmp_path, monkeypatch):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
data = tmp_path / "data"
|
|
(data / "memory").mkdir(parents=True)
|
|
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: task)
|
|
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
|
|
monkeypatch.setattr("ouroboros.workspace_admission.bootstrap_process_path", lambda: [])
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
client = TestClient(app)
|
|
|
|
for internal_type in ("evolution", "review", "deep_self_review"):
|
|
resp = client.post("/api/tasks", json={"description": "x", "type": internal_type})
|
|
assert resp.status_code == 400, (internal_type, resp.text)
|
|
assert "internal" in resp.json().get("error", "").lower()
|
|
|
|
# A normal task type is still accepted.
|
|
ok = client.post("/api/tasks", json={"description": "do normal work", "type": "task"})
|
|
assert ok.status_code == 200, ok.text
|
|
|
|
|
|
def test_compose_task_text_extends_existing_headless_workspace_block(tmp_path):
|
|
text = _compose_task_text(
|
|
"fix\n\n[HEADLESS_WORKSPACE]\nexisting: yes\n[END_HEADLESS_WORKSPACE]",
|
|
workspace_root=tmp_path,
|
|
workspace_mode="external",
|
|
memory_mode="empty",
|
|
workspace_preflight={"error": "probe failed"},
|
|
attachments=[],
|
|
)
|
|
|
|
assert text.count("[HEADLESS_WORKSPACE]") == 1
|
|
assert "existing: yes" in text
|
|
assert "preflight_error: probe failed" in text
|
|
assert text.index("workspace_root:") < text.index("[END_HEADLESS_WORKSPACE]")
|
|
|
|
|
|
def test_task_api_rejects_unsafe_task_id_and_system_workspace(tmp_path, monkeypatch):
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
data = tmp_path / "data"
|
|
data.mkdir()
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: task)
|
|
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
client = TestClient(app)
|
|
|
|
bad_id = client.post("/api/tasks", json={"description": "x", "task_id": "../settings", "workspace_root": str(workspace)})
|
|
assert bad_id.status_code == 400
|
|
assert not (data / "settings.json").exists()
|
|
|
|
system_repo = client.post("/api/tasks", json={"description": "x", "workspace_root": str(repo)})
|
|
assert system_repo.status_code == 400
|
|
assert "system repo" in system_repo.json()["error"]
|
|
|
|
bad_numbers = client.post("/api/tasks", json={"description": "x", "chat_id": "not-int", "workspace_root": str(workspace)})
|
|
assert bad_numbers.status_code == 400
|
|
bad_deadline = client.post("/api/tasks", json={"description": "x", "deadline_at": "not-a-date", "workspace_root": str(workspace)})
|
|
assert bad_deadline.status_code == 400
|
|
assert "deadline_at" in bad_deadline.json()["error"]
|
|
naive_deadline = client.post("/api/tasks", json={"description": "x", "deadline_at": "2026-06-04T12:00:00", "workspace_root": str(workspace)})
|
|
assert naive_deadline.status_code == 400
|
|
assert "timezone" in naive_deadline.json()["error"]
|
|
|
|
first = client.post("/api/tasks", json={"description": "x", "task_id": "fixed1", "workspace_root": str(workspace)})
|
|
assert first.status_code == 200
|
|
duplicate = client.post("/api/tasks", json={"description": "x", "task_id": "fixed1", "workspace_root": str(workspace)})
|
|
assert duplicate.status_code == 409
|
|
|
|
typed = client.post("/api/tasks", json={"description": "x", "type": "deep_self_review", "workspace_root": str(workspace)})
|
|
assert typed.status_code == 400
|
|
|
|
|
|
def test_task_api_rejects_negative_depth_before_reservation_or_queue(tmp_path, monkeypatch):
|
|
from supervisor import queue as queue_module
|
|
|
|
repo = tmp_path / "repo"
|
|
data = tmp_path / "data"
|
|
repo.mkdir()
|
|
data.mkdir()
|
|
captured = []
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: captured.append(task) or task)
|
|
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
|
|
|
|
def fail_reservation(*_args, **_kwargs):
|
|
pytest.fail("invalid depth must be rejected before admission reservation")
|
|
|
|
monkeypatch.setattr(queue_module, "reserve_task_admission", fail_reservation)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
client = TestClient(app)
|
|
|
|
cases = ((-1, "depth must be a non-negative integer"),
|
|
(-0.5, "depth must be a non-negative integer"),
|
|
("-1", "depth must be a non-negative integer"),
|
|
("not-a-depth", "chat_id and depth must be integers"))
|
|
for index, (raw_depth, expected_error) in enumerate(cases):
|
|
task_id = f"api-invalid-depth-{index}"
|
|
response = client.post(
|
|
"/api/tasks", json={"task_id": task_id, "description": "x", "depth": raw_depth}
|
|
)
|
|
assert response.status_code == 400
|
|
assert response.json()["error"] == expected_error
|
|
assert task_id not in queue_module.ADMISSION_RESERVATIONS
|
|
assert not (data / "task_results" / f"{task_id}.json").exists()
|
|
assert captured == []
|
|
|
|
|
|
def test_resolve_workspace_root_blocks_case_variant_control_plane(tmp_path):
|
|
system_repo = tmp_path / "Ouroboros" / "repo"
|
|
drive = tmp_path / "Ouroboros" / "data"
|
|
workspace_repo_case = tmp_path / "ouroboros" / "repo"
|
|
workspace_data_case = tmp_path / "ouroboros" / "data" / "workspace"
|
|
for path in (system_repo, drive / "workspace"):
|
|
path.mkdir(parents=True)
|
|
|
|
with pytest.raises(ValueError, match="Ouroboros system repo"):
|
|
_resolve_workspace_root(workspace_repo_case, system_repo_dir=system_repo, drive_root=drive)
|
|
with pytest.raises(ValueError, match="Ouroboros data drive"):
|
|
_resolve_workspace_root(workspace_data_case, system_repo_dir=system_repo, drive_root=drive)
|
|
|
|
|
|
def test_task_api_rejects_forged_subagent_without_child_drive_side_effect(tmp_path, monkeypatch):
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
data = tmp_path / "data"
|
|
data.mkdir()
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: pytest.fail("forged subagent enqueued"))
|
|
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
client = TestClient(app)
|
|
|
|
top_level = client.post(
|
|
"/api/tasks",
|
|
json={"description": "x", "task_id": "forged1", "workspace_root": str(workspace), "delegation_role": "subagent"},
|
|
)
|
|
metadata = client.post(
|
|
"/api/tasks",
|
|
json={"description": "x", "task_id": "forged2", "workspace_root": str(workspace), "metadata": {"delegation_role": "subagent"}},
|
|
)
|
|
|
|
assert top_level.status_code == 400
|
|
assert metadata.status_code == 400
|
|
assert "internal schedule_subagent" in top_level.json()["error"]
|
|
assert not (data / "state" / "headless_tasks" / "forged1").exists()
|
|
assert not (data / "state" / "headless_tasks" / "forged2").exists()
|
|
|
|
|
|
def test_task_api_rejects_external_lineage_forgery(tmp_path, monkeypatch):
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
data = tmp_path / "data"
|
|
data.mkdir()
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: pytest.fail("forged lineage enqueued"))
|
|
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
|
|
response = TestClient(app).post(
|
|
"/api/tasks",
|
|
json={
|
|
"description": "x",
|
|
"workspace_root": str(workspace),
|
|
"parent_task_id": "parent1",
|
|
"root_task_id": "root1",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert "internal lineage fields" in response.json()["error"]
|
|
assert not list((data / "task_results").glob("*.json"))
|
|
|
|
|
|
def test_task_api_preserves_top_level_actor_id_after_metadata_sanitization(tmp_path, monkeypatch):
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
data = tmp_path / "data"
|
|
data.mkdir()
|
|
captured = []
|
|
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: captured.append(dict(task)) or task)
|
|
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
|
|
app.state.drive_root = data
|
|
app.state.repo_dir = repo
|
|
|
|
response = TestClient(app).post(
|
|
"/api/tasks",
|
|
json={
|
|
"description": "x",
|
|
"workspace_root": str(workspace),
|
|
"memory_mode": "forked",
|
|
"actor_id": "operator-1",
|
|
"metadata": {"actor_id": "forged-metadata"},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert captured[0]["actor_id"] == "operator-1"
|
|
result = json.loads((data / "task_results" / f"{response.json()['task_id']}.json").read_text(encoding="utf-8"))
|
|
assert result["metadata"]["actor_id"] == "operator-1"
|
|
assert "forged-metadata" not in json.dumps(result)
|
|
|
|
|
|
def test_task_event_replay_uses_existing_logs_and_result(tmp_path):
|
|
data = tmp_path / "data"
|
|
logs = data / "logs"
|
|
logs.mkdir(parents=True)
|
|
task_id = "abc123"
|
|
(logs / "progress.jsonl").write_text(
|
|
json.dumps({"ts": "2026-01-01T00:00:00Z", "task_id": task_id, "content": "working"}) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
result_dir = data / "task_results"
|
|
result_dir.mkdir()
|
|
(result_dir / f"{task_id}.json").write_text(
|
|
json.dumps({"task_id": task_id, "status": "completed", "result": "done", "ts": "2026-01-01T00:00:01Z"}),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
events = iter_task_events(data, task_id)
|
|
|
|
assert [event["type"] for event in events] == ["progress", "task_result"]
|
|
assert events[0]["seq"] == 1
|
|
assert events[1]["data"]["result"] == "done"
|
|
|
|
|
|
def test_task_event_replay_parent_includes_child_lineage_events(tmp_path):
|
|
data = tmp_path / "data"
|
|
logs = data / "logs"
|
|
logs.mkdir(parents=True)
|
|
parent_id = "parent1"
|
|
child_id = "child1"
|
|
(logs / "progress.jsonl").write_text(
|
|
"\n".join([
|
|
json.dumps({"ts": "2026-01-01T00:00:00Z", "task_id": parent_id, "content": "parent"}),
|
|
json.dumps({
|
|
"ts": "2026-01-01T00:00:01Z",
|
|
"task_id": child_id,
|
|
"parent_task_id": parent_id,
|
|
"root_task_id": parent_id,
|
|
"delegation_role": "subagent",
|
|
"subagent_task_id": child_id,
|
|
"content": "child progress",
|
|
}),
|
|
]) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
write_task_result(
|
|
data,
|
|
parent_id,
|
|
"running",
|
|
result="parent pending",
|
|
ts="2026-01-01T00:00:00Z",
|
|
)
|
|
write_task_result(
|
|
data,
|
|
child_id,
|
|
"running",
|
|
result="child pending",
|
|
parent_task_id=parent_id,
|
|
root_task_id=parent_id,
|
|
delegation_role="subagent",
|
|
ts="2026-01-01T00:00:01Z",
|
|
)
|
|
|
|
events = iter_task_events(data, parent_id)
|
|
|
|
progress_events = [event for event in events if event["type"] == "progress"]
|
|
assert [event["task_id"] for event in progress_events] == [parent_id, child_id]
|
|
assert progress_events[1]["data"]["content"] == "child progress"
|
|
|
|
|
|
def test_logs_tail_parent_filter_includes_child_lineage_events(tmp_path):
|
|
from ouroboros.gateway.logs import api_logs_tail
|
|
|
|
data = tmp_path / "data"
|
|
logs = data / "logs"
|
|
logs.mkdir(parents=True)
|
|
(logs / "progress.jsonl").write_text(
|
|
"\n".join([
|
|
json.dumps({"ts": "2026-01-01T00:00:00Z", "task_id": "parent1", "content": "parent"}),
|
|
json.dumps({
|
|
"ts": "2026-01-01T00:00:01Z",
|
|
"task_id": "child1",
|
|
"subagent_task_id": "child1",
|
|
"parent_task_id": "parent1",
|
|
"root_task_id": "parent1",
|
|
"delegation_role": "subagent",
|
|
"content": "child",
|
|
}),
|
|
json.dumps({"ts": "2026-01-01T00:00:02Z", "task_id": "other", "content": "other"}),
|
|
]) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
app = Starlette(routes=[Route("/api/logs/{name}", endpoint=api_logs_tail, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
|
|
response = TestClient(app).get("/api/logs/progress?task_id=parent1&limit=10")
|
|
payload = response.json()
|
|
|
|
assert response.status_code == 200
|
|
assert [row["content"] for row in payload["entries"]] == ["parent", "child"]
|
|
|
|
|
|
def test_workspace_event_replay_suppresses_task_done_until_artifacts_terminal(tmp_path):
|
|
data = tmp_path / "data"
|
|
logs = data / "logs"
|
|
logs.mkdir(parents=True)
|
|
task_id = "abc123"
|
|
(logs / "events.jsonl").write_text(
|
|
json.dumps({"ts": "2026-01-01T00:00:01Z", "type": "task_done", "task_id": task_id}) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
write_task_result(
|
|
data,
|
|
task_id,
|
|
"completed",
|
|
workspace_root=str(tmp_path / "workspace"),
|
|
artifact_status="finalizing",
|
|
child_status="completed",
|
|
)
|
|
|
|
events = iter_task_events(data, task_id)
|
|
|
|
assert "task_done" not in [event["type"] for event in events]
|
|
assert events[-1]["type"] == "task_result"
|
|
|
|
|
|
def test_effective_child_completion_waits_for_artifacts(tmp_path):
|
|
data = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
for root in (data, child):
|
|
(root / "task_results").mkdir(parents=True)
|
|
write_task_result(
|
|
data,
|
|
"task-artifacts",
|
|
"scheduled",
|
|
child_drive_root=str(child),
|
|
workspace_root=str(tmp_path / "workspace"),
|
|
artifact_status="pending",
|
|
result="queued",
|
|
)
|
|
write_task_result(
|
|
child,
|
|
"task-artifacts",
|
|
"completed",
|
|
result="done",
|
|
ts="2026-01-01T00:00:02Z",
|
|
outcome_axes={
|
|
"lifecycle": {"status": "completed"},
|
|
"artifacts": {"status": "not_applicable"},
|
|
},
|
|
)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
payload = TestClient(app).get("/api/tasks/task-artifacts").json()
|
|
|
|
assert payload["status"] == "running"
|
|
assert payload["artifact_status"] == "finalizing"
|
|
assert payload["child_status"] == "completed"
|
|
assert payload["outcome_axes"]["lifecycle"]["status"] == "running"
|
|
assert payload["outcome_axes"]["artifacts"]["status"] == "finalizing"
|
|
|
|
write_task_result(data, "task-artifacts", "completed", artifact_status="ready", child_drive_root=str(child), workspace_root=str(tmp_path / "workspace"))
|
|
payload = TestClient(app).get("/api/tasks/task-artifacts").json()
|
|
assert payload["status"] == "completed"
|
|
assert payload["artifact_status"] == "ready"
|
|
|
|
|
|
def test_public_task_result_strips_nested_legacy_result_status(tmp_path):
|
|
data = tmp_path / "data"
|
|
(data / "task_results").mkdir(parents=True)
|
|
write_task_result(
|
|
data,
|
|
"legacy-loop",
|
|
"completed",
|
|
result="done",
|
|
loop_outcome={"result_status": "failed", "compat_result_status": "failed", "reason_code": "legacy"},
|
|
verification_ledger={
|
|
"entries": [
|
|
{"kind": "legacy", "result_status": "partial"},
|
|
{"kind": "nested", "payload": {"compat_result_status": "infra_failed"}},
|
|
{"kind": "list", "items": [{"result_status": "failed"}]},
|
|
],
|
|
},
|
|
)
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
|
|
payload = TestClient(app).get("/api/tasks/legacy-loop").json()
|
|
|
|
assert "result_status" not in payload
|
|
assert "result_status" not in payload["loop_outcome"]
|
|
assert "compat_result_status" not in payload["loop_outcome"]
|
|
rendered = json.dumps(payload)
|
|
assert "result_status" not in rendered
|
|
assert "compat_result_status" not in rendered
|
|
|
|
|
|
def test_effective_child_failure_waits_for_artifacts(tmp_path):
|
|
data = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
for root in (data, child):
|
|
(root / "task_results").mkdir(parents=True)
|
|
write_task_result(
|
|
data,
|
|
"task-failed",
|
|
"failed",
|
|
child_drive_root=str(child),
|
|
workspace_root=str(tmp_path / "workspace"),
|
|
artifact_status="finalizing",
|
|
child_status="failed",
|
|
result="boom",
|
|
)
|
|
write_task_result(child, "task-failed", "failed", result="boom", ts="2026-01-01T00:00:02Z")
|
|
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
payload = TestClient(app).get("/api/tasks/task-failed").json()
|
|
|
|
assert payload["status"] == "running"
|
|
assert payload["artifact_status"] == "finalizing"
|
|
assert payload["child_status"] == "failed"
|
|
|
|
|
|
def test_task_sse_emits_final_result_after_cursor_saw_scheduled_result(tmp_path):
|
|
data = tmp_path / "data"
|
|
(data / "task_results").mkdir(parents=True)
|
|
task_id = "abc123"
|
|
(data / "task_results" / f"{task_id}.json").write_text(
|
|
json.dumps({"task_id": task_id, "status": "completed", "result": "done", "ts": "2026-01-01T00:00:01Z"}),
|
|
encoding="utf-8",
|
|
)
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}/events", endpoint=api_task_events, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
|
|
response = TestClient(app).get(f"/api/tasks/{task_id}/events?cursor=1&wait=0")
|
|
|
|
assert response.status_code == 200
|
|
assert '"type": "task_result"' in response.text
|
|
assert '"status": "completed"' in response.text
|
|
|
|
|
|
def test_task_list_filters_on_effective_child_status(tmp_path):
|
|
data = tmp_path / "data"
|
|
child_running = tmp_path / "child-running"
|
|
child_done = tmp_path / "child-done"
|
|
for root in (data, child_running, child_done):
|
|
(root / "task_results").mkdir(parents=True)
|
|
|
|
write_task_result(data, "task-running", "scheduled", child_drive_root=str(child_running), result="queued")
|
|
write_task_result(child_running, "task-running", "running", result="working", ts="2026-01-01T00:00:01Z")
|
|
write_task_result(data, "task-done", "scheduled", child_drive_root=str(child_done), result="queued")
|
|
write_task_result(child_done, "task-done", "completed", result="done", ts="2026-01-01T00:00:02Z")
|
|
|
|
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_list, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
client = TestClient(app)
|
|
|
|
running = client.get("/api/tasks?status=running").json()["tasks"]
|
|
completed = client.get("/api/tasks?status=completed").json()["tasks"]
|
|
|
|
assert [task["task_id"] for task in running] == ["task-running"]
|
|
assert running[0]["result"] == "working"
|
|
assert [task["task_id"] for task in completed] == ["task-done"]
|
|
assert completed[0]["result"] == "done"
|
|
|
|
|
|
@pytest.mark.parametrize("status", ["cancelled", "failed"])
|
|
def test_effective_task_result_preserves_parent_terminal_status(tmp_path, status):
|
|
data = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
for root in (data, child):
|
|
(root / "task_results").mkdir(parents=True)
|
|
write_task_result(
|
|
data,
|
|
"task-terminal",
|
|
status,
|
|
child_drive_root=str(child),
|
|
result="parent terminal",
|
|
ts="2026-01-01T00:00:02Z",
|
|
)
|
|
write_task_result(
|
|
child,
|
|
"task-terminal",
|
|
"running",
|
|
result="child stale",
|
|
ts="2026-01-01T00:00:03Z",
|
|
)
|
|
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
|
|
payload = TestClient(app).get("/api/tasks/task-terminal").json()
|
|
|
|
assert payload["status"] == status
|
|
assert payload["result"] == "parent terminal"
|
|
assert payload["ts"] == "2026-01-01T00:00:02Z"
|
|
|
|
|
|
def test_workspace_context_routes_project_files_and_keeps_system_tools_reachable(tmp_path):
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
data = tmp_path / "data"
|
|
system_repo.mkdir()
|
|
workspace.mkdir()
|
|
data.mkdir()
|
|
(system_repo / "README.md").write_text("system", encoding="utf-8")
|
|
(workspace / "README.md").write_text("workspace", encoding="utf-8")
|
|
(workspace / "BIBLE.md").write_text("external bible", encoding="utf-8")
|
|
|
|
ctx = ToolContext(
|
|
repo_dir=system_repo,
|
|
drive_root=data,
|
|
workspace_root=workspace,
|
|
workspace_mode="external",
|
|
)
|
|
|
|
assert "workspace" in _repo_read(ctx, "README.md")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
commit_result = registry.execute("commit_reviewed", {"commit_message": "nope"})
|
|
assert "WORKSPACE_MODE_BLOCKED" not in commit_result
|
|
assert registry.get_schema_by_name("commit_reviewed") is not None
|
|
assert registry.get_schema_by_name("request_restart") is not None
|
|
assert "Written" in registry.execute("write_file", {"path": "BIBLE.md", "content": "external edit"})
|
|
assert (workspace / "BIBLE.md").read_text(encoding="utf-8") == "external edit"
|
|
replaced = registry.execute(
|
|
"edit_text",
|
|
{"path": "README.md", "old_str": "workspace", "new_str": "workspace edited"},
|
|
)
|
|
assert "Replaced" in replaced
|
|
assert (workspace / "README.md").read_text(encoding="utf-8") == "workspace edited"
|
|
|
|
|
|
def test_workspace_run_shell_cwd_allows_scratch_and_explicit_system(tmp_path, monkeypatch):
|
|
"""External-workspace tasks may run from host scratch (a sibling checkout, a
|
|
/tmp tree) and explicitly select the system repo; generic runtime data stays
|
|
off-limits and system-repo mutation remains independently governed."""
|
|
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
|
|
# Pin $HOME outside tmp_path so the host-scratch cwd allowance holds on Windows
|
|
# CI too (where pytest's tmp dir lives UNDER home and the data-parent-under-home
|
|
# protection would otherwise block the sibling scratch cwd). See the same fixture
|
|
# in test_external_workspace_access.py.
|
|
fake_home = tmp_path / "_home"
|
|
fake_home.mkdir()
|
|
monkeypatch.setattr(pathlib.Path, "home", lambda: fake_home)
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
outside = tmp_path / "outside"
|
|
data = tmp_path / "data"
|
|
for path in (system_repo, workspace, outside, data):
|
|
path.mkdir()
|
|
ctx = ToolContext(
|
|
repo_dir=system_repo,
|
|
drive_root=data,
|
|
workspace_root=workspace,
|
|
workspace_mode="external",
|
|
)
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
# Host scratch outside the declared workspace is now a legitimate cwd...
|
|
scratch_cwd = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(outside)})
|
|
assert "SHELL_CWD_BLOCKED" not in scratch_cwd
|
|
# The approved root contract makes system_repo an explicit cwd; generic
|
|
# runtime_data remains unavailable to process tools.
|
|
runtime_repo_cwd = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(system_repo)})
|
|
assert "SHELL_CWD_BLOCKED" not in runtime_repo_cwd
|
|
assert f"cwd={system_repo.resolve()}" in runtime_repo_cwd
|
|
runtime_data_cwd = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(data)})
|
|
assert "SHELL_CWD_BLOCKED" in runtime_data_cwd
|
|
# READ-ONLY git at a runtime target is ALLOWED (owner contract "read-only
|
|
# everywhere"; the f14baf8f false-block class). Only MUTATING git is target-checked.
|
|
git_read = registry._run_shell_safety_check(
|
|
{"cmd": ["git", "-C", str(system_repo), "status"]}, "advanced"
|
|
)
|
|
assert git_read is None, git_read
|
|
git_escape = registry._run_shell_safety_check(
|
|
{"cmd": ["git", "-C", str(system_repo), "commit", "-m", "x"]}, "advanced"
|
|
)
|
|
assert git_escape and "WORKSPACE_GIT_BLOCKED" in git_escape
|
|
git_chain = registry.execute("run_command", {"cmd": ["sh", "-c", "true && git --version; echo git binary OK"]})
|
|
assert "WORKSPACE_GIT_BLOCKED" not in git_chain
|
|
outside_write = registry.execute("run_command", {"cmd": ["touch", str(system_repo / "README.md")]})
|
|
assert "WORKSPACE_SHELL_BLOCKED" in outside_write
|
|
embedded_outside_write = registry.execute(
|
|
"run_command",
|
|
{"cmd": ["python", "-c", "open('/tmp/ouroboros-outside.txt','w').write('x')"]},
|
|
)
|
|
assert "WORKSPACE_SHELL_BLOCKED" in embedded_outside_write
|
|
|
|
|
|
def test_workspace_shell_safe_stdio_redirects_are_not_write_like(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
outside = tmp_path / "outside"
|
|
data = tmp_path / "data"
|
|
for path in (system_repo, workspace, outside, data):
|
|
path.mkdir()
|
|
(outside / "visible.txt").write_text("ok\n", encoding="utf-8")
|
|
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
stderr_sink = registry.execute("run_command", {"cmd": f"find {outside} -maxdepth 1 2>/dev/null"})
|
|
fd_dup = registry.execute("run_command", {"cmd": f"ls {outside} 2>&1 | head -n 1"})
|
|
fd_close = registry.execute("run_command", {"cmd": f"find {outside} -maxdepth 1 2>&-"})
|
|
real_redirect = registry.execute("run_command", {"cmd": f"echo x > {outside / 'out.txt'}"})
|
|
|
|
assert "WORKSPACE_SHELL_BLOCKED" not in stderr_sink, stderr_sink
|
|
assert "WORKSPACE_SHELL_BLOCKED" not in fd_dup, fd_dup
|
|
assert "WORKSPACE_SHELL_BLOCKED" not in fd_close, fd_close
|
|
assert "WORKSPACE_SHELL_BLOCKED" in real_redirect
|
|
|
|
|
|
def test_workspace_shell_blocks_windows_absolute_redirects_before_shell_execution(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
data = tmp_path / "data"
|
|
for path in (system_repo, workspace, data):
|
|
path.mkdir()
|
|
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
drive_redirect = registry.execute("run_command", {"cmd": r"echo x > C:\ouroboros-outside\out.txt"})
|
|
unc_redirect = registry.execute("run_command", {"cmd": r"echo x > \\server\share\out.txt"})
|
|
|
|
assert "WORKSPACE_SHELL_BLOCKED" in drive_redirect
|
|
assert "SHELL_SYNTAX_UNSUPPORTED" not in drive_redirect
|
|
assert "WORKSPACE_SHELL_BLOCKED" in unc_redirect
|
|
assert "SHELL_SYNTAX_UNSUPPORTED" not in unc_redirect
|
|
|
|
|
|
def test_workspace_shell_keeps_symlinked_workspace_absolute_paths_allowed(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
|
|
system_repo = tmp_path / "system"
|
|
real_workspace = tmp_path / "real_workspace"
|
|
workspace_link = tmp_path / "workspace_link"
|
|
data = tmp_path / "data"
|
|
for path in (system_repo, real_workspace, data):
|
|
path.mkdir()
|
|
try:
|
|
workspace_link.symlink_to(real_workspace, target_is_directory=True)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlink unavailable on this platform: {exc}")
|
|
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace_link, workspace_mode="external")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
target = workspace_link / "inside.txt"
|
|
result = registry.execute("run_command", {"cmd": [sys.executable, "-c", f"open({str(target)!r}, 'w').write('ok')"]})
|
|
|
|
assert "WORKSPACE_SHELL_BLOCKED" not in result, result
|
|
assert (real_workspace / "inside.txt").exists()
|
|
|
|
|
|
def test_workspace_shell_blocks_nested_symlink_escape_absolute_path(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
outside = tmp_path / "outside"
|
|
data = tmp_path / "data"
|
|
for path in (system_repo, workspace, outside, data):
|
|
path.mkdir()
|
|
outlink = workspace / "outlink"
|
|
outside_file = outside / "target.txt"
|
|
outside_file.write_text("old\n", encoding="utf-8")
|
|
filelink = workspace / "filelink"
|
|
executable_name_link = workspace / "touch"
|
|
try:
|
|
outlink.symlink_to(outside, target_is_directory=True)
|
|
filelink.symlink_to(outside_file)
|
|
executable_name_link.symlink_to(outside_file)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlink unavailable on this platform: {exc}")
|
|
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
result = registry.execute("run_command", {"cmd": f"touch {outlink / 'escaped.txt'}"})
|
|
relative_result = registry.execute("run_command", {"cmd": "touch outlink/escaped-relative.txt"})
|
|
bare_result = registry.execute("run_command", {"cmd": "touch outlink"})
|
|
executable_name_result = registry.execute("run_command", {"cmd": ["touch", "touch"]})
|
|
redirect_result = registry.execute("run_command", {"cmd": "echo changed > filelink"})
|
|
compact_redirect_result = registry.execute("run_command", {"cmd": "echo changed >filelink"})
|
|
shell_inline_result = registry.execute("run_command", {"cmd": ["sh", "-c", "echo changed > filelink"]})
|
|
shell_inline_touch_result = registry.execute("run_command", {"cmd": ["sh", "-c", "touch filelink"]})
|
|
bash_redirect_result = registry.execute("run_command", {"cmd": ["bash", "-c", "echo changed &> filelink"]})
|
|
compact_bash_redirect_result = registry.execute("run_command", {"cmd": ["bash", "-c", "echo changed &>filelink"]})
|
|
tee_result = registry.execute("run_command", {"cmd": "printf changed | tee filelink"})
|
|
shell_inline_tee_result = registry.execute("run_command", {"cmd": ["sh", "-c", "printf changed | tee filelink"]})
|
|
python_inline_result = registry.execute(
|
|
"run_command",
|
|
{"cmd": [sys.executable, "-c", "open('filelink', 'w').write('changed')"]},
|
|
)
|
|
python_versioned_result = registry.execute(
|
|
"run_command",
|
|
{"cmd": ["python3.12", "-c", "open('filelink', 'w').write('changed')"]},
|
|
)
|
|
node_script_result = registry.execute(
|
|
"run_script",
|
|
{
|
|
"interpreter": "node",
|
|
"script": "require('fs').writeFileSync('filelink', 'changed')",
|
|
},
|
|
)
|
|
|
|
assert "WORKSPACE_SHELL_BLOCKED" in result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in relative_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in bare_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in executable_name_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in redirect_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in compact_redirect_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in shell_inline_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in shell_inline_touch_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in bash_redirect_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in compact_bash_redirect_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in tee_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in shell_inline_tee_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in python_inline_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in python_versioned_result
|
|
assert "WORKSPACE_SHELL_BLOCKED" in node_script_result
|
|
assert not (outside / "escaped.txt").exists()
|
|
assert not (outside / "escaped-relative.txt").exists()
|
|
assert outside_file.read_text(encoding="utf-8") == "old\n"
|
|
|
|
|
|
def test_external_workspace_shell_allows_task_local_git(tmp_path, monkeypatch):
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
data = tmp_path / "data"
|
|
system_repo.mkdir()
|
|
data.mkdir()
|
|
_init_repo_with_file(workspace)
|
|
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
monkeypatch.setenv("OUROBOROS_TEST_RUNTIME_REPO", str(system_repo))
|
|
|
|
allowed = [
|
|
["git", "for-each-ref", "--format=%(refname)"],
|
|
["git", "rev-list", "--count", "HEAD"],
|
|
["git", "show-ref", "--heads"],
|
|
["git", "branch", "--show-current"],
|
|
["git", "branch", "--list"],
|
|
["git", "branch", "--list", "ma*"],
|
|
["git", "branch", "-av"],
|
|
["git", "tag", "-l"],
|
|
["git", "tag", "--list", "v*"],
|
|
["git", "branch", "new-branch"],
|
|
["git", "branch", "-v", "new-branch"],
|
|
["git", "branch", "--verbose", "new-branch"],
|
|
["git", "branch", "-d", "main"],
|
|
["git", "tag", "v1"],
|
|
["git", "tag", "-a", "v1", "-m", "x"],
|
|
["git", "commit", "--allow-empty", "-m", "task-local commit"],
|
|
["sh", "-c", "git --version; echo git binary OK"],
|
|
]
|
|
|
|
for cmd in allowed:
|
|
assert registry._run_shell_safety_check({"cmd": cmd}, "advanced") is None, cmd
|
|
|
|
# READ-ONLY git reaches the runtime through EVERY retarget vector — that is the
|
|
# owner contract ("read-only everywhere, including at a runtime target") and the
|
|
# recorded false-block class f14baf8f. Before the Q4=A composition these four
|
|
# were refused: the target-aware resolver let them through and the
|
|
# external-workspace runtime-READ guard then blocked them as
|
|
# WORKSPACE_SHELL_BLOCKED, naming the wrong reason.
|
|
for cmd in (
|
|
["git", "-C", str(system_repo), "status"],
|
|
["git", "--git-dir", str(system_repo / ".git"), "status"],
|
|
# as_posix(): a POSIX shell (sh -c) uses forward slashes; a Windows
|
|
# backslash literal would be eaten as shell escapes during parsing.
|
|
["sh", "-c", f"cd {system_repo.as_posix()} && git status"],
|
|
["sh", "-c", "git -C $OUROBOROS_TEST_RUNTIME_REPO status"],
|
|
):
|
|
result = registry._run_shell_safety_check({"cmd": cmd}, "advanced")
|
|
assert result is None, (cmd, result)
|
|
|
|
# ...while the MUTATING form of each vector stays blocked.
|
|
for cmd in (
|
|
["git", "-C", str(system_repo), "commit", "-m", "x"],
|
|
["git", "--git-dir", str(system_repo / ".git"), "commit", "-m", "x"],
|
|
["sh", "-c", f"cd {system_repo.as_posix()} && git commit -m x"],
|
|
["sh", "-c", "git -C $OUROBOROS_TEST_RUNTIME_REPO commit -m x"],
|
|
):
|
|
result = registry._run_shell_safety_check({"cmd": cmd}, "advanced")
|
|
assert result and "WORKSPACE_GIT_BLOCKED" in result, (cmd, result)
|
|
|
|
# The read-only exemption is ALL-or-NOTHING per segment: a compound that only
|
|
# STARTS with git still meets the runtime/secret read guard in full.
|
|
mixed = registry._run_shell_safety_check(
|
|
{"cmd": ["sh", "-c", f"git status && cat {(data / 'settings.json').as_posix()}"]},
|
|
"advanced",
|
|
)
|
|
assert mixed and "WORKSPACE_SHELL_BLOCKED" in mixed, mixed
|
|
|
|
|
|
def test_workspace_shell_git_ls_remote_requires_network_contract(tmp_path):
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
data = tmp_path / "data"
|
|
system_repo.mkdir()
|
|
data.mkdir()
|
|
_init_repo_with_file(workspace)
|
|
contract = {
|
|
"allowed_resources": {"network": False},
|
|
"resource_policy": {},
|
|
}
|
|
ctx = ToolContext(
|
|
repo_dir=system_repo,
|
|
drive_root=data,
|
|
workspace_root=workspace,
|
|
workspace_mode="external",
|
|
task_contract=contract,
|
|
task_metadata={"task_contract": contract},
|
|
)
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
for cmd in (
|
|
["git", "ls-remote", "origin"],
|
|
["git", "submodule", "update", "--init", "--recursive"],
|
|
):
|
|
result = registry._run_shell_safety_check({"cmd": cmd}, "advanced")
|
|
assert result and "RESOURCE_CONSTRAINT_BLOCKED" in result, (cmd, result)
|
|
|
|
|
|
def test_workspace_run_shell_allows_absolute_cwd_under_workspace_and_child_drive(tmp_path):
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
parent_data = tmp_path / "data"
|
|
parent_task_dir = parent_data / "task_drives" / "task-workspace" / "scratch"
|
|
child_drive = tmp_path / "child-data"
|
|
child_dir = child_drive / "task_drives" / "task-workspace" / "scratch"
|
|
child_control_dir = child_drive / "memory"
|
|
for path in (system_repo, workspace, parent_data / "logs", parent_task_dir, child_dir, child_control_dir):
|
|
path.mkdir(parents=True)
|
|
ctx = ToolContext(
|
|
repo_dir=system_repo,
|
|
drive_root=parent_data,
|
|
workspace_root=workspace,
|
|
workspace_mode="external",
|
|
task_id="task-workspace",
|
|
task_metadata={"drive_root": str(child_drive), "budget_drive_root": str(parent_data)},
|
|
)
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=parent_data)
|
|
registry.set_context(ctx)
|
|
|
|
def assert_python_cwd(path):
|
|
output = registry.execute(
|
|
"run_command",
|
|
{"cmd": [sys.executable, "-c", "import os; print(os.getcwd())"], "cwd": str(path)},
|
|
)
|
|
assert "exit_code=0" in output
|
|
cwd_output = output.rsplit("STDOUT:\n", 1)[-1].strip()
|
|
assert pathlib.Path(cwd_output).resolve() == path.resolve()
|
|
|
|
assert_python_cwd(workspace)
|
|
assert_python_cwd(child_dir)
|
|
child_control = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(child_control_dir)})
|
|
assert "SHELL_CWD_BLOCKED" in child_control
|
|
blocked = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(parent_data / "logs")})
|
|
assert "SHELL_CWD_BLOCKED" in blocked
|
|
# Read-only git is allowed everywhere now; the escape check uses a MUTATING form.
|
|
git_read = registry._run_shell_safety_check(
|
|
{"cmd": ["git", "-C", "../other-repo", "status"], "cwd": str(child_dir)},
|
|
"advanced",
|
|
)
|
|
assert git_read is None, git_read
|
|
git_escape = registry._run_shell_safety_check(
|
|
{"cmd": ["git", "-C", "..", "commit", "-m", "x"], "cwd": str(child_dir)},
|
|
"advanced",
|
|
)
|
|
assert git_escape and "WORKSPACE_GIT_BLOCKED" in git_escape, git_escape
|
|
protected_escape = registry._run_shell_safety_check(
|
|
{"cmd": ["touch", "../data/state/state.json"]},
|
|
"pro",
|
|
)
|
|
assert "WORKSPACE_SHELL_BLOCKED" in protected_escape
|
|
task_drive_write = registry.execute("run_command", {"cmd": ["touch", "output.txt"], "cwd": str(child_dir)})
|
|
assert "WORKSPACE_SHELL_BLOCKED" not in task_drive_write
|
|
assert (child_dir / "output.txt").is_file()
|
|
parent_task_drive_write = registry.execute("run_command", {"cmd": ["touch", "output.txt"], "cwd": str(parent_task_dir)})
|
|
assert "WORKSPACE_SHELL_BLOCKED" not in parent_task_drive_write
|
|
assert (parent_task_dir / "output.txt").is_file()
|
|
absolute_task_drive_file = parent_task_dir / "absolute-python.txt"
|
|
absolute_task_drive_write = registry.execute(
|
|
"run_command",
|
|
{"cmd": [sys.executable, "-c", f"open({str(absolute_task_drive_file)!r}, 'w').write('ok')"]},
|
|
)
|
|
assert "WORKSPACE_SHELL_BLOCKED" not in absolute_task_drive_write
|
|
assert absolute_task_drive_file.read_text(encoding="utf-8") == "ok"
|
|
|
|
|
|
def test_workspace_shell_allows_nested_relative_write_paths(tmp_path):
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
data = tmp_path / "data"
|
|
for path in (system_repo, workspace, data):
|
|
path.mkdir(parents=True)
|
|
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
assert registry._run_shell_safety_check({"cmd": ["touch", "subdir/file.txt"]}, "advanced") is None
|
|
assert registry._run_shell_safety_check({"cmd": ["mkdir", "-p", "build/output"]}, "advanced") is None
|
|
python_write = {"cmd": [sys.executable, "-c", "open('subdir/python.txt', 'w').write('ok')"]}
|
|
assert registry._run_shell_safety_check(python_write, "advanced") is None
|
|
|
|
|
|
def test_workspace_shell_sudo_and_pro_passthrough_policy(tmp_path):
|
|
system_repo = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
data = tmp_path / "data"
|
|
for path in (system_repo, workspace, data):
|
|
path.mkdir()
|
|
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
|
|
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
|
|
registry.set_context(ctx)
|
|
|
|
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudo", "true"]}, "pro")
|
|
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sh", "-c", "sudo true"]}, "pro")
|
|
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudo", "-S", "true"]}, "pro")
|
|
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudo", "-nS", "true"]}, "pro")
|
|
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudoedit", "/etc/hosts"]}, "pro")
|
|
assert registry._run_shell_safety_check({"cmd": ["sudo", "-n", "python", "-S", "-c", "print(1)"]}, "pro") is None
|
|
assert "SAFETY_VIOLATION" in registry._run_shell_safety_check({"cmd": ["sh", "-c", "gh\nrepo\ncreate x"]}, "pro")
|
|
assert "SAFETY_VIOLATION" in registry._run_shell_safety_check({"cmd": ["sh", "-c", "gh\nauth\nlogin"]}, "pro")
|
|
outside_write = {"cmd": ["python", "-c", "open('/tmp/ouroboros-pro.txt','w').write('x')"]}
|
|
assert "WORKSPACE_SHELL_BLOCKED" in registry._run_shell_safety_check(outside_write, "advanced")
|
|
assert registry._run_shell_safety_check(outside_write, "pro") is None
|
|
|
|
|
|
def test_workspace_preflight_infers_binaries_from_script_commands():
|
|
tools = _infer_tools_from_manifests([
|
|
{
|
|
"type": "node",
|
|
"scripts": ["test"],
|
|
"script_commands": {"test": "vitest --run"},
|
|
}
|
|
])
|
|
assert "vitest" in tools
|
|
assert "test" not in tools
|
|
noisy = _infer_tools_from_manifests([
|
|
{
|
|
"type": "node",
|
|
"scripts": ["build"],
|
|
"script_commands": {"build": "NODE_ENV=production cd web && vite build"},
|
|
}
|
|
])
|
|
assert "NODE_ENV=production" not in noisy
|
|
assert "cd" not in noisy
|
|
assert "vite" in noisy
|
|
|
|
|
|
def test_workspace_patch_includes_tracked_and_untracked_files(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
(repo / "tracked.txt").write_text("old\n", encoding="utf-8")
|
|
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
|
|
(repo / "new.txt").write_text("hello\n", encoding="utf-8")
|
|
|
|
patch = build_workspace_patch(repo)
|
|
|
|
assert "diff --git a/tracked.txt b/tracked.txt" in patch
|
|
assert "+new" in patch
|
|
assert "diff --git" in patch and "new.txt" in patch
|
|
|
|
|
|
def test_workspace_patch_lockfile_without_manifest_is_incidental_only_with_code_changes():
|
|
assert _incidental_lockfile_excludes(["package-lock.json"]) == set()
|
|
assert _incidental_lockfile_excludes(["package-lock.json", "package.json", "app.js"]) == set()
|
|
assert _incidental_lockfile_excludes(["package-lock.json", "app.js"]) == {"package-lock.json"}
|
|
assert _incidental_lockfile_excludes(["pkg/poetry.lock", "pkg/module.py"]) == {"pkg/poetry.lock"}
|
|
|
|
|
|
def test_workspace_patch_preserves_lockfile_when_other_changes_are_junk(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
(repo / "README.md").write_text("base\n", encoding="utf-8")
|
|
subprocess.run(["git", "add", "README.md"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
(repo / "package-lock.json").write_text('{"lockfileVersion": 3}\n', encoding="utf-8")
|
|
(repo / "dist").mkdir()
|
|
(repo / "dist" / "out.txt").write_text("junk\n", encoding="utf-8")
|
|
|
|
_artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
patch = (tmp_path / "artifacts" / "workspace.patch").read_text(encoding="utf-8")
|
|
|
|
assert "package-lock.json" in patch
|
|
assert "dist/out.txt" not in patch
|
|
assert manifest["counts"]["untracked_included"] == 1
|
|
assert manifest["counts"]["untracked_excluded"] == 1
|
|
|
|
|
|
def test_workspace_patch_excludes_binary_junk_and_oversize(tmp_path, monkeypatch):
|
|
"""T7 (v6.35.0): the real-usage workspace patch drops untracked build/runtime
|
|
binaries, junk artifacts, and oversize blobs (recorded, not silently lost),
|
|
while keeping real source additions."""
|
|
import ouroboros.headless as headless
|
|
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
(repo / "seed.txt").write_text("seed\n", encoding="utf-8")
|
|
subprocess.run(["git", "add", "seed.txt"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
|
|
cwd=repo, check=True, capture_output=True,
|
|
)
|
|
# Untracked additions: a real source file (keep), a compiled binary (drop),
|
|
# a redis dump + log junk (drop), and an oversize text file (drop).
|
|
(repo / "fix.py").write_text("def fixed():\n return 1\n", encoding="utf-8")
|
|
(repo / "app").write_bytes(b"\x7fELF\x00\x01\x02\x03binary\x00blob") # compiled binary
|
|
(repo / "dump.rdb").write_bytes(b"REDIS\x00\x01")
|
|
(repo / "run.log").write_text("noise\n", encoding="utf-8")
|
|
(repo / "htmlcov").mkdir()
|
|
(repo / "htmlcov" / "index.html").write_text("<html></html>\n", encoding="utf-8") # top-level coverage junk
|
|
monkeypatch.setattr(headless, "_PATCH_MAX_UNTRACKED_FILE_BYTES", 100)
|
|
(repo / "big.txt").write_text("x" * 200, encoding="utf-8") # 200 bytes > cap; small files pass size
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["exclude_rules_version"] == 2
|
|
excluded = {item["path"]: item["reason"] for item in manifest["untracked_excluded"]}
|
|
assert "binary file" in excluded.get("app", "")
|
|
assert "binary file" in excluded.get("dump.rdb", "") or "junk artifact" in excluded.get("dump.rdb", "")
|
|
assert "junk artifact" in excluded.get("run.log", "")
|
|
assert "junk artifact" in excluded.get("htmlcov/index.html", "") # top-level htmlcov excluded
|
|
assert "size cap" in excluded.get("big.txt", "")
|
|
assert "fix.py" in manifest["untracked_included"]
|
|
patch = (tmp_path / "artifacts" / "workspace.patch").read_text(encoding="utf-8")
|
|
assert "fix.py" in patch
|
|
assert "diff --git a/app b/app" not in patch
|
|
assert "dump.rdb" not in patch
|
|
assert "run.log" not in patch
|
|
assert "big.txt" not in patch
|
|
|
|
|
|
def test_workspace_patch_supports_unborn_git_worktree(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
(repo / "created.txt").write_text("hello\n", encoding="utf-8")
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
assert manifest["base_is_empty_tree"] is True
|
|
assert manifest["base_head"] == "(unborn)"
|
|
assert manifest["current_head"] == "(unborn)"
|
|
assert any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
patch = (tmp_path / "artifacts" / "workspace.patch").read_text(encoding="utf-8")
|
|
assert "created.txt" in patch
|
|
assert "+hello" in patch
|
|
head = subprocess.run(["git", "rev-parse", "--verify", "HEAD"], cwd=repo, capture_output=True)
|
|
assert head.returncode != 0
|
|
|
|
|
|
def test_workspace_patch_supports_unborn_sha256_git_worktree(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
init = subprocess.run(["git", "init", "--object-format=sha256"], cwd=repo, capture_output=True)
|
|
if init.returncode != 0:
|
|
pytest.skip("git does not support sha256 object-format")
|
|
(repo / "created.txt").write_text("hello\n", encoding="utf-8")
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
assert manifest["base_is_empty_tree"] is True
|
|
assert len(manifest["base_ref"]) == 64
|
|
assert any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_allows_external_workspace_first_commit(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
(repo / "created.txt").write_text("hello\n", encoding="utf-8")
|
|
subprocess.run(["git", "add", "created.txt"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "first"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
task = {"metadata": {"workspace_preflight": {"git": {"head": ""}}}}
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task=task)
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
assert manifest["errors"] == []
|
|
assert any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_fails_on_invalid_head_not_unborn(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
head_ref = subprocess.run(["git", "symbolic-ref", "--quiet", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
|
|
ref_path = repo / ".git" / head_ref
|
|
ref_path.unlink()
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_FAILED
|
|
assert any(error["type"] == "git_invalid_head" for error in manifest["errors"])
|
|
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_manifest_excludes_env_cache_dirs(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
(repo / "new.txt").write_text("hello\n", encoding="utf-8")
|
|
(repo / "node_modules" / "pkg").mkdir(parents=True)
|
|
(repo / "node_modules" / "pkg" / "index.js").write_text("generated\n", encoding="utf-8")
|
|
artifact_dir = tmp_path / "artifacts"
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, artifact_dir, task={})
|
|
|
|
assert manifest["status"] == "ready_with_changes"
|
|
assert "new.txt" in (artifact_dir / "workspace.patch").read_text(encoding="utf-8")
|
|
assert "node_modules" not in (artifact_dir / "workspace.patch").read_text(encoding="utf-8")
|
|
assert manifest["counts"]["untracked_excluded"] == 1
|
|
assert any(item["kind"] == "workspace_patch_manifest" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_fails_on_sensitive_untracked_file(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
(repo / ".npmrc").write_text("//registry.npmjs.org/:_authToken=secret\n", encoding="utf-8")
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_FAILED
|
|
assert manifest["errors"][0]["type"] == "sensitive_untracked_files"
|
|
assert manifest["sensitive_blocked"][0]["path"] == ".npmrc"
|
|
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_fails_on_sensitive_untracked_file_inside_excluded_dir(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
secret = repo / "node_modules" / "pkg" / "service-account.json"
|
|
secret.parent.mkdir(parents=True)
|
|
secret.write_text("TOKEN=secret\n", encoding="utf-8")
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_FAILED
|
|
assert manifest["counts"]["sensitive_blocked"] == 1
|
|
assert manifest["sensitive_blocked"][0]["path"] == "node_modules/pkg/service-account.json"
|
|
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_fails_on_common_credential_paths(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
(repo / "credentials").write_text("secret\n", encoding="utf-8")
|
|
(repo / "prod.env").write_text("SECRET=1\n", encoding="utf-8")
|
|
(repo / "settings.env.local").write_text("SECRET=1\n", encoding="utf-8")
|
|
(repo / ".aws").mkdir()
|
|
(repo / ".aws" / "credentials").write_text("secret\n", encoding="utf-8")
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_FAILED
|
|
assert {item["path"] for item in manifest["sensitive_blocked"]} == {
|
|
"credentials",
|
|
"prod.env",
|
|
"settings.env.local",
|
|
".aws/credentials",
|
|
}
|
|
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_allows_benign_tokenizer_json(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
(repo / "tokenizer.json").write_text("{}\n", encoding="utf-8")
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
assert manifest["sensitive_blocked"] == []
|
|
assert any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_failed_refinalization_drops_stale_workspace_patch_metadata(tmp_path):
|
|
parent = tmp_path / "data"
|
|
repo = tmp_path / "repo"
|
|
parent.mkdir()
|
|
_init_repo_with_file(repo)
|
|
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
|
|
task = {"id": "task-stale", "workspace_root": str(repo)}
|
|
write_task_result(parent, "task-stale", "completed", workspace_root=str(repo), artifact_status="finalizing")
|
|
finalize_task_artifacts(parent, task)
|
|
result = json.loads((parent / "task_results" / "task-stale.json").read_text(encoding="utf-8"))
|
|
assert any(item.get("kind") == "workspace_patch" for item in result["artifacts"])
|
|
|
|
(repo / ".env").write_text("TOKEN=secret\n", encoding="utf-8")
|
|
finalize_task_artifacts(parent, task)
|
|
|
|
result = json.loads((parent / "task_results" / "task-stale.json").read_text(encoding="utf-8"))
|
|
assert result["artifact_status"] == ARTIFACT_STATUS_FAILED
|
|
assert not any(item.get("kind") == "workspace_patch" for item in result["artifacts"])
|
|
|
|
|
|
def test_workspace_patch_preserves_untracked_paths_with_whitespace(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
leading = repo / " leading.txt"
|
|
nested = repo / "dir with space" / "file name.txt"
|
|
leading.write_text("leading\n", encoding="utf-8")
|
|
nested.parent.mkdir()
|
|
nested.write_text("nested\n", encoding="utf-8")
|
|
|
|
_artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
|
|
|
|
assert manifest["status"] == "ready_with_changes"
|
|
assert " leading.txt" in manifest["untracked_included"]
|
|
assert "dir with space/file name.txt" in manifest["untracked_included"]
|
|
assert manifest["patch_size"] > 0
|
|
|
|
|
|
def test_finalize_workspace_patch_allows_external_workspace_head_changed(tmp_path):
|
|
parent = tmp_path / "data"
|
|
repo = tmp_path / "repo"
|
|
parent.mkdir()
|
|
_init_repo_with_file(repo)
|
|
old_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
|
|
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
|
|
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "move"], cwd=repo, check=True, capture_output=True)
|
|
task = {
|
|
"id": "task-head",
|
|
"workspace_root": str(repo),
|
|
"metadata": {"workspace_preflight": {"git": {"head": old_head}}},
|
|
}
|
|
write_task_result(parent, "task-head", "completed", workspace_root=str(repo), artifact_status="finalizing")
|
|
|
|
finalize_task_artifacts(parent, task)
|
|
|
|
result = json.loads((parent / "task_results" / "task-head.json").read_text(encoding="utf-8"))
|
|
assert result["artifact_status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
manifest = json.loads((task_artifacts_dir(parent, "task-head") / "workspace_patch.json").read_text(encoding="utf-8"))
|
|
assert manifest["errors"] == []
|
|
|
|
|
|
def test_finalize_workspace_patch_exception_manifest_keeps_base_fields(tmp_path, monkeypatch):
|
|
import ouroboros.headless as headless
|
|
|
|
parent = tmp_path / "data"
|
|
repo = tmp_path / "repo"
|
|
parent.mkdir()
|
|
_init_repo_with_file(repo)
|
|
task = {"id": "task-exception", "workspace_root": str(repo)}
|
|
write_task_result(parent, "task-exception", "completed", workspace_root=str(repo), artifact_status="finalizing")
|
|
|
|
def boom(*_args, **_kwargs):
|
|
raise RuntimeError("artifact failure")
|
|
|
|
monkeypatch.setattr(headless, "write_workspace_patch_artifacts", boom)
|
|
headless.finalize_task_artifacts(parent, task)
|
|
|
|
result = json.loads((parent / "task_results" / "task-exception.json").read_text(encoding="utf-8"))
|
|
manifest = json.loads((task_artifacts_dir(parent, "task-exception") / "workspace_patch.json").read_text(encoding="utf-8"))
|
|
assert result["artifact_status"] == ARTIFACT_STATUS_FAILED
|
|
assert manifest["status"] == ARTIFACT_STATUS_FAILED
|
|
assert manifest["base_ref"] == ""
|
|
assert manifest["base_head"] == ""
|
|
assert manifest["base_is_empty_tree"] is False
|
|
assert manifest["current_head"] == ""
|
|
|
|
|
|
def test_workspace_patch_uses_acting_base_sha_without_preflight_metadata(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
|
|
(repo / "tracked.txt").write_text("acting edit\n", encoding="utf-8")
|
|
task = {
|
|
"task_constraint": {
|
|
"mode": "acting_subagent",
|
|
"surface": "self_worktree",
|
|
"base_sha": base_head,
|
|
},
|
|
}
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task=task)
|
|
|
|
assert manifest["status"] == "ready_with_changes"
|
|
assert manifest["base_ref"] == base_head
|
|
assert manifest["base_head"] == base_head
|
|
assert manifest["current_head"] == base_head
|
|
assert any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_workspace_patch_fails_when_acting_base_sha_head_changed(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
_init_repo_with_file(repo)
|
|
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
|
|
(repo / "tracked.txt").write_text("committed by child\n", encoding="utf-8")
|
|
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "child commit"], cwd=repo, check=True, capture_output=True)
|
|
moved_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
|
|
task = {
|
|
"task_constraint": {
|
|
"mode": "acting_subagent",
|
|
"surface": "self_worktree",
|
|
"base_sha": base_head,
|
|
},
|
|
}
|
|
|
|
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task=task)
|
|
|
|
assert manifest["status"] == ARTIFACT_STATUS_FAILED
|
|
assert manifest["base_ref"] == base_head
|
|
assert manifest["errors"][-1]["type"] == "workspace_head_changed"
|
|
assert manifest["errors"][-1]["expected_head"] == base_head
|
|
assert manifest["errors"][-1]["current_head"] == moved_head
|
|
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
|
|
|
|
|
|
def test_copy_child_result_cannot_overwrite_finalized_accounting(tmp_path):
|
|
"""F2: once the root's terminal checkpoint has finalized accounting
|
|
(task_cost_finalized rides the same write as post_task_synthesis), a late
|
|
headless-mirror copy-back may still enrich the result but the parent-owned
|
|
cost/round/token fields stay finalized (the saga displayed the $66 root-only
|
|
mirror cost instead of the $128 finalized subtree total)."""
|
|
from ouroboros.headless import copy_child_task_result
|
|
from ouroboros.task_results import STATUS_COMPLETED
|
|
|
|
parent = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
parent.mkdir()
|
|
child.mkdir()
|
|
task_id = "costfinal"
|
|
write_task_result(
|
|
parent, task_id, STATUS_COMPLETED,
|
|
result="root done",
|
|
root_phase_checkpoint={"post_task_synthesis": "completed"},
|
|
cost_usd=127.97, cost_final=True,
|
|
cost_usd_with_children=127.97, cost_with_children_partial=False,
|
|
total_rounds=200, prompt_tokens=1000, completion_tokens=500,
|
|
)
|
|
write_task_result(
|
|
child, task_id, STATUS_COMPLETED,
|
|
result="mirror done",
|
|
cost_usd=66.30, cost_final=True,
|
|
cost_usd_with_children=66.30, cost_with_children_partial=True,
|
|
total_rounds=150, prompt_tokens=700, completion_tokens=300,
|
|
mirror_only_fact="from-child",
|
|
)
|
|
|
|
merged = copy_child_task_result(parent, {"id": task_id, "drive_root": str(child)})
|
|
|
|
assert merged is not None
|
|
assert merged["cost_usd"] == 127.97
|
|
assert merged["cost_usd_with_children"] == 127.97
|
|
assert merged["cost_with_children_partial"] is False
|
|
assert merged["total_rounds"] == 200
|
|
assert merged["prompt_tokens"] == 1000
|
|
assert merged["completion_tokens"] == 500
|
|
# Non-accounting enrichment from the child mirror still lands.
|
|
assert merged["mirror_only_fact"] == "from-child"
|
|
assert merged["result"] == "mirror done"
|
|
assert merged["root_phase_checkpoint"]["post_task_synthesis"] == "completed"
|
|
|
|
|
|
def test_copy_child_result_merges_cost_before_finalization(tmp_path):
|
|
"""Before the terminal checkpoint finalizes accounting, the child mirror's
|
|
cost projection is still the freshest fact and must keep flowing."""
|
|
from ouroboros.headless import copy_child_task_result
|
|
from ouroboros.task_results import STATUS_COMPLETED
|
|
|
|
parent = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
parent.mkdir()
|
|
child.mkdir()
|
|
task_id = "costlive"
|
|
write_task_result(parent, task_id, STATUS_COMPLETED, result="root running")
|
|
write_task_result(
|
|
child, task_id, STATUS_COMPLETED,
|
|
result="mirror done", cost_usd=12.5, total_rounds=42,
|
|
)
|
|
|
|
merged = copy_child_task_result(parent, {"id": task_id, "drive_root": str(child)})
|
|
|
|
assert merged is not None
|
|
assert merged["cost_usd"] == 12.5
|
|
assert merged["total_rounds"] == 42
|
|
|
|
|
|
def test_effective_result_preserves_workspace_artifact_status_with_child_drive(tmp_path):
|
|
from ouroboros.headless import copy_child_task_result
|
|
from ouroboros.task_results import STATUS_COMPLETED
|
|
from ouroboros.task_status import load_effective_task_result
|
|
|
|
parent = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
repo = tmp_path / "repo"
|
|
parent.mkdir()
|
|
child.mkdir()
|
|
_init_repo_with_file(repo)
|
|
old_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
|
|
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
|
|
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "move"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
task_id = "patchfail"
|
|
write_task_result(
|
|
child,
|
|
task_id,
|
|
STATUS_COMPLETED,
|
|
result="child done",
|
|
artifact_status=ARTIFACT_STATUS_READY,
|
|
artifact_bundle={"status": ARTIFACT_STATUS_READY, "artifacts": [], "errors": []},
|
|
ts="2026-01-01T00:00:02Z",
|
|
)
|
|
ledger_path = parent / "task_results" / "artifacts" / task_id / "verification_ledger.json"
|
|
ledger_path.parent.mkdir(parents=True)
|
|
ledger_path.write_text(
|
|
json.dumps({
|
|
"schema_version": 2,
|
|
"outcome_axes": {
|
|
"artifacts": {"status": "finalizing"},
|
|
"objective": {"status": "not_evaluated", "source": "none"},
|
|
},
|
|
"entries": [{"kind": "objective_outcome", "status": "not_evaluated"}],
|
|
}),
|
|
encoding="utf-8",
|
|
)
|
|
write_task_result(
|
|
parent,
|
|
task_id,
|
|
STATUS_COMPLETED,
|
|
result="child done",
|
|
workspace_root=str(repo),
|
|
child_drive_root=str(child),
|
|
artifact_status="finalizing",
|
|
artifacts=[{"kind": "verification_ledger", "name": "verification_ledger.json", "path": str(ledger_path)}],
|
|
child_status=STATUS_COMPLETED,
|
|
)
|
|
|
|
finalize_task_artifacts(
|
|
parent,
|
|
{
|
|
"id": task_id,
|
|
"workspace_root": str(repo),
|
|
"drive_root": str(child),
|
|
"metadata": {"workspace_preflight": {"git": {"head": old_head}}},
|
|
},
|
|
)
|
|
|
|
effective = load_effective_task_result(parent, task_id)
|
|
assert effective["artifact_status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
assert not effective.get("artifact_error")
|
|
assert effective["artifact_bundle"]["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
refreshed_ledger = json.loads(ledger_path.read_text(encoding="utf-8"))
|
|
assert refreshed_ledger["outcome_axes"]["artifacts"]["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
|
|
copied = copy_child_task_result(parent, {"id": task_id, "workspace_root": str(repo), "drive_root": str(child)})
|
|
assert copied is not None
|
|
assert copied["artifact_status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
assert not copied.get("artifact_error")
|
|
assert copied["artifact_bundle"]["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
|
|
|
|
readonly_task_id = "readonlychild"
|
|
write_task_result(
|
|
child,
|
|
readonly_task_id,
|
|
STATUS_COMPLETED,
|
|
result="readonly handoff",
|
|
workspace_root=str(repo),
|
|
workspace_mode="external",
|
|
delegation_role="subagent",
|
|
task_constraint={"mode": "local_readonly_subagent"},
|
|
)
|
|
copied_readonly = copy_child_task_result(
|
|
parent,
|
|
{
|
|
"id": readonly_task_id,
|
|
"workspace_root": str(repo),
|
|
"drive_root": str(child),
|
|
"delegation_role": "subagent",
|
|
"task_constraint": {"mode": "local_readonly_subagent"},
|
|
},
|
|
)
|
|
assert copied_readonly is not None
|
|
assert copied_readonly.get("artifact_status", "") != "finalizing"
|
|
assert "child_status" not in copied_readonly
|
|
effective_readonly = load_effective_task_result(parent, readonly_task_id)
|
|
assert effective_readonly["status"] == STATUS_COMPLETED
|
|
assert effective_readonly["workspace_root"] == str(repo)
|
|
|
|
|
|
def test_child_copyback_preserves_acceptance_verdict_and_terminal_post_task_marker(tmp_path):
|
|
from ouroboros.headless import copy_child_task_result
|
|
from ouroboros.task_results import STATUS_COMPLETED, write_task_result
|
|
|
|
parent = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
parent.mkdir()
|
|
child.mkdir()
|
|
task_id = "root-checkpoint"
|
|
write_task_result(
|
|
child,
|
|
task_id,
|
|
STATUS_COMPLETED,
|
|
root_phase_checkpoint={
|
|
"phase": "task_acceptance",
|
|
"status": "degraded",
|
|
"pass_index": 2,
|
|
"post_task_synthesis": "pending_once",
|
|
},
|
|
)
|
|
write_task_result(
|
|
parent,
|
|
task_id,
|
|
STATUS_COMPLETED,
|
|
root_phase_checkpoint={
|
|
"phase": "task_acceptance",
|
|
"status": "not_required",
|
|
"pass_index": 0,
|
|
"post_task_synthesis": "completed",
|
|
},
|
|
)
|
|
|
|
copied = copy_child_task_result(parent, {"id": task_id, "drive_root": str(child)})
|
|
|
|
assert copied is not None
|
|
assert copied["root_phase_checkpoint"] == {
|
|
"phase": "task_acceptance",
|
|
"status": "degraded",
|
|
"pass_index": 2,
|
|
"post_task_synthesis": "completed",
|
|
}
|
|
|
|
|
|
def test_finalize_task_artifacts_preserves_existing_artifact_axis_fields(tmp_path):
|
|
from ouroboros.cli import _is_terminal_result
|
|
from ouroboros.task_results import STATUS_COMPLETED, load_task_result
|
|
|
|
parent = tmp_path / "data"
|
|
repo = tmp_path / "repo"
|
|
parent.mkdir()
|
|
_init_repo_with_file(repo)
|
|
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
|
|
task_id = "axisfields"
|
|
write_task_result(
|
|
parent,
|
|
task_id,
|
|
STATUS_COMPLETED,
|
|
workspace_root=str(repo),
|
|
artifact_status=ARTIFACT_STATUS_FINALIZING,
|
|
artifact_bundle={"schema_version": 1, "status": "pending", "artifacts": [], "errors": []},
|
|
outcome_axes={
|
|
"lifecycle": {"status": STATUS_COMPLETED},
|
|
"artifacts": {
|
|
"status": ARTIFACT_STATUS_FINALIZING,
|
|
"diagnostics": {"existing": True},
|
|
"error_count": 0,
|
|
},
|
|
"objective": {"status": "not_evaluated", "source": "none"},
|
|
},
|
|
)
|
|
|
|
finalize_task_artifacts(parent, {"id": task_id, "workspace_root": str(repo)})
|
|
|
|
result = load_task_result(parent, task_id)
|
|
artifact_axis = result["outcome_axes"]["artifacts"]
|
|
assert artifact_axis["status"] == result["artifact_bundle"]["status"]
|
|
assert result["artifact_bundle"]["status"] == result["artifact_status"]
|
|
assert result["artifact_bundle"]["status"] not in {"pending", "finalizing"}
|
|
assert _is_terminal_result(result) is True
|
|
assert artifact_axis["diagnostics"] == {"existing": True}
|
|
assert artifact_axis["error_count"] == 0
|
|
|
|
|
|
def test_effective_result_preserves_workspace_patch_kind_with_child_drive(tmp_path):
|
|
from ouroboros.artifacts import copy_file_to_task_artifacts
|
|
from ouroboros.cli import _patch_from_result
|
|
from ouroboros.task_results import STATUS_COMPLETED
|
|
from ouroboros.task_status import load_effective_task_result
|
|
|
|
parent = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
repo = tmp_path / "repo"
|
|
parent.mkdir()
|
|
child.mkdir()
|
|
_init_repo_with_file(repo)
|
|
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
|
|
|
|
task_id = "patchkind"
|
|
report = tmp_path / "report.html"
|
|
report.write_text("<h1>done</h1>", encoding="utf-8")
|
|
child_record = copy_file_to_task_artifacts(SimpleNamespace(drive_root=child, task_id=task_id), report, kind="user_file")
|
|
assert child_record is not None
|
|
write_task_result(
|
|
child,
|
|
task_id,
|
|
STATUS_COMPLETED,
|
|
result="child done",
|
|
artifacts=[child_record],
|
|
artifact_status=ARTIFACT_STATUS_READY,
|
|
ts="2026-01-01T00:00:02Z",
|
|
)
|
|
write_task_result(
|
|
parent,
|
|
task_id,
|
|
STATUS_COMPLETED,
|
|
result="child done",
|
|
workspace_root=str(repo),
|
|
child_drive_root=str(child),
|
|
artifacts=[child_record],
|
|
artifact_status="finalizing",
|
|
child_status=STATUS_COMPLETED,
|
|
)
|
|
|
|
finalize_task_artifacts(parent, {"id": task_id, "workspace_root": str(repo), "drive_root": str(child)})
|
|
|
|
effective = load_effective_task_result(parent, task_id)
|
|
patch_artifacts = [
|
|
item
|
|
for item in effective.get("artifacts") or []
|
|
if isinstance(item, dict) and item.get("name") == "workspace.patch"
|
|
]
|
|
assert patch_artifacts
|
|
assert patch_artifacts[0]["kind"] == "workspace_patch"
|
|
assert any(item.get("kind") == "user_file" for item in effective.get("artifacts") or [] if isinstance(item, dict))
|
|
|
|
class FakeClient:
|
|
def __init__(self):
|
|
self.paths = []
|
|
|
|
def get_bytes(self, path):
|
|
self.paths.append(path)
|
|
return b"diff --git a/tracked.txt b/tracked.txt\n"
|
|
|
|
client = FakeClient()
|
|
assert _patch_from_result(client, task_id, effective, strict=True).startswith("diff --git")
|
|
assert client.paths == [f"/api/tasks/{task_id}/artifacts/workspace.patch"]
|
|
|
|
|
|
def test_task_artifact_endpoint_serves_only_declared_artifacts(tmp_path):
|
|
data = tmp_path / "data"
|
|
artifact_dir = task_artifacts_dir(data, "task-artifact")
|
|
patch_path = artifact_dir / "workspace.patch"
|
|
patch_path.write_text("diff --git a/a b/a\n", encoding="utf-8")
|
|
write_task_result(
|
|
data,
|
|
"task-artifact",
|
|
"completed",
|
|
artifacts=[{"kind": "workspace_patch", "name": "workspace.patch", "path": str(patch_path), "size": patch_path.stat().st_size}],
|
|
artifact_status="ready",
|
|
)
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
client = TestClient(app)
|
|
|
|
assert client.get("/api/tasks/task-artifact/artifacts/workspace.patch").text.startswith("diff --git")
|
|
assert client.get("/api/tasks/task-artifact/artifacts/missing.patch").status_code == 404
|
|
assert client.get("/api/tasks/task-artifact/artifacts/bad%5Cname").status_code == 400
|
|
|
|
|
|
def test_task_artifact_endpoint_serves_exact_chat_media_without_task_result(tmp_path):
|
|
from ouroboros.artifacts import collect_task_artifact_records, store_chat_media_bytes
|
|
|
|
data = tmp_path / "data"
|
|
stored = store_chat_media_bytes(data, "ephemeral1", b"photo-bytes", "image/png")
|
|
assert stored is not None
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
client = TestClient(app)
|
|
|
|
response = client.get(f"/api/tasks/ephemeral1/artifacts/{stored['name']}")
|
|
assert response.status_code == 200
|
|
assert response.content == b"photo-bytes"
|
|
assert collect_task_artifact_records(data, "ephemeral1") == []
|
|
|
|
assert client.get("/api/tasks/ephemeral1/artifacts/chat-media-bad.png").status_code == 404
|
|
|
|
|
|
def test_task_artifact_endpoint_serves_manifest_artifact_after_status_repair(tmp_path):
|
|
from ouroboros.artifacts import copy_file_to_task_artifacts
|
|
|
|
data = tmp_path / "data"
|
|
source_dir = tmp_path / "Desktop"
|
|
source_dir.mkdir()
|
|
source = source_dir / "report.html"
|
|
source.write_text("<h1>ok</h1>", encoding="utf-8")
|
|
copy_file_to_task_artifacts(SimpleNamespace(drive_root=data, task_id="orphaned"), source, kind="user_file")
|
|
write_task_result(
|
|
data,
|
|
"orphaned",
|
|
"running",
|
|
result_status="infra_failed",
|
|
reason_code="provider_failure",
|
|
result="provider failed before normal finalization",
|
|
)
|
|
(data / "state").mkdir(parents=True, exist_ok=True)
|
|
(data / "state" / "queue_snapshot.json").write_text('{"pending": [], "running": []}', encoding="utf-8")
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
|
|
response = TestClient(app).get("/api/tasks/orphaned/artifacts/report.html")
|
|
|
|
assert response.status_code == 200
|
|
assert response.text == "<h1>ok</h1>"
|
|
|
|
|
|
def test_task_artifact_endpoint_rebases_child_drive_artifact_after_status_repair(tmp_path):
|
|
from ouroboros.artifacts import collect_task_artifact_records, copy_file_to_task_artifacts
|
|
|
|
data = tmp_path / "data"
|
|
child = tmp_path / "child"
|
|
source_dir = tmp_path / "Desktop"
|
|
source_dir.mkdir()
|
|
source = source_dir / "report.html"
|
|
source.write_text("<h1>child</h1>", encoding="utf-8")
|
|
copy_file_to_task_artifacts(SimpleNamespace(drive_root=child, task_id="childart"), source, kind="user_file")
|
|
child_artifacts = collect_task_artifact_records(child, "childart")
|
|
write_task_result(
|
|
child,
|
|
"childart",
|
|
"completed",
|
|
result="done",
|
|
artifacts=child_artifacts,
|
|
artifact_status="ready",
|
|
ts="2026-01-01T00:00:02Z",
|
|
)
|
|
write_task_result(
|
|
data,
|
|
"childart",
|
|
"running",
|
|
child_drive_root=str(child),
|
|
workspace_root=str(tmp_path / "workspace"),
|
|
result_status="infra_failed",
|
|
reason_code="provider_failure",
|
|
result="provider failed before normal finalization",
|
|
)
|
|
(data / "state").mkdir(parents=True, exist_ok=True)
|
|
(data / "state" / "queue_snapshot.json").write_text('{"pending": [], "running": []}', encoding="utf-8")
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
|
|
response = TestClient(app).get("/api/tasks/childart/artifacts/report.html")
|
|
|
|
parent_artifact = task_artifacts_dir(data, "childart", create=False) / "report.html"
|
|
assert response.status_code == 200
|
|
assert response.text == "<h1>child</h1>"
|
|
assert parent_artifact.read_text(encoding="utf-8") == "<h1>child</h1>"
|
|
|
|
|
|
def test_task_artifact_endpoint_rejects_metadata_name_path_mismatch(tmp_path):
|
|
data = tmp_path / "data"
|
|
artifact_dir = task_artifacts_dir(data, "task-artifact")
|
|
wrong_path = artifact_dir / "memory_export.json"
|
|
wrong_path.write_text("{}", encoding="utf-8")
|
|
write_task_result(
|
|
data,
|
|
"task-artifact",
|
|
"completed",
|
|
artifacts=[{"kind": "workspace_patch", "name": "workspace.patch", "path": str(wrong_path), "size": wrong_path.stat().st_size}],
|
|
artifact_status="ready",
|
|
)
|
|
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
|
|
app.state.drive_root = data
|
|
|
|
assert TestClient(app).get("/api/tasks/task-artifact/artifacts/workspace.patch").status_code == 500
|
|
|
|
|
|
def test_memory_export_includes_nested_memory_files(tmp_path):
|
|
drive = tmp_path / "child"
|
|
memory = drive / "memory"
|
|
nested = memory / "knowledge" / "patterns"
|
|
nested.mkdir(parents=True)
|
|
(memory / "identity.md").write_text("id\n", encoding="utf-8")
|
|
(nested / "cli.md").write_text("pattern\n", encoding="utf-8")
|
|
|
|
export = build_memory_export(drive, {"id": "task-1", "memory_mode": "forked"})
|
|
|
|
assert export["files"]["identity.md"] == "id\n"
|
|
assert export["files"]["knowledge/patterns/cli.md"] == "pattern\n"
|
|
|
|
|
|
def test_startup_prune_removes_only_old_terminal_child_drives(tmp_path):
|
|
data = tmp_path / "data"
|
|
terminal_dir = data / "state" / "headless_tasks" / "oldterminal"
|
|
pending_dir = data / "state" / "headless_tasks" / "oldpending"
|
|
fresh_timestamp_dir = data / "state" / "headless_tasks" / "freshresult"
|
|
terminal_drive = terminal_dir / "data"
|
|
pending_drive = pending_dir / "data"
|
|
fresh_timestamp_drive = fresh_timestamp_dir / "data"
|
|
terminal_drive.mkdir(parents=True)
|
|
pending_drive.mkdir(parents=True)
|
|
fresh_timestamp_drive.mkdir(parents=True)
|
|
|
|
now = time.time()
|
|
old = now - (8 * 86400)
|
|
old_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(old))
|
|
fresh_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(now))
|
|
write_task_result(data, "oldterminal", "completed", child_drive_root=str(terminal_drive), artifact_status="ready", result="done", ts=old_iso)
|
|
write_task_result(data, "oldpending", "scheduled", child_drive_root=str(pending_drive), result="queued")
|
|
write_task_result(data, "freshresult", "completed", child_drive_root=str(fresh_timestamp_drive), artifact_status="ready", result="done", ts=fresh_iso)
|
|
os.utime(terminal_dir, (old, old))
|
|
os.utime(pending_dir, (old, old))
|
|
os.utime(fresh_timestamp_dir, (old, old))
|
|
|
|
report = prune_headless_task_drives(data, retention_days=7, now=now)
|
|
|
|
assert [item["task_id"] for item in report["pruned"]] == ["oldterminal"]
|
|
assert not terminal_dir.exists()
|
|
assert pending_dir.exists()
|
|
assert fresh_timestamp_dir.exists()
|
|
assert any(item["task_id"] == "oldpending" and item["reason"] == "parent_not_terminal" for item in report["skipped"])
|
|
assert any(item["task_id"] == "freshresult" and item["reason"] == "younger_than_retention" for item in report["skipped"])
|
|
|
|
|
|
def test_startup_prune_uses_effective_terminal_status(tmp_path):
|
|
data = tmp_path / "data"
|
|
task_drive = data / "task_drives" / "stalerun"
|
|
child_dir = data / "state" / "headless_tasks" / "stalechild"
|
|
child_drive = child_dir / "data"
|
|
task_drive.mkdir(parents=True)
|
|
child_drive.mkdir(parents=True)
|
|
(task_drive / "scratch.txt").write_text("scratch", encoding="utf-8")
|
|
(child_drive / "scratch.txt").write_text("child", encoding="utf-8")
|
|
(data / "state").mkdir(parents=True, exist_ok=True)
|
|
(data / "state" / "queue_snapshot.json").write_text('{"pending": [], "running": []}', encoding="utf-8")
|
|
|
|
now = time.time()
|
|
old = now - (8 * 86400)
|
|
old_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(old))
|
|
for task_id, extra in (
|
|
("stalerun", {}),
|
|
("stalechild", {"child_drive_root": str(child_drive)}),
|
|
):
|
|
write_task_result(
|
|
data,
|
|
task_id,
|
|
"running",
|
|
result_status="infra_failed",
|
|
reason_code="provider_failure",
|
|
result="provider failed",
|
|
ts=old_iso,
|
|
**extra,
|
|
)
|
|
os.utime(task_drive, (old, old))
|
|
os.utime(child_dir, (old, old))
|
|
|
|
direct_report = prune_task_drives(data, retention_days=7, now=now)
|
|
child_report = prune_headless_task_drives(data, retention_days=7, now=now)
|
|
|
|
assert [item["task_id"] for item in direct_report["pruned"]] == ["stalerun"]
|
|
assert [item["task_id"] for item in child_report["pruned"]] == ["stalechild"]
|
|
assert not task_drive.exists()
|
|
assert not child_dir.exists()
|
|
|
|
|
|
def test_startup_prune_removes_only_old_terminal_task_scratch(tmp_path):
|
|
data = tmp_path / "data"
|
|
old_terminal = data / "task_drives" / "oldterminal"
|
|
old_pending = data / "task_drives" / "oldpending"
|
|
fresh_terminal = data / "task_drives" / "freshterminal"
|
|
for path in (old_terminal, old_pending, fresh_terminal):
|
|
path.mkdir(parents=True)
|
|
(path / "scratch.txt").write_text("scratch", encoding="utf-8")
|
|
|
|
now = time.time()
|
|
old = now - (8 * 86400)
|
|
old_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(old))
|
|
fresh_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(now))
|
|
write_task_result(data, "oldterminal", "completed", result="done", ts=old_iso)
|
|
write_task_result(data, "oldpending", "running", result="running")
|
|
write_task_result(data, "freshterminal", "completed", result="done", ts=fresh_iso)
|
|
os.utime(old_terminal, (old, old))
|
|
os.utime(old_pending, (old, old))
|
|
os.utime(fresh_terminal, (old, old))
|
|
|
|
report = prune_task_drives(data, retention_days=7, now=now)
|
|
|
|
assert [item["task_id"] for item in report["pruned"]] == ["oldterminal"]
|
|
assert not old_terminal.exists()
|
|
assert old_pending.exists()
|
|
assert fresh_terminal.exists()
|
|
assert any(item["task_id"] == "oldpending" and item["reason"] == "task_not_terminal" for item in report["skipped"])
|
|
assert any(item["task_id"] == "freshterminal" and item["reason"] == "younger_than_retention" for item in report["skipped"])
|
|
|
|
|
|
def test_external_child_task_budget_uses_parent_drive_state(tmp_path, monkeypatch):
|
|
from ouroboros import usage_accounting
|
|
from ouroboros.agent import Env, OuroborosAgent
|
|
|
|
repo = tmp_path / "repo"
|
|
parent = tmp_path / "parent-data"
|
|
child = tmp_path / "child-data"
|
|
for root in (repo, parent, child):
|
|
root.mkdir()
|
|
for drive in (parent, child):
|
|
(drive / "state").mkdir()
|
|
(drive / "logs").mkdir()
|
|
# Compatibility projections are deliberately misleading here: the physical-attempt
|
|
# ledger in the parent budget root is the sole monetary authority.
|
|
(parent / "state" / "state.json").write_text('{"spent_usd": 0.0}\n', encoding="utf-8")
|
|
(child / "state" / "state.json").write_text('{"spent_usd": 0.0}\n', encoding="utf-8")
|
|
reservation = usage_accounting.reserve_attempt(usage_accounting.AttemptRequest(
|
|
model="test/model",
|
|
provider="test",
|
|
reservation_usd=9.0,
|
|
drive_root=parent,
|
|
task_id="prior-task",
|
|
root_task_id="prior-task",
|
|
source="test",
|
|
))
|
|
usage_accounting.mark_dispatched(reservation)
|
|
usage_accounting.settle_attempt(reservation, {}, cost_usd=9.0, cost_final=True)
|
|
|
|
monkeypatch.setenv("TOTAL_BUDGET", "10")
|
|
monkeypatch.setattr(OuroborosAgent, "_log_worker_boot_once", lambda self: None)
|
|
monkeypatch.setattr("ouroboros.agent.build_llm_messages", lambda **kwargs: ([], {}))
|
|
|
|
agent = OuroborosAgent(Env(repo_dir=repo, drive_root=child))
|
|
ctx, _messages, cap_info = agent._prepare_task_context({
|
|
"id": "budget-task",
|
|
"type": "task",
|
|
"text": "x",
|
|
"budget_drive_root": str(parent),
|
|
})
|
|
|
|
assert cap_info["budget_remaining"] == 1.0
|
|
assert ctx.task_metadata["budget_drive_root"] == str(parent)
|
|
|
|
|
|
def test_cli_patch_downloads_http_artifact():
|
|
from ouroboros.cli import _patch_from_result
|
|
|
|
class FakeClient:
|
|
def __init__(self):
|
|
self.paths = []
|
|
|
|
def get_bytes(self, path):
|
|
self.paths.append(path)
|
|
return b"diff --git a/a b/a\n"
|
|
|
|
client = FakeClient()
|
|
result = {"artifact_status": "ready", "artifacts": [{"kind": "workspace_patch", "name": "workspace.patch"}]}
|
|
|
|
assert _patch_from_result(client, "task-1", result, strict=True).startswith("diff --git")
|
|
assert client.paths == ["/api/tasks/task-1/artifacts/workspace.patch"]
|
|
|
|
|
|
def test_cli_patch_falls_back_to_workspace_patch_name():
|
|
from ouroboros.cli import _patch_from_result
|
|
|
|
class FakeClient:
|
|
def __init__(self):
|
|
self.paths = []
|
|
|
|
def get_bytes(self, path):
|
|
self.paths.append(path)
|
|
return b"diff --git a/a b/a\n"
|
|
|
|
client = FakeClient()
|
|
result = {"artifact_status": "ready", "artifacts": [{"kind": "task_artifact", "name": "workspace.patch"}]}
|
|
|
|
assert _patch_from_result(client, "task-1", result, strict=True).startswith("diff --git")
|
|
assert client.paths == ["/api/tasks/task-1/artifacts/workspace.patch"]
|
|
|
|
|
|
def test_cli_patch_strict_rejects_empty_artifact():
|
|
from ouroboros.cli import PatchCLIError, _patch_from_result
|
|
|
|
class FakeClient:
|
|
def get_bytes(self, path):
|
|
return b""
|
|
|
|
result = {"artifact_status": "ready", "artifacts": [{"kind": "workspace_patch", "name": "workspace.patch"}]}
|
|
with pytest.raises(PatchCLIError, match="empty"):
|
|
_patch_from_result(FakeClient(), "task-1", result, strict=True)
|
|
|
|
|
|
def test_cli_terminal_success_uses_outcome_axes():
|
|
from ouroboros.cli import _is_terminal_success
|
|
|
|
base = {
|
|
"status": "completed",
|
|
"artifact_status": "ready",
|
|
"outcome_axes": {
|
|
"execution": {"status": "ok"},
|
|
"objective": {"status": "not_evaluated"},
|
|
},
|
|
}
|
|
assert _is_terminal_success(base) is True
|
|
|
|
failed_objective = {
|
|
**base,
|
|
"outcome_axes": {
|
|
"execution": {"status": "ok"},
|
|
"objective": {"status": "fail", "source": "task_acceptance_review"},
|
|
},
|
|
}
|
|
assert _is_terminal_success(failed_objective) is False
|
|
|
|
degraded_execution = {
|
|
**base,
|
|
"outcome_axes": {
|
|
"execution": {"status": "degraded"},
|
|
"objective": {"status": "not_evaluated"},
|
|
},
|
|
}
|
|
assert _is_terminal_success(degraded_execution) is False
|
|
|
|
# Pin the documented best_effort contract: a forced-finalization best-effort
|
|
# completion is NOT clean terminal success (CLI strict modes must not treat
|
|
# it as a clean pass)...
|
|
best_effort_execution = {
|
|
**base,
|
|
"outcome_axes": {
|
|
"execution": {"status": "best_effort"},
|
|
"objective": {"status": "not_evaluated"},
|
|
},
|
|
}
|
|
assert _is_terminal_success(best_effort_execution) is False
|
|
|
|
|
|
def test_cli_has_no_file_or_review_commit_groups():
|
|
from ouroboros.cli import build_parser
|
|
|
|
parser = build_parser()
|
|
assert parser.parse_args(["run", "hello"]).command == "run"
|
|
with pytest.raises(SystemExit):
|
|
parser.parse_args(["files"])
|
|
with pytest.raises(SystemExit):
|
|
parser.parse_args(["commit"])
|
|
with pytest.raises(SystemExit):
|
|
parser.parse_args(["review"])
|
|
with pytest.raises(SystemExit):
|
|
parser.parse_args(["skills", "review", "demo"])
|
|
|
|
|
|
def test_source_server_start_is_blocked_in_packaged_cli_env(monkeypatch):
|
|
from ouroboros import cli
|
|
|
|
monkeypatch.setenv("OUROBOROS_PACKAGED_CLI", "1")
|
|
monkeypatch.setattr(cli.subprocess, "Popen", lambda *args, **kwargs: pytest.fail("direct server start"))
|
|
|
|
with pytest.raises(cli.CLIError, match="packaged CLI must launch the desktop app"):
|
|
cli._start_local_server("http://127.0.0.1:8765")
|
|
|
|
|
|
def test_packaged_cli_run_start_scan_skips_timeout_value():
|
|
from ouroboros.packaged_cli import _run_start_index
|
|
|
|
assert _run_start_index(["run", "--timeout", "5", "--start", "hello"], 0) == 3
|
|
|
|
|
|
def test_cli_run_no_stream_waits_without_jsonl(monkeypatch, capsys):
|
|
from ouroboros import cli
|
|
|
|
class FakeClient:
|
|
def request(self, method, path, body=None):
|
|
assert method == "POST"
|
|
assert path == "/api/tasks"
|
|
return {"task_id": "abc123"}
|
|
|
|
monkeypatch.setattr(cli, "_client", lambda args, start=False: FakeClient())
|
|
monkeypatch.setattr(cli, "_wait_task", lambda client, task_id, timeout_sec: {"status": "completed", "result": "done"})
|
|
|
|
assert cli.main(["run", "--no-stream", "hello"]) == 0
|
|
captured = capsys.readouterr()
|
|
assert captured.out.strip() == "done"
|
|
|
|
|
|
def test_cli_run_timeout_waits_through_finalization_grace(monkeypatch):
|
|
from ouroboros import cli
|
|
from supervisor import queue
|
|
|
|
captured = {}
|
|
|
|
class FakeClient:
|
|
def request(self, method, path, body=None):
|
|
return {"task_id": "abc123"}
|
|
|
|
def fake_wait(_client, _task_id, timeout_sec):
|
|
captured["timeout_sec"] = timeout_sec
|
|
return {"status": "completed", "result": "done"}
|
|
|
|
monkeypatch.setenv("OUROBOROS_FINALIZATION_GRACE_SEC", "2")
|
|
queue.init(pathlib.Path("/tmp/ouroboros-test-data"), 600, 1800)
|
|
assert queue.FINALIZATION_GRACE_SEC == 2
|
|
monkeypatch.setattr(cli, "_client", lambda args, start=False: FakeClient())
|
|
monkeypatch.setattr(cli, "_wait_task", fake_wait)
|
|
|
|
assert cli.main(["run", "--no-stream", "--timeout", "7", "hello"]) == 0
|
|
assert captured["timeout_sec"] == 14.0
|
|
monkeypatch.delenv("OUROBOROS_FINALIZATION_GRACE_SEC", raising=False)
|
|
assert cli._deadline_wait_timeout(7) == 132.0
|
|
|
|
|
|
def test_cli_run_detach_prints_task_id_without_waiting(monkeypatch, capsys):
|
|
from ouroboros import cli
|
|
|
|
class FakeClient:
|
|
def request(self, method, path, body=None):
|
|
assert method == "POST"
|
|
assert path == "/api/tasks"
|
|
return {"task_id": "abc123"}
|
|
|
|
monkeypatch.setattr(cli, "_client", lambda args, start=False: FakeClient())
|
|
monkeypatch.setattr(cli, "_watch_task", lambda *args, **kwargs: pytest.fail("detach should not watch"))
|
|
monkeypatch.setattr(cli, "_wait_task", lambda *args, **kwargs: pytest.fail("detach should not wait"))
|
|
|
|
assert cli.main(["run", "--detach", "hello"]) == 0
|
|
captured = capsys.readouterr()
|
|
assert captured.out.strip() == "abc123"
|
|
|
|
|
|
def test_cli_run_actor_id_is_sent_as_gateway_root_field(monkeypatch, capsys):
|
|
from ouroboros import cli
|
|
|
|
captured = {}
|
|
|
|
class FakeClient:
|
|
def request(self, method, path, body=None):
|
|
captured["method"] = method
|
|
captured["path"] = path
|
|
captured["body"] = body
|
|
return {"task_id": "abc123"}
|
|
|
|
monkeypatch.setattr(cli, "_client", lambda args, start=False: FakeClient())
|
|
monkeypatch.setattr(cli, "_watch_task", lambda *args, **kwargs: pytest.fail("detach should not watch"))
|
|
|
|
assert cli.main(["run", "--detach", "--timeout", "7", "--actor-id", "operator-1", "hello"]) == 0
|
|
assert captured["method"] == "POST"
|
|
assert captured["path"] == "/api/tasks"
|
|
assert captured["body"]["description"] == "hello"
|
|
assert "text" not in captured["body"]
|
|
assert "prompt" not in captured["body"]
|
|
assert captured["body"]["actor_id"] == "operator-1"
|
|
assert captured["body"]["timeout_sec"] == 7.0
|
|
assert captured["body"]["source"] == "cli"
|
|
assert captured["body"]["metadata"]["source"] == "cli"
|
|
assert "actor_id" not in captured["body"]["metadata"]
|
|
assert capsys.readouterr().out.strip() == "abc123"
|
|
|
|
|
|
def test_cli_run_disable_tools_sent_as_gateway_root_field(monkeypatch, capsys):
|
|
from ouroboros import cli
|
|
|
|
captured = {}
|
|
|
|
class FakeClient:
|
|
def request(self, method, path, body=None):
|
|
captured["method"] = method
|
|
captured["path"] = path
|
|
captured["body"] = body
|
|
return {"task_id": "abc123"}
|
|
|
|
monkeypatch.setattr(cli, "_client", lambda args, start=False: FakeClient())
|
|
monkeypatch.setattr(cli, "_watch_task", lambda *args, **kwargs: pytest.fail("detach should not watch"))
|
|
|
|
assert cli.main([
|
|
"run", "--detach",
|
|
"--disable-tools", "web_search,browse_page",
|
|
"--disable-tools", "claude_code_edit",
|
|
"hello",
|
|
]) == 0
|
|
assert captured["method"] == "POST"
|
|
assert captured["path"] == "/api/tasks"
|
|
assert captured["body"]["disabled_tools"] == ["web_search", "browse_page", "claude_code_edit"]
|
|
assert capsys.readouterr().out.strip() == "abc123"
|
|
|
|
|
|
def test_cli_run_task_metadata_json_merges_but_cannot_forge_service_keys(monkeypatch, capsys):
|
|
"""--task-metadata-json (v6.56.0) merges user metadata (e.g. budget_profile)
|
|
into body.metadata, but the host-owned delegation_role/source keys are spread
|
|
last and can never be overridden by the user JSON (subagent forgery)."""
|
|
from ouroboros import cli
|
|
|
|
captured = {}
|
|
|
|
class FakeClient:
|
|
def request(self, method, path, body=None):
|
|
captured["body"] = body
|
|
return {"task_id": "abc123"}
|
|
|
|
monkeypatch.setattr(cli, "_client", lambda args, start=False: FakeClient())
|
|
monkeypatch.setattr(cli, "_watch_task", lambda *args, **kwargs: pytest.fail("detach should not watch"))
|
|
|
|
payload = (
|
|
'{"budget_profile": {"improvement_policy": "until_deadline", "cost_hard_stop_pct": 0},'
|
|
' "delegation_role": "subagent", "source": "forged"}'
|
|
)
|
|
assert cli.main(["run", "--detach", "--task-metadata-json", payload, "hello"]) == 0
|
|
metadata = captured["body"]["metadata"]
|
|
assert metadata["budget_profile"] == {
|
|
"improvement_policy": "until_deadline",
|
|
"cost_hard_stop_pct": 0,
|
|
}
|
|
assert metadata["delegation_role"] == "root"
|
|
assert metadata["source"] == "cli"
|
|
assert capsys.readouterr().out.strip() == "abc123"
|
|
|
|
|
|
def test_cli_run_task_metadata_json_rejects_invalid_payloads(monkeypatch):
|
|
from ouroboros import cli
|
|
|
|
monkeypatch.setattr(cli, "_client", lambda *args, **kwargs: pytest.fail("client should not be created"))
|
|
|
|
for bad in ("not json", "[1, 2]"):
|
|
args = SimpleNamespace(
|
|
prompt=["hello"], delegation_role="root", task_metadata_json=bad,
|
|
)
|
|
with pytest.raises(cli.CLIError, match="task-metadata-json"):
|
|
cli._run_command(args)
|
|
|
|
|
|
def test_cli_run_rejects_forged_subagent_role_before_request(monkeypatch):
|
|
from ouroboros import cli
|
|
|
|
monkeypatch.setattr(cli, "_client", lambda *args, **kwargs: pytest.fail("client should not be created"))
|
|
|
|
args = SimpleNamespace(prompt=["hello"], delegation_role="subagent")
|
|
with pytest.raises(cli.CLIError, match="internal schedule_subagent"):
|
|
cli._run_command(args)
|
|
|
|
|
|
def test_cli_watch_caps_sse_wait_by_timeout(monkeypatch):
|
|
from ouroboros import cli
|
|
|
|
calls = []
|
|
times = iter([100.0, 100.1, 100.2, 101.0])
|
|
|
|
class FakeClient:
|
|
def stream_sse(self, path, timeout=120.0):
|
|
calls.append((path, timeout))
|
|
return iter(())
|
|
|
|
monkeypatch.setattr(cli.time, "time", lambda: next(times))
|
|
monkeypatch.setattr(cli.time, "sleep", lambda _seconds: None)
|
|
|
|
with pytest.raises(cli.TaskTimeoutCLIError):
|
|
cli._watch_task(FakeClient(), "abc123", jsonl=False, quiet=True, timeout_sec=0.5)
|
|
assert "wait=0" in calls[0][0]
|
|
assert calls[0][1] <= 1.5
|
|
|
|
|
|
def test_cli_wait_task_caps_poll_request_by_timeout(monkeypatch):
|
|
from ouroboros import cli
|
|
|
|
calls = []
|
|
times = iter([100.0, 100.1, 100.6])
|
|
|
|
class FakeClient:
|
|
timeout = 30.0
|
|
|
|
def request(self, method, path, body=None, *, timeout=None):
|
|
calls.append(timeout)
|
|
raise cli.ConnectionCLIError("poll timed out")
|
|
|
|
monkeypatch.setattr(cli.time, "time", lambda: next(times))
|
|
|
|
with pytest.raises(cli.TaskTimeoutCLIError):
|
|
cli._wait_task(FakeClient(), "abc123", timeout_sec=0.5)
|
|
assert calls and calls[0] <= 0.5
|
|
|
|
|
|
def test_swebench_helper_records_cli_timeout_with_continue(tmp_path, monkeypatch):
|
|
script_path = (
|
|
pathlib.Path(__file__).resolve().parent.parent
|
|
/ "devtools"
|
|
/ "benchmarks"
|
|
/ "swe_bench"
|
|
/ "swebench_predictions.py"
|
|
)
|
|
spec = importlib.util.spec_from_file_location("swebench_predictions_test", script_path)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
rows_path = tmp_path / "rows.jsonl"
|
|
output_path = tmp_path / "predictions.jsonl"
|
|
logs_dir = tmp_path / "logs"
|
|
rows_path.write_text(
|
|
json.dumps({"instance_id": "inst1", "workspace_root": str(workspace), "problem_statement": "fix"}) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
run_timeouts = []
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
if cmd[:2] == ["git", "rev-parse"]:
|
|
return subprocess.CompletedProcess(cmd, 0, stdout="abc\n", stderr="")
|
|
if cmd[:2] == ["git", "status"]:
|
|
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
|
run_timeouts.append(kwargs.get("timeout"))
|
|
raise subprocess.TimeoutExpired(cmd, kwargs.get("timeout", 1), output="partial-out", stderr="partial-err")
|
|
|
|
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
|
monkeypatch.setattr(module, "get_finalization_grace_sec", lambda: 7)
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
[
|
|
"swebench_predictions.py",
|
|
# --allow-dirty-seed: this test exercises the CLI-timeout ledger path, not the
|
|
# v6.75.0 seed-provenance gate, so it must not depend on the developer's tree state.
|
|
"--allow-dirty-seed",
|
|
"--input",
|
|
str(rows_path),
|
|
"--output",
|
|
str(output_path),
|
|
"--timeout",
|
|
"1",
|
|
"--continue-on-error",
|
|
"--logs-dir",
|
|
str(logs_dir),
|
|
],
|
|
)
|
|
|
|
assert module.main() == 0
|
|
errors = (tmp_path / "predictions.jsonl.errors.jsonl").read_text(encoding="utf-8")
|
|
assert '"timeout": true' in errors
|
|
assert run_timeouts == [68]
|
|
assert (logs_dir / "inst1" / "ouroboros.stdout").read_text(encoding="utf-8") == "partial-out"
|
|
assert (logs_dir / "inst1" / "ouroboros.stderr").read_text(encoding="utf-8") == "partial-err"
|
|
|
|
|
|
def test_terminal_bench_harbor_adapter_imports_without_harbor():
|
|
script_path = (
|
|
pathlib.Path(__file__).resolve().parent.parent
|
|
/ "devtools"
|
|
/ "benchmarks"
|
|
/ "terminal_bench"
|
|
/ "harbor_installed_agent.py"
|
|
)
|
|
spec = importlib.util.spec_from_file_location("terminal_bench_harbor_adapter_test", script_path)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
assert module.OuroborosTerminalBenchAgent.name() == "Ouroboros Installed"
|
|
assert module._repo_root() == pathlib.Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def test_queue_restore_accepts_headless_chat_zero(tmp_path, monkeypatch):
|
|
import supervisor.queue as queue
|
|
|
|
monkeypatch.setattr(queue, "PENDING", [])
|
|
monkeypatch.setattr(queue, "RUNNING", {})
|
|
monkeypatch.setattr(queue, "QUEUE_SEQ_COUNTER_REF", {"value": 0})
|
|
monkeypatch.setattr(queue, "DRIVE_ROOT", tmp_path)
|
|
monkeypatch.setattr(queue, "QUEUE_SNAPSHOT_PATH", tmp_path / "queue_snapshot.json")
|
|
monkeypatch.setattr(queue, "append_jsonl", lambda *args, **kwargs: None)
|
|
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
|
|
(tmp_path / "queue_snapshot.json").write_text(
|
|
json.dumps({
|
|
"ts": utc_now_iso(),
|
|
"pending": [{"task": {"id": "headless1", "type": "task", "chat_id": 0, "text": "x"}}],
|
|
}),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert queue.restore_pending_from_snapshot(max_age_sec=900) == 1
|
|
assert queue.PENDING[0]["id"] == "headless1"
|