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:
|
||||
# UPDATE existing doc instead of creating new
|
||||
return await self._update_existing_doc(
|
||||
result = await self._update_existing_doc(
|
||||
agent_id=agent_id,
|
||||
existing_path=existing_path,
|
||||
req=req,
|
||||
doc_type=doc_type,
|
||||
)
|
||||
|
||||
else:
|
||||
# 5. No similar doc found - create new
|
||||
return await self._create_new_doc(
|
||||
result = await self._create_new_doc(
|
||||
agent_id=agent_id,
|
||||
team=team,
|
||||
req=req,
|
||||
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(
|
||||
self,
|
||||
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")
|
||||
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