mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(docs): documenter output is committed to the project repo, not just the knowledge store
Documenter docs only ever reached DOCS_BASE_PATH=/app/docs (a host-mounted, RAG-indexed knowledge store) and were never committed to the project's git repository — so the documenter's deliverable never landed in the repo. write_doc now also writes the doc into the agent's workspace clone under docs/<type>/<file> and commits it onto the task branch via GitService.commit, so it rides the existing PR into the repository on merge. Best-effort: a documenter without a cloned workspace or task branch still succeeds (logged, never fatal); the /app/docs knowledge store + RAG indexing are unchanged. Adds tests that the doc is written into the workspace and committed onto the task branch, and that it no-ops cleanly without a branch.
This commit is contained in:
+79
-3
@@ -194,21 +194,97 @@ class DocsService(BaseService):
|
|||||||
|
|
||||||
if existing_path:
|
if existing_path:
|
||||||
# UPDATE existing doc instead of creating new
|
# UPDATE existing doc instead of creating new
|
||||||
return await self._update_existing_doc(
|
result = await self._update_existing_doc(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
existing_path=existing_path,
|
existing_path=existing_path,
|
||||||
req=req,
|
req=req,
|
||||||
doc_type=doc_type,
|
doc_type=doc_type,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
# 5. No similar doc found - create new
|
# 5. No similar doc found - create new
|
||||||
return await self._create_new_doc(
|
result = await self._create_new_doc(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
team=team,
|
team=team,
|
||||||
req=req,
|
req=req,
|
||||||
doc_type=doc_type,
|
doc_type=doc_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 6. Persist the doc into the project's repo and commit it onto the task
|
||||||
|
# branch, so the documenter's output actually lands in the repository via
|
||||||
|
# the open PR — not only in the /app/docs knowledge store. Best-effort: a
|
||||||
|
# documenter without a cloned workspace / task branch still succeeds.
|
||||||
|
await self._commit_doc_to_repo(agent_id, req, doc_type)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _commit_doc_to_repo(
|
||||||
|
self, agent_id: str, req: WriteDocInput, doc_type: str
|
||||||
|
) -> None:
|
||||||
|
"""Write the doc into the project's workspace clone and commit it onto
|
||||||
|
the task branch, so it persists to the repository through the open PR.
|
||||||
|
|
||||||
|
Best-effort: any failure (no task branch yet, workspace not cloned, a git
|
||||||
|
hiccup) is logged and swallowed — it must never fail the doc write.
|
||||||
|
"""
|
||||||
|
from roboco.services.git import get_git_service
|
||||||
|
from roboco.services.project import get_project_service
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(TaskTable).where(TaskTable.id == req.task_id)
|
||||||
|
)
|
||||||
|
task = result.scalar_one_or_none()
|
||||||
|
if task is None or not task.branch_name or task.project_id is None:
|
||||||
|
return
|
||||||
|
project = await get_project_service(self.session).get(
|
||||||
|
UUID(str(task.project_id))
|
||||||
|
)
|
||||||
|
if project is None:
|
||||||
|
return
|
||||||
|
actor = self._agent_uuid(agent_id)
|
||||||
|
if actor is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
git = get_git_service(self.session)
|
||||||
|
workspace = await git.get_workspace(project.slug, actor)
|
||||||
|
subfolder = TYPE_SUBFOLDERS[doc_type]
|
||||||
|
rel_path = (
|
||||||
|
f"docs/{subfolder}/{req.filename}"
|
||||||
|
if subfolder
|
||||||
|
else f"docs/{req.filename}"
|
||||||
|
)
|
||||||
|
await self._write_file(workspace / rel_path, req.content)
|
||||||
|
await git.commit(
|
||||||
|
branch_name=task.branch_name,
|
||||||
|
message=f"docs: {req.title}",
|
||||||
|
task_id=req.task_id,
|
||||||
|
files=[rel_path],
|
||||||
|
actor_agent_id=actor,
|
||||||
|
)
|
||||||
|
self.log.info(
|
||||||
|
"Documentation committed to project repo",
|
||||||
|
agent_id=agent_id,
|
||||||
|
task_id=str(req.task_id),
|
||||||
|
path=rel_path,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self.log.warning(
|
||||||
|
"Could not commit documentation to repo (non-fatal)",
|
||||||
|
agent_id=agent_id,
|
||||||
|
task_id=str(req.task_id),
|
||||||
|
error=str(exc),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _agent_uuid(agent_id: str) -> UUID | None:
|
||||||
|
"""Resolve an agent slug or UUID string to its UUID, or None."""
|
||||||
|
from roboco.seeds.initial_data import AGENT_UUIDS
|
||||||
|
|
||||||
|
try:
|
||||||
|
return UUID(agent_id)
|
||||||
|
except ValueError:
|
||||||
|
raw = AGENT_UUIDS.get(agent_id)
|
||||||
|
return UUID(str(raw)) if raw is not None else None
|
||||||
|
|
||||||
async def _find_similar_doc(
|
async def _find_similar_doc(
|
||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
|
|||||||
@@ -802,3 +802,69 @@ async def test_list_docs_for_team_shallow_path_infers_readme(
|
|||||||
):
|
):
|
||||||
docs = await svc._list_docs_for_team("backend")
|
docs = await svc._list_docs_for_team("backend")
|
||||||
assert any(d.doc_type == "readme" for d in docs)
|
assert any(d.doc_type == "readme" for d in docs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_commit_doc_to_repo_writes_into_workspace_and_commits(
|
||||||
|
docs_setup: dict,
|
||||||
|
db_session: AsyncSession,
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""A doc write also commits the file into the project repo on the task branch."""
|
||||||
|
svc = docs_setup["svc"]
|
||||||
|
task_id = docs_setup["task_id"]
|
||||||
|
agent_uuid = docs_setup["agent_id"]
|
||||||
|
|
||||||
|
task = (
|
||||||
|
await db_session.execute(select(TaskTable).where(TaskTable.id == task_id))
|
||||||
|
).scalar_one()
|
||||||
|
task.branch_name = "feature/backend/ABC12345"
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
mock_git = MagicMock()
|
||||||
|
mock_git.get_workspace = AsyncMock(return_value=tmp_path)
|
||||||
|
mock_git.commit = AsyncMock(return_value={"sha": "deadbeef"})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"roboco.services.git.get_git_service", lambda _session: mock_git
|
||||||
|
)
|
||||||
|
|
||||||
|
req = WriteDocInput(
|
||||||
|
task_id=task_id,
|
||||||
|
filename="guide.md",
|
||||||
|
doc_type="api",
|
||||||
|
title="API Guide",
|
||||||
|
content="# API Guide\n",
|
||||||
|
)
|
||||||
|
await svc._commit_doc_to_repo(str(agent_uuid), req, "api")
|
||||||
|
|
||||||
|
# The doc landed in the workspace repo under docs/...
|
||||||
|
assert (tmp_path / "docs" / "api" / "guide.md").read_text() == "# API Guide\n"
|
||||||
|
# ...and was committed onto the task branch.
|
||||||
|
mock_git.commit.assert_awaited_once()
|
||||||
|
kwargs = mock_git.commit.await_args.kwargs
|
||||||
|
assert kwargs["branch_name"] == "feature/backend/ABC12345"
|
||||||
|
assert kwargs["files"] == ["docs/api/guide.md"]
|
||||||
|
assert kwargs["task_id"] == task_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_commit_doc_to_repo_skips_without_task_branch(
|
||||||
|
docs_setup: dict, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""No task branch yet → best-effort no-op (no git commit, no raise)."""
|
||||||
|
svc = docs_setup["svc"]
|
||||||
|
mock_git = MagicMock()
|
||||||
|
mock_git.commit = AsyncMock()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"roboco.services.git.get_git_service", lambda _session: mock_git
|
||||||
|
)
|
||||||
|
req = WriteDocInput(
|
||||||
|
task_id=docs_setup["task_id"],
|
||||||
|
filename="x.md",
|
||||||
|
doc_type="api",
|
||||||
|
title="X",
|
||||||
|
content="x",
|
||||||
|
)
|
||||||
|
await svc._commit_doc_to_repo(str(docs_setup["agent_id"]), req, "api")
|
||||||
|
mock_git.commit.assert_not_awaited()
|
||||||
|
|||||||
Reference in New Issue
Block a user