Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 8 additions & 0 deletions codeframe/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2724,6 +2724,14 @@ def work_resume(
console.print(" Use 'codeframe blocker list' to see blockers")
elif state.status == AgentStatus.FAILED:
console.print("[red]Task execution failed[/red]")

# Same rule as `work start`: anything short of COMPLETED exits 1, so
# `cf work resume <id> && next_step` cannot proceed on a task that
# did not finish. Exiting 0 on FAILED made the shell read a failed
# run as success. BLOCKED is included for parity with start — it is
# equally "normal" there, and one predictable rule beats two.
if state.status in (AgentStatus.BLOCKED, AgentStatus.FAILED):
raise typer.Exit(1)
else:
console.print(
"\n[yellow]Not executed.[/yellow] The run is RUNNING with no worker; "
Expand Down
41 changes: 41 additions & 0 deletions tests/ui/test_resume_starts_worker_1005.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,44 @@ def _boom(*a, **k):
assert "ANTHROPIC_API_KEY is not set" in result.output, (
f"the real error was masked: {result.output!r}"
)


# ---------------------------------------------------------------------------
# Exit codes must match `work start` (#1005 follow-up review)
# ---------------------------------------------------------------------------


def _invoke_resume_with_status(ws, task_id, status, monkeypatch):
from typer.testing import CliRunner

from codeframe.core.agent import AgentStatus

class _State:
blocker = None

_State.status = getattr(AgentStatus, status)
monkeypatch.setattr(runtime, "execute_agent", lambda *a, **k: _State())

from codeframe.cli.app import app
from codeframe.core import workspace as ws_module

monkeypatch.setattr(ws_module, "get_workspace", lambda p: ws)
return CliRunner().invoke(app, ["work", "resume", task_id[:8]])


@pytest.mark.parametrize("status,expected", [
("COMPLETED", 0),
("FAILED", 1),
("BLOCKED", 1),
])
def test_cli_resume_exit_code_matches_work_start(
blocked, monkeypatch, status, expected
):
"""Review finding: resume exited 0 even on FAILED, so
`cf work resume <id> && next_step` proceeded on a failed run. `work start`
exits 1 for anything short of COMPLETED (app.py) — resume now does too."""
_, ws, task_id, _ = blocked

result = _invoke_resume_with_status(ws, task_id, status, monkeypatch)

assert result.exit_code == expected, result.output
Loading