mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(gateway): i_documented auto-records journal:reflect from submission
The reflect gate's first, by-design tracing_gap ('journal:reflect missing')
counts toward the per-verb circuit breaker (i_documented limit 3 / 60s). A
documenter that fumbled note(scope='reflect') even twice got locked out and
went idle, stranding the task in awaiting_documentation with the PR orphaned.
i_documented now synthesizes the required reflect entry from the notes + files
it already carries, but only when the submission clears the notes/files gate
thresholds and the agent did not journal one themselves (theirs is richer and
left untouched). One call, no loop.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""i_documented synthesizes the required journal:reflect from the documenter's
|
||||
own submission so the reflect gate passes in one call.
|
||||
|
||||
Before this, i_documented returned a by-design tracing_gap ("journal:reflect
|
||||
missing") on the first call. That rejection counts toward the per-verb circuit
|
||||
breaker (limit 3 / 60s), so a documenter that fumbled note(scope='reflect')
|
||||
even twice got locked out and went idle, stranding the task in
|
||||
awaiting_documentation. The notes + files i_documented already carries are the
|
||||
reflection's substance, so _ensure_doc_reflect writes the entry from them —
|
||||
but only when the submission is substantive and the agent didn't journal one.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
||||
|
||||
|
||||
def _make_deps(**overrides: Any) -> ChoreographerDeps:
|
||||
base: dict[str, Any] = {
|
||||
"task": AsyncMock(),
|
||||
"work_session": AsyncMock(),
|
||||
"git": AsyncMock(),
|
||||
"a2a": AsyncMock(),
|
||||
"journal": AsyncMock(),
|
||||
"audit": AsyncMock(),
|
||||
"evidence_repo": AsyncMock(),
|
||||
"messaging": AsyncMock(),
|
||||
}
|
||||
base.update(overrides)
|
||||
return ChoreographerDeps(**base)
|
||||
|
||||
|
||||
_ADEQUATE_NOTES = "Documented the auth flow in README and the API reference."
|
||||
_SHORT_NOTES = "done" # below docs_notes_min_chars (20)
|
||||
_FILES = ["README.md", "docs/api.md"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_writes_reflect_from_submission_when_absent() -> None:
|
||||
journal = AsyncMock()
|
||||
journal.has_reflect_for_task.return_value = False
|
||||
c = Choreographer(_make_deps(journal=journal))
|
||||
doc_id, task_id = uuid4(), uuid4()
|
||||
|
||||
await c._ensure_doc_reflect(doc_id, task_id, _ADEQUATE_NOTES, _FILES)
|
||||
|
||||
journal.write_entry.assert_awaited_once()
|
||||
kwargs = journal.write_entry.await_args.kwargs
|
||||
assert kwargs["scope"] == "reflect"
|
||||
assert kwargs["agent_id"] == doc_id
|
||||
assert kwargs["task_id"] == task_id
|
||||
assert _ADEQUATE_NOTES in kwargs["content"]
|
||||
assert "README.md" in kwargs["content"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_when_agent_already_authored_reflect() -> None:
|
||||
journal = AsyncMock()
|
||||
journal.has_reflect_for_task.return_value = True
|
||||
c = Choreographer(_make_deps(journal=journal))
|
||||
|
||||
await c._ensure_doc_reflect(uuid4(), uuid4(), _ADEQUATE_NOTES, _FILES)
|
||||
|
||||
journal.write_entry.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_when_notes_below_threshold() -> None:
|
||||
journal = AsyncMock()
|
||||
journal.has_reflect_for_task.return_value = False
|
||||
c = Choreographer(_make_deps(journal=journal))
|
||||
|
||||
await c._ensure_doc_reflect(uuid4(), uuid4(), _SHORT_NOTES, _FILES)
|
||||
|
||||
journal.write_entry.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_when_no_files() -> None:
|
||||
journal = AsyncMock()
|
||||
journal.has_reflect_for_task.return_value = False
|
||||
c = Choreographer(_make_deps(journal=journal))
|
||||
|
||||
await c._ensure_doc_reflect(uuid4(), uuid4(), _ADEQUATE_NOTES, [])
|
||||
|
||||
journal.write_entry.assert_not_awaited()
|
||||
Reference in New Issue
Block a user