mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(run-hardening): workspace branch-collision + verb-runner None guard (#251)
* fix(gateway): guard the verb runner against a None task/agent The runner's atomic steps dereference task.id / agent.id with no None-check, so a verb invoked when the task or agent could not be resolved crashed with a cryptic "'NoneType' object has no attribute 'id'" (observed on i_will_plan for a task forced into an unexpected state out-of-band). Fail fast at run_intent's entry with an actionable INVALID_STATE error instead. * fix(git): reset the dev workspace before a fresh-claim branch checkout A developer's persistent per-dev clone is shared across tasks, so a finished or abandoned prior task can leave it dirty and on a sibling branch. create_branch's checkouts then fail on the dirty tree — and because this git work runs as a side-effect AFTER the claim's DB transition commits, the task is left marked assigned while the workspace stays on the wrong branch, so the dev's next commit is rejected BRANCH_MISMATCH (stalling then blocking the task). reset --hard the tree before the base/feature checkouts. This runs only on a fresh claim (resume short-circuits in _dev_reentry), so discarded changes are abandoned cruft from a finished task — never commits (reset --hard keeps HEAD), never the gitignored .venv. The branch-preservation test invariant is refined to its real intent: a work-carrying branch must never be RE-POINTED (reset --hard <base>); a bare tree-clean reset is allowed. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -53,6 +53,25 @@ async def test_runner_runs_composed_actions_in_order() -> None:
|
||||
assert final_task.status == "in_progress"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_rejects_none_task_or_agent() -> None:
|
||||
"""A None task/agent fails loud with a clean error, not a NoneType crash.
|
||||
|
||||
The atomic handlers dereference task.id / agent.id; without the guard a
|
||||
missing one crashes with "'NoneType' object has no attribute 'id'" (observed
|
||||
when a task was forced into an unexpected state out-of-band).
|
||||
"""
|
||||
runner = VerbRunner(task_service=AsyncMock(), git_service=AsyncMock())
|
||||
ctx = spec.Context(plan="p")
|
||||
agent = MagicMock(id=uuid4(), role="cell_pm")
|
||||
task = MagicMock(id=uuid4(), status="in_progress")
|
||||
|
||||
with pytest.raises(ValueError, match="INVALID_STATE"):
|
||||
await runner.run_intent("i_will_plan", None, agent, ctx)
|
||||
with pytest.raises(ValueError, match="INVALID_STATE"):
|
||||
await runner.run_intent("i_will_plan", task, None, ctx)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_runs_side_effects_after_db_commit() -> None:
|
||||
"""For open_pr: composes is empty; side_effects (push_branch, create_pr) run."""
|
||||
|
||||
Reference in New Issue
Block a user