Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions tests/ui/test_v2_routers_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,73 @@ def test_start_task_not_found(self, test_client):

assert response.status_code == 404

def test_start_task_execute_runs_agent_to_terminal(
self, test_client_with_task, monkeypatch
):
"""Start with execute=true dispatches the agent (BUILD path) to a terminal run.

Covers the agent-dispatch branch (`_run_agent` -> `runtime.execute_agent`),
which the other `/start` tests skip by omitting `execute=true` (#749).

MockProvider is injected via CODEFRAME_LLM_PROVIDER=mock: the router builds
it through `get_provider("mock")`, and its text-only response makes the
ReAct agent complete on the first iteration (react_agent.py:518).
"""
import threading
import time

from codeframe.core import runtime, streaming

monkeypatch.setenv("CODEFRAME_LLM_PROVIDER", "mock")

task_id = test_client_with_task.task.id
workspace = test_client_with_task.workspace

# Snapshot threads so we can join the endpoint's fire-and-forget worker
# before the fixture deletes the temp workspace (avoids a teardown race
# where the still-draining thread touches a removed DB).
threads_before = set(threading.enumerate())

response = test_client_with_task.post(
f"/api/v2/tasks/{task_id}/start?execute=true"
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["status"] == "executing"
run_id = data["run_id"]

# execute_agent runs in a background daemon thread; poll the run until it
# leaves RUNNING (terminal = COMPLETED / FAILED / BLOCKED).
deadline = time.monotonic() + 30.0
status = "RUNNING"
while time.monotonic() < deadline:
run_resp = test_client_with_task.get(f"/api/v2/tasks/{task_id}/run")
assert run_resp.status_code == 200
status = run_resp.json()["status"]
if status != runtime.RunStatus.RUNNING.value:
break
time.sleep(0.2)

# Let the worker thread finish its tail work (logging/recording) before
# assertions and fixture teardown remove the workspace out from under it.
for thread in set(threading.enumerate()) - threads_before:
thread.join(timeout=10.0)

assert status != runtime.RunStatus.RUNNING.value, (
f"Run {run_id} never reached a terminal status (stuck at {status})"
)
assert status == runtime.RunStatus.COMPLETED.value, (
f"Expected COMPLETED for the mock-driven BUILD path, got {status}"
)

# The BUILD path emitted output events (the whole point of dispatching).
# Assert on file *content*, not mere existence: RunOutputLogger creates the
# output file at execute_agent start, so path.exists() only proves the run
# started — a non-empty file proves the agent actually wrote output.
output_path = streaming.get_run_output_path(workspace, run_id)
assert output_path.stat().st_size > 0, "BUILD path produced no output"

def test_get_task_run(self, test_client_with_task):
"""Get task run status after starting."""
task_id = test_client_with_task.task.id
Expand Down
Loading