Never leave the agent stranded in a working state
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 1m20s
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 1m20s
The first live run found four dead ends one at a time: states where no operator message could move the agent. Most had one shape: work ended, by an exception or by a path that never transitioned, and left the agent in PLANNING_* or EXECUTING, where every message is answered "busy". The worker only prints an exception, so that would keep happening. - Every job the orchestrator runs for an operator message or a Gitea comment now goes through _settled(). If the job leaves a working state behind, whatever the reason, the agent moves to AWAIT_INPUT with the continue / re-plan choice a restart offers, and says why. An exception in a waiting state is reported and the state kept, since its commands still lead on. - A sign-off state whose gate record is missing asks to plan again instead of crashing on every reply. - The stub configurations (no executor, no publisher) complete the milestone and wait in PR_OPEN instead of resting in EXECUTING. test_no_stranding.py drives a whole two-milestone project with a fault injected at each planner, executor and publisher call, and asserts after every message that the agent is not in a working state and that the project still reaches 'project complete'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
1ccea20a10
commit
92e5452161
3 files changed
+325
-10
No files matched your search
@@ -12,6 +12,7 @@ from nisse_dev.control import (
|
||||
EXECUTING,
|
||||
IDLE,
|
||||
PLANNING_ARCH,
|
||||
PR_OPEN,
|
||||
AskHuman,
|
||||
Orchestrator,
|
||||
PlanningError,
|
||||
@@ -52,6 +53,10 @@ class Harness:
|
||||
return [m.text for m in self.transport.outbound]
|
||||
|
||||
|
||||
def executed(h: Harness) -> bool:
|
||||
return any(e["to"] == EXECUTING for e in h.events("state.transition"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def h(tmp_path: Path) -> Harness:
|
||||
return Harness(tmp_path)
|
||||
@@ -74,14 +79,16 @@ def test_use_project_submits_arch_plan_and_opens_gate(h: Harness) -> None:
|
||||
assert opened["task_id"] is not None
|
||||
|
||||
|
||||
def test_full_happy_path_to_executing(h: Harness) -> None:
|
||||
def test_full_happy_path_through_execution(h: Harness) -> None:
|
||||
h.transport.receive("don", "use demo")
|
||||
h.transport.receive("don", "approve")
|
||||
assert h.state == AWAIT_MILESTONE_SIGNOFF
|
||||
h.transport.receive("don", "approve")
|
||||
assert h.state == AWAIT_MILESTONE_SIGNOFF # approved, awaiting start
|
||||
h.transport.receive("don", "start milestone 1")
|
||||
assert h.state == EXECUTING
|
||||
assert executed(h)
|
||||
# The stub completes at once and waits in PR_OPEN, never in EXECUTING.
|
||||
assert h.state == PR_OPEN
|
||||
approvals = h.events("gate.approve")
|
||||
assert [e["gate"] for e in approvals] == ["arch", "milestone-1"]
|
||||
|
||||
@@ -123,7 +130,7 @@ def test_start_milestone_requires_approval_first(h: Harness) -> None:
|
||||
assert h.state == AWAIT_MILESTONE_SIGNOFF # it was rejection feedback
|
||||
h.transport.receive("don", "approve rev-2")
|
||||
h.transport.receive("don", "start milestone 1")
|
||||
assert h.state == EXECUTING
|
||||
assert executed(h)
|
||||
|
||||
|
||||
def test_a_message_is_matched_as_stripped_but_recorded_as_sent(
|
||||
@@ -231,15 +238,16 @@ def test_restart_mid_execution_continue_returns_to_the_approved_plan(
|
||||
h.transport.receive("don", "use demo")
|
||||
h.transport.receive("don", "approve")
|
||||
h.transport.receive("don", "approve")
|
||||
h.transport.receive("don", "start milestone 1")
|
||||
assert h.state == EXECUTING
|
||||
# A crash mid-milestone: the store still says EXECUTING.
|
||||
state = h.store.get_state()
|
||||
h.store.transition(EXECUTING, cause="test", project="demo", data=state.data)
|
||||
h = Harness(tmp_path)
|
||||
assert h.state == AWAIT_INPUT
|
||||
h.transport.receive("don", "continue")
|
||||
assert h.state == AWAIT_MILESTONE_SIGNOFF
|
||||
assert "start milestone 1" in h.outbound_texts()[-1]
|
||||
h.transport.receive("don", "start milestone 1")
|
||||
assert h.state == EXECUTING
|
||||
assert executed(h)
|
||||
|
||||
|
||||
def test_restart_question_without_its_interrupted_state_still_replans(
|
||||
|
||||
Reference in new issue
Block a user