mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Board Program LEARN context, ruff 0.16, and verb-rejection observability (#700)
* fix(board): LEARN decisions name the item, not its per-cycle index A cycle's reject reasons are rendered into the NEXT cycle's exploration prompt, but the ref recorded alongside each reason was the item's stored id (item-0/item-1) — a per-cycle index that means something different every cycle and appears nowhere the explorer can resolve. The reason survived the loop; what it was about did not. Record the item's title instead, via a shared learn_ref() helper (falls back to the id when title-less, and reads target_task_title for Scales, whose items name the live task they mutate). * chore(lint): satisfy ruff 0.16 — keyword-only signatures and markdown formatting The dev toolchain resolved ruff 0.16.0, which stabilises PLR0917 (too many positional arguments) and formats python code blocks inside markdown. Both fired repo-wide and neither had anything to do with the code they flagged. - 36 signatures gain a `*` so their tail arguments are keyword-only, and the 104 call sites that passed them positionally are converted. mypy was the safety net for the static ones; the full suite caught nine more that only bind at runtime (the MCP tool functions, whose real callers already pass named JSON arguments). - 28 markdown files reformatted by 0.16's code-block formatter. - One RUF036 (`None` mid-union) autofixed in the GitLab provider. * fix(gateway): log the reason when a verb rejects A rejected envelope rides an HTTP 200, its body is never logged, and there is no trace table — so in the access log a verb an agent could not satisfy looks identical to one that worked. On 2026-07-25 four Board Programs (Periscope, Sentinel, Scales, Barfly) each POSTed their propose verb three or four times, persisted nothing, and left their exploration tasks PENDING; the reason was unrecoverable afterwards, from the logs or from the agents' own transcripts. Log error/message/remediate/missing plus the calling agent at envelope_to_response — the one chokepoint every v1 flow and do route returns through. Success envelopes stay silent. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -127,7 +127,7 @@ async def test_i_will_work_on_matches_spec(
|
||||
# Per-role claim authority (CLAIM_RULES) is now enforced inside
|
||||
# spec.can_invoke_action when action == "claim", dispatched by
|
||||
# can_invoke_intent. The verb's single gate is can_invoke_intent.
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="my plan")
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, plan="my plan")
|
||||
body = env.as_dict()
|
||||
if expected.allowed:
|
||||
# Verb may still fail downstream of the gate (e.g. claim() returns
|
||||
|
||||
@@ -352,8 +352,8 @@ async def test_dev_can_claim_pending_task_via_gateway(
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -401,8 +401,8 @@ async def test_dev_full_chain_through_awaiting_qa(
|
||||
|
||||
# 1. Claim
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -480,8 +480,8 @@ async def test_full_chain_through_doc_handoff(
|
||||
|
||||
# Drive the dev side first (same as test_dev_full_chain_through_awaiting_qa).
|
||||
await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -420,8 +420,8 @@ async def test_dev_full_chain_through_awaiting_qa(
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -906,8 +906,8 @@ async def test_block_then_unblock_restore(
|
||||
|
||||
# Drive into in_progress via the real claim+start sequence.
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -969,8 +969,8 @@ async def test_pause_then_resume(
|
||||
c = _build_choreographer(db_session, task, task_service)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -723,8 +723,8 @@ async def test_i_am_done_full_chain_blocks_then_resolves(
|
||||
|
||||
# --- Round 1: no findings exist yet — i_am_done must pass untouched. ---
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -784,8 +784,8 @@ async def test_i_am_done_full_chain_blocks_then_resolves(
|
||||
# re-enforces the rich-plan gate for a developer — so the full plan is
|
||||
# supplied again here, same as the very first claim. ---
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -57,13 +57,7 @@ class _MockChoreographer:
|
||||
next="claim it",
|
||||
)
|
||||
|
||||
async def i_will_work_on(
|
||||
self,
|
||||
_agent_id: object,
|
||||
_task_id: object,
|
||||
_plan: object = None,
|
||||
**_kwargs: object,
|
||||
) -> Envelope:
|
||||
async def i_will_work_on(self, **_kwargs: object) -> Envelope:
|
||||
self._state["task_status"] = "in_progress"
|
||||
return Envelope.ok(
|
||||
status="in_progress",
|
||||
|
||||
@@ -77,9 +77,9 @@ async def test_i_will_work_on_dispatches_task_id() -> None:
|
||||
assert body["status"] == "in_progress"
|
||||
mock_chore.i_will_work_on.assert_awaited_once()
|
||||
call_args = mock_chore.i_will_work_on.call_args
|
||||
# second positional arg is task_id (UUID), third is plan
|
||||
assert str(call_args.args[1]) == _TASK_ID
|
||||
assert call_args.args[2] == "implement the feature"
|
||||
# the verb's params are keyword-only past agent_id, so read the kwargs
|
||||
assert str(call_args.kwargs["task_id"]) == _TASK_ID
|
||||
assert call_args.kwargs["plan"] == "implement the feature"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""A rejected envelope must leave a server-side trace.
|
||||
|
||||
An error envelope rides a 200, so the access log cannot distinguish a verb an
|
||||
agent could not satisfy from one that worked. Four Board Programs died that
|
||||
way on 2026-07-25 with no recoverable reason.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from roboco.api.routes.v1._role_dep import envelope_to_response
|
||||
from roboco.services.gateway.envelope import Envelope
|
||||
from structlog.testing import capture_logs
|
||||
|
||||
|
||||
def _request(path: str) -> MagicMock:
|
||||
request = MagicMock()
|
||||
request.url.path = path
|
||||
request.state.correlation_id = "cid-1"
|
||||
request.headers = {"X-Agent-ID": "agent-7", "X-Agent-Role": "head_marketing"}
|
||||
return request
|
||||
|
||||
|
||||
def _captured(env: Envelope, path: str) -> list[Any]:
|
||||
"""``capture_logs`` rather than a hand-rolled processor swap — it is
|
||||
independent of whatever global structlog config the rest of the suite has
|
||||
already installed, which made the swap pass alone and fail in-suite."""
|
||||
with capture_logs() as entries:
|
||||
envelope_to_response(env=env, request=_request(path))
|
||||
return [e for e in entries if e.get("event") == "verb rejected"]
|
||||
|
||||
|
||||
def test_rejection_logs_reason_and_remediation() -> None:
|
||||
env = Envelope.invalid_state(
|
||||
message="finding 0 is missing 'source_url' — an uncited market claim is noise",
|
||||
remediate="provide the http(s) source URL finding 0's claim came from",
|
||||
context_briefing={},
|
||||
)
|
||||
entries = _captured(env, "/api/v1/do/propose_market_brief")
|
||||
|
||||
assert len(entries) == 1, entries
|
||||
logged = entries[0]
|
||||
assert logged["verb"] == "propose_market_brief"
|
||||
assert logged["error"]
|
||||
assert "source_url" in str(logged["detail"])
|
||||
assert "http(s) source URL" in str(logged["remediate"])
|
||||
assert logged["agent_id"] == "agent-7"
|
||||
assert logged["agent_role"] == "head_marketing"
|
||||
|
||||
|
||||
def test_success_envelope_logs_nothing() -> None:
|
||||
env = Envelope.ok(
|
||||
status="market_brief_proposed",
|
||||
task_id="t-1",
|
||||
next="i_am_idle()",
|
||||
context_briefing={},
|
||||
)
|
||||
assert _captured(env, "/api/v1/do/propose_market_brief") == []
|
||||
@@ -68,8 +68,8 @@ async def test_off_mode_ceo_header_spoof_still_works(
|
||||
new=AsyncMock(return_value=(aid, "ceo")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=CEO_AGENT_ID,
|
||||
x_agent_role="ceo",
|
||||
)
|
||||
@@ -89,8 +89,8 @@ async def test_off_mode_developer_header_trust_unchanged(
|
||||
new=AsyncMock(return_value=(aid, "be-dev-1")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role="developer",
|
||||
x_agent_team="backend",
|
||||
@@ -111,8 +111,8 @@ async def test_on_mode_spoofed_ceo_header_without_token_or_session_401(
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=CEO_AGENT_ID,
|
||||
x_agent_role="ceo",
|
||||
)
|
||||
@@ -127,7 +127,7 @@ async def test_on_mode_no_headers_no_cookie_401(
|
||||
401s without a valid session — never silently falls back to anything."""
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(MagicMock(), MagicMock())
|
||||
await get_agent_context(db=MagicMock(), response=MagicMock())
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@@ -154,9 +154,7 @@ async def test_on_mode_valid_session_yields_ceo_context_and_slides_cookie(
|
||||
response = Response()
|
||||
|
||||
ctx = await get_agent_context(
|
||||
db_session,
|
||||
response,
|
||||
roboco_session=token,
|
||||
db=db_session, response=response, roboco_session=token
|
||||
)
|
||||
|
||||
assert ctx.role == AgentRole.CEO
|
||||
@@ -174,7 +172,9 @@ async def test_on_mode_invalid_session_cookie_401(
|
||||
) -> None:
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(db_session, Response(), roboco_session="not-a-real-jwt")
|
||||
await get_agent_context(
|
||||
db=db_session, response=Response(), roboco_session="not-a-real-jwt"
|
||||
)
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@@ -192,8 +192,8 @@ async def test_on_mode_agent_hmac_still_works(
|
||||
new=AsyncMock(return_value=(aid, "be-dev-1")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=str(aid),
|
||||
x_agent_role="developer",
|
||||
x_agent_team="backend",
|
||||
@@ -212,8 +212,8 @@ async def test_on_mode_system_self_patch_still_works(
|
||||
monkeypatch.setenv("ROBOCO_AGENT_AUTH_SECRET", _SECRET)
|
||||
token = issue_agent_token(_SYSTEM_AGENT_ID, "system", "")
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=_SYSTEM_AGENT_ID,
|
||||
x_agent_role="system",
|
||||
x_agent_token=token,
|
||||
@@ -228,8 +228,8 @@ async def test_on_mode_forged_agent_token_401(monkeypatch: pytest.MonkeyPatch) -
|
||||
monkeypatch.setenv("ROBOCO_AGENT_AUTH_SECRET", _SECRET)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role="developer",
|
||||
x_agent_token="forged",
|
||||
@@ -248,8 +248,8 @@ async def test_on_mode_spoofed_non_ceo_role_without_token_401(
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="main-pm",
|
||||
x_agent_role="main_pm",
|
||||
)
|
||||
|
||||
@@ -228,7 +228,7 @@ def test_do_server_attaches_correlation_id_header(
|
||||
) -> None:
|
||||
fake = _fake_client({"status": "noted"})
|
||||
with patch("httpx.Client", return_value=fake):
|
||||
do_module.note("hi")
|
||||
do_module.note(text="hi")
|
||||
_args, kwargs = fake.post.call_args
|
||||
headers = kwargs["headers"]
|
||||
assert headers["X-Agent-ID"] == "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
+16
-10
@@ -113,7 +113,9 @@ def test_get_orchestrator_raises_503_when_unset() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_agent_id_raises_when_header_missing() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_current_agent_id(MagicMock(), MagicMock(), x_agent_id=None)
|
||||
await get_current_agent_id(
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id=None
|
||||
)
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@@ -125,7 +127,7 @@ async def test_get_current_agent_id_returns_uuid() -> None:
|
||||
new=AsyncMock(return_value=expected),
|
||||
):
|
||||
out = await get_current_agent_id(
|
||||
MagicMock(), MagicMock(), x_agent_id="be-dev-1"
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id="be-dev-1"
|
||||
)
|
||||
assert out == expected
|
||||
|
||||
@@ -133,13 +135,17 @@ async def test_get_current_agent_id_returns_uuid() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_agent_slug_raises_when_header_missing() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_current_agent_slug(MagicMock(), MagicMock(), x_agent_id=None)
|
||||
await get_current_agent_slug(
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id=None
|
||||
)
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_agent_slug_returns_header() -> None:
|
||||
out = await get_current_agent_slug(MagicMock(), MagicMock(), x_agent_id="be-dev-1")
|
||||
out = await get_current_agent_slug(
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id="be-dev-1"
|
||||
)
|
||||
assert out == "be-dev-1"
|
||||
|
||||
|
||||
@@ -494,8 +500,8 @@ def test_coerce_agent_team_empty_returns_none() -> None:
|
||||
async def test_get_agent_context_missing_id_raises() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=None,
|
||||
x_agent_role="developer",
|
||||
)
|
||||
@@ -506,8 +512,8 @@ async def test_get_agent_context_missing_id_raises() -> None:
|
||||
async def test_get_agent_context_missing_role_raises() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role=None,
|
||||
)
|
||||
@@ -523,8 +529,8 @@ async def test_get_agent_context_happy_path(monkeypatch: pytest.MonkeyPatch) ->
|
||||
new=AsyncMock(return_value=(aid, "be-dev-1")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role="developer",
|
||||
x_agent_team="backend",
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_pm_cannot_execute_code_writes_audit_row() -> None:
|
||||
deps = _make_deps(task=task_svc, audit=audit_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(aid, tid, plan="x")
|
||||
env = await c.i_will_work_on(agent_id=aid, task_id=tid, plan="x")
|
||||
|
||||
assert env.error == "not_authorized"
|
||||
audit_svc.log_event.assert_awaited()
|
||||
@@ -215,7 +215,7 @@ async def test_rejection_remediate_lands_in_audit_details() -> None:
|
||||
deps = _make_deps(task=task_svc, audit=audit_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(aid, tid, plan="x")
|
||||
env = await c.i_will_work_on(agent_id=aid, task_id=tid, plan="x")
|
||||
|
||||
assert env.error == "not_authorized"
|
||||
assert env.remediate
|
||||
|
||||
@@ -239,7 +239,9 @@ async def test_i_will_work_on_refuses_at_cap() -> None:
|
||||
task_svc.project_month_spend_usd = AsyncMock(return_value=999.0)
|
||||
c = Choreographer(_make_deps(task_svc))
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state", body
|
||||
assert "10.00" in body["message"] and "999.00" in body["message"]
|
||||
|
||||
@@ -131,7 +131,9 @@ async def test_i_will_work_on_blocks_when_agent_has_in_progress_task() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, target_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=target_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
assert str(other_id) in body["message"] or str(other_id) in body["remediate"]
|
||||
@@ -165,7 +167,7 @@ async def test_i_will_work_on_resumption_does_not_self_block() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, steps=_STEPS)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, steps=_STEPS)
|
||||
assert env.error is None
|
||||
task_svc.start.assert_awaited_once_with(task_id, agent_id)
|
||||
|
||||
@@ -195,7 +197,9 @@ async def test_i_will_work_on_blocks_when_agent_has_paused_task() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, target_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=target_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
assert str(paused_id) in body["remediate"]
|
||||
@@ -226,7 +230,9 @@ async def test_cell_pm_cannot_claim_code_task_via_i_will_work_on() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(pm_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=pm_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_authorized"
|
||||
# Spec produces "role 'cell_pm' may not call 'i_will_work_on'".
|
||||
@@ -253,7 +259,9 @@ async def test_main_pm_cannot_claim_code_task_via_i_will_work_on() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(pm_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=pm_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_authorized"
|
||||
|
||||
@@ -403,7 +411,7 @@ async def test_developer_cannot_claim_qa_status_task() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(dev_id, task_id, steps=_STEPS)
|
||||
env = await c.i_will_work_on(agent_id=dev_id, task_id=task_id, steps=_STEPS)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_authorized"
|
||||
assert "developer" in body["message"]
|
||||
@@ -494,7 +502,9 @@ async def test_non_developer_role_cannot_claim_via_i_will_work_on() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(doc_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=doc_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
# Role-typed claim refuses with not_authorized
|
||||
assert body["error"] == "not_authorized"
|
||||
|
||||
@@ -149,8 +149,8 @@ async def test_dev_claim_acquires_lock_before_guard_read() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -167,8 +167,8 @@ async def test_i_will_work_on_pending_with_plan() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -213,7 +213,7 @@ async def test_i_will_work_on_pending_no_plan_returns_tracing_gap() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan=None)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, plan=None)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "tracing_gap"
|
||||
assert "plan" in body["missing"]
|
||||
@@ -263,8 +263,8 @@ async def test_i_will_work_on_needs_revision_re_starts() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -283,7 +283,7 @@ async def test_i_will_work_on_task_not_found_returns_not_found() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_found"
|
||||
|
||||
@@ -316,7 +316,7 @@ async def test_i_will_work_on_invalid_state_returns_invalid_state() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
# Spec produces "task is in 'completed', 'claim' requires: ..."
|
||||
@@ -376,8 +376,8 @@ async def test_i_will_work_on_blocks_when_journal_note_at_claim_missing() -> Non
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -173,8 +173,8 @@ async def test_i_will_work_on_pending_claim_raises_returns_invalid_state() -> No
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -196,8 +196,8 @@ async def test_i_will_work_on_pending_claim_returns_none_invalid_state() -> None
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -224,7 +224,9 @@ async def test_i_will_work_on_pending_no_plan_tracing_gap() -> None:
|
||||
task_svc.claim.return_value = claimed_task
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan=None, steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan=None, steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "tracing_gap"
|
||||
|
||||
@@ -249,8 +251,8 @@ async def test_i_will_work_on_start_returns_none_invalid_state() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -279,8 +281,8 @@ async def test_needs_revision_branch_claim_fails_invalid_state() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -302,8 +304,8 @@ async def test_needs_revision_branch_start_fails() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -329,7 +331,9 @@ async def test_claimed_branch_returns_start_failed() -> None:
|
||||
task_svc.start.return_value = None
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="ok", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="ok", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
|
||||
@@ -350,7 +354,9 @@ async def test_i_will_work_on_in_progress_assigned_to_self_idempotent() -> None:
|
||||
task_svc.heartbeat = AsyncMock()
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="ok", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="ok", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
# No error — re-entry pass.
|
||||
assert "error" not in body or body.get("error") is None
|
||||
@@ -1095,7 +1101,9 @@ async def test_claimed_branch_already_active_guard() -> None:
|
||||
task_svc.list_in_progress_for_agent.return_value = [in_prog]
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="ok", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="ok", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
|
||||
@@ -1231,8 +1239,8 @@ async def test_i_will_work_on_envelope_carries_introspection_on_success() -> Non
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -1258,7 +1266,9 @@ async def test_i_will_work_on_envelope_carries_introspection_on_rejection() -> N
|
||||
task_svc = _wire_dev_task_svc(task_id, status="completed", assigned_to=agent_id)
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
assert body["current_state"] == "completed"
|
||||
@@ -1319,7 +1329,9 @@ async def test_i_will_work_on_missing_plan_does_not_claim_pending_task() -> None
|
||||
task_svc = _wire_dev_task_svc(task_id, status="pending")
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan=None, steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan=None, steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "tracing_gap"
|
||||
assert "plan" in body["missing"]
|
||||
@@ -1354,7 +1366,9 @@ async def test_i_will_work_on_claimed_with_no_plan_accepts_recovery_plan() -> No
|
||||
task_svc.start.return_value = started
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="recovery plan", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="recovery plan", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] is None, f"expected success, got {body}"
|
||||
task_svc.set_plan.assert_awaited_once()
|
||||
|
||||
@@ -148,8 +148,8 @@ async def test_i_will_work_on_pending_calls_claim_with_task_id_first() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -207,8 +207,8 @@ async def test_i_will_work_on_needs_revision_calls_start_with_task_id_first() ->
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -261,7 +261,7 @@ async def test_i_will_work_on_claimed_resumption_calls_start_with_task_id_first(
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, steps=_STEPS)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, steps=_STEPS)
|
||||
|
||||
task_svc.start.assert_awaited_once_with(task_id, agent_id)
|
||||
assert env.error is None
|
||||
|
||||
@@ -115,7 +115,7 @@ async def test_dev_fresh_claim_without_steps_is_rejected() -> None:
|
||||
task_id = uuid4()
|
||||
c = Choreographer(_make_deps(task=_dev_task_svc(task_id)))
|
||||
|
||||
env = await c.i_will_work_on(dev_id, task_id, plan="do the thing")
|
||||
env = await c.i_will_work_on(agent_id=dev_id, task_id=task_id, plan="do the thing")
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "incomplete_input", body
|
||||
assert "steps" in (body.get("missing") or []), body
|
||||
@@ -128,8 +128,8 @@ async def test_dev_thin_step_description_is_rejected() -> None:
|
||||
c = Choreographer(_make_deps(task=_dev_task_svc(task_id)))
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_id,
|
||||
task_id,
|
||||
agent_id=dev_id,
|
||||
task_id=task_id,
|
||||
plan="do the thing",
|
||||
steps=[{"title": "Edit README", "description": "edit it"}],
|
||||
)
|
||||
@@ -162,7 +162,9 @@ async def test_dev_with_substantive_steps_passes_gate_and_persists_checklist() -
|
||||
{"title": "Edit README", "description": _GOOD_STEP_DESC},
|
||||
{"title": "Commit + open PR", "description": _GOOD_STEP_DESC},
|
||||
]
|
||||
env = await c.i_will_work_on(dev_id, task_id, **_full_plan_kwargs(steps_in))
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=dev_id, task_id=task_id, **_full_plan_kwargs(steps_in)
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body.get("error") != "incomplete_input", body
|
||||
# The full rich plan was layered into the panel-shaped dict and persisted,
|
||||
@@ -188,8 +190,8 @@ async def test_dev_fresh_claim_missing_considerations_and_risks_rejected() -> No
|
||||
c = Choreographer(_make_deps(task=_dev_task_svc(task_id)))
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_id,
|
||||
task_id,
|
||||
agent_id=dev_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=[{"title": "Edit README", "description": _GOOD_STEP_DESC}],
|
||||
)
|
||||
@@ -210,7 +212,9 @@ async def test_dev_reentry_in_progress_short_circuits_before_steps_gate() -> Non
|
||||
svc.get.return_value.assigned_to = dev_id
|
||||
c = Choreographer(_make_deps(task=svc))
|
||||
|
||||
env = await c.i_will_work_on(dev_id, task_id, plan="resume: keep going")
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=dev_id, task_id=task_id, plan="resume: keep going"
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body.get("error") is None, body
|
||||
assert body.get("status") == "in_progress", body
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_heartbeat_fires_on_rejection_not_authorized() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(aid, tid, plan="x")
|
||||
env = await c.i_will_work_on(agent_id=aid, task_id=tid, plan="x")
|
||||
|
||||
assert env.error == "not_authorized"
|
||||
task_svc.heartbeat.assert_awaited_with(tid)
|
||||
|
||||
@@ -86,7 +86,7 @@ async def test_i_will_work_on_calls_heartbeat() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
await c.i_will_work_on(aid, tid, plan="go")
|
||||
await c.i_will_work_on(agent_id=aid, task_id=tid, plan="go")
|
||||
|
||||
task_svc.heartbeat.assert_awaited_with(tid)
|
||||
|
||||
|
||||
@@ -67,7 +67,12 @@ async def test_superseded_closes_pr_and_completes_without_merge(
|
||||
)
|
||||
|
||||
env = await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/frontend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/frontend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
git.close_pull_request.assert_awaited_once()
|
||||
@@ -100,7 +105,12 @@ async def test_rebased_retries_merge_and_completes(
|
||||
)
|
||||
|
||||
await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
git.pr_merge.assert_awaited_once()
|
||||
@@ -133,7 +143,12 @@ async def test_genuine_conflict_escalates_to_ceo_and_does_not_loop(
|
||||
)
|
||||
|
||||
env = await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), tid, t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=tid,
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
task.admin_set_status.assert_awaited_once()
|
||||
@@ -171,7 +186,12 @@ async def test_diverged_rebase_outcome_escalates_rather_than_completing(
|
||||
)
|
||||
|
||||
await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
task.admin_set_status.assert_awaited_once()
|
||||
@@ -198,7 +218,12 @@ async def test_unknown_rebase_outcome_escalates_rather_than_completing(
|
||||
)
|
||||
|
||||
await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
task.admin_set_status.assert_awaited_once()
|
||||
|
||||
@@ -148,8 +148,8 @@ async def test_i_will_work_on_calls_ensure_work_session() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -290,8 +290,8 @@ async def test_ensure_work_session_not_called_when_start_fails() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_note_default_scope_note(do_module: Any) -> None:
|
||||
fake_client.post.return_value = fake_response
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
do_module.note("hello world")
|
||||
do_module.note(text="hello world")
|
||||
|
||||
_args, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"]["scope"] == "note"
|
||||
@@ -88,7 +88,7 @@ def test_note_with_scope_reflect(do_module: Any) -> None:
|
||||
fake_client.post.return_value = fake_response
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
do_module.note("did x", scope="reflect")
|
||||
do_module.note(text="did x", scope="reflect")
|
||||
|
||||
_args, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"]["scope"] == "reflect"
|
||||
|
||||
@@ -105,7 +105,7 @@ def test_flow_post_returns_envelope_on_400(flow_module: types.ModuleType) -> Non
|
||||
}
|
||||
client = _fake_client_with(400, body)
|
||||
with patch("httpx.Client", return_value=client):
|
||||
result = flow_module.i_will_work_on("task-id", plan="x")
|
||||
result = flow_module.i_will_work_on(task_id="task-id", plan="x")
|
||||
assert result["error"] == "not_authorized"
|
||||
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ def test_i_will_work_on_passes_plan(flow_module: types.ModuleType) -> None:
|
||||
fake_client = _make_fake_client({"status": "in_progress"})
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
flow_module.i_will_work_on("task-uuid", plan="my plan")
|
||||
flow_module.i_will_work_on(task_id="task-uuid", plan="my plan")
|
||||
|
||||
args, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"] == {
|
||||
@@ -210,7 +210,7 @@ def test_i_will_work_on_plan_defaults_to_none(flow_module: types.ModuleType) ->
|
||||
fake_client = _make_fake_client({"status": "in_progress"})
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
flow_module.i_will_work_on("task-uuid")
|
||||
flow_module.i_will_work_on(task_id="task-uuid")
|
||||
|
||||
_, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"] == {
|
||||
@@ -234,7 +234,7 @@ def test_i_will_work_on_passes_steps(flow_module: types.ModuleType) -> None:
|
||||
]
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
flow_module.i_will_work_on("task-uuid", plan="p", steps=steps)
|
||||
flow_module.i_will_work_on(task_id="task-uuid", plan="p", steps=steps)
|
||||
|
||||
_, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"] == {
|
||||
|
||||
@@ -471,6 +471,23 @@ async def test_prior_cycle_context_renders_rejections_with_reasons(
|
||||
assert "item-2 — too risky" in context
|
||||
|
||||
|
||||
def test_learn_ref_names_the_item_not_its_per_cycle_index() -> None:
|
||||
"""The ref reaches the next cycle's prompt, so it must say WHAT was
|
||||
decided — ``item-1`` means something different in every cycle."""
|
||||
assert bp_module.learn_ref({"id": "item-1", "title": "Fix the CLA gate"}) == (
|
||||
"Fix the CLA gate"
|
||||
)
|
||||
# Scales items name the live task they mutate instead of a draft title.
|
||||
assert bp_module.learn_ref(
|
||||
{"id": "item-0", "target_task_title": "Panel UX wave"}
|
||||
) == ("Panel UX wave")
|
||||
# A title-less item degrades to the old behaviour rather than an empty ref.
|
||||
assert bp_module.learn_ref({"id": "item-2"}) == "item-2"
|
||||
assert bp_module.learn_ref({"id": "item-3", "title": " "}) == "item-3"
|
||||
cap = 80
|
||||
assert len(bp_module.learn_ref({"id": "item-4", "title": "x" * 200})) == cap
|
||||
|
||||
|
||||
def test_originators_cover_exactly_the_registry() -> None:
|
||||
assert set(bp_module._ORIGINATORS) == set(PROGRAMS)
|
||||
|
||||
|
||||
@@ -418,8 +418,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -442,8 +445,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -421,8 +421,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -445,8 +448,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -415,8 +415,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -439,8 +442,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -413,8 +413,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -437,8 +440,10 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# Title, not index — the reject reason is only useful to the next cycle
|
||||
# if it says which proposal it was about.
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -528,8 +528,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Stale task",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -553,8 +556,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Stale task",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -418,8 +418,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -442,8 +445,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
Reference in New Issue
Block a user