mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* fix(gateway): gate review diffs against the task's real parent branch (#444) The in-path PR-review gate's evidence diff (claim_gate_review) and the pr_pass conventions guard derived their diff base via parent_branch_for string surgery, which reuses the child branch's own team segment — wrong for every cross-team hop (a frontend child of a main_pm root derives a ref that never existed) and silently falls back to the repo default branch, so the reviewer judged the entire inherited base-branch content as the task's own work and failed acceptance criteria the task never touched. Bounced a live goals-tab fix three times, unfixable by branch surgery. The gate now resolves the base via resolve_parent_branch (the parent task's recorded branch_name, cross-team correct) and threads it as a new preferred_parent override through git.diff / list_changed_files / conventions_check_for_task — consulted only when no explicit base is given, so the pinned literal-base contract (base="HEAD~1") and every other diff caller (QA, doc, content) are byte-identical. Parent lookup fails open (derived-base fallback) like the other resolve_parent_branch call sites, and is skipped entirely while the conventions flag is off. Also excludes .uv-cache/ and .claude/ (agent worktrees, private uv cache) from the markdown prose scanner — both are repo-local tool dirs whose vendored/generated files tripped make reflow-check. Co-authored-by: Renn F <rennf93@users.noreply.github.com> * feat(vault): Obsidian vault V1 — projection core + input loop The vault is a rebuildable projection of the DB (never a source of truth), default-off behind ROBOCO_OBSIDIAN_VAULT_ENABLED + ROBOCO_VAULT_PATH. Projection core: VaultWriter materializes tasks/journals/A2A/agents as wikilinked markdown (id-suffixed stable filenames, alias-based links so renames never break, is_private journals excluded like the RAG corpus); event seams materialize on journal write and A2A send and touch task-note frontmatter at the status-transition chokepoint — all best-effort, a vault failure never blocks a verb. Shipped .obsidian config (Dataview, Kanban, team/status graph groups) + _meta dashboards; python -m roboco.vault rebuild/relocate (rebuild preserves the narrative section). Auditor narrative duty: curate_vault content verb (auditor-only, playbook-curation pattern) spawned by a dedicated root-completion hook with its own cooldown — fully separate from _dispatch_audit_work, whose scheduled-sweep/alert-producer revival belongs to the queued fleet task. Input loop: VaultIntakeEngine (ROBOCO_VAULT_INTAKE_*) watches the intake folder for #roboco-tagged notes and materializes each as ONE held draft (confirmed_by_human=False, Secretary-owned, source=vault_note, excluded by the dispatchers via _is_held_ceo_source) with local-model extraction and a deterministic fallback; vault_seen_notes ledger (migration 070) keyed on path+content-hash (the CEO-feedback callout is stripped before hashing so the engine's own append never self-triggers); per-cycle and open-draft caps. Nothing auto-starts. * fix(vault): integrate with master — re-chain migration 070 onto 069, mypy-clean tests The vault branch was cut from slave before the sequence-gate promotion, so migration 070 chained from 068 while master already carried 069 — two heads on merge. Master is now merged in and 070 revises 069. Vault test files also get mypy-clean mock idioms (monkeypatch.setattr over method assignment; await_args narrowed before access). * fix(scripts): dedupe SKIP_DIRS again after the master merge Master still carries the twin-merge duplicate (its dedupe hotfix is an unmerged PR); the merge re-imported it here. * fix(vault): reflow hard-wrapped prose in the vault asset templates * chore(config): exclude .uv-cache and .claude from deptry's scan scope Same repo-local tool dirs the prose scanner skips; the standalone deptry target walks the repo root and drowned in the cache's unpacked wheels. * fix(vault): board-review activation path for vault drafts + relocate graft The input loop's held-artifact posture was a dead end: vault_note drafts were unconditionally held by _is_held_ceo_source, owned by the verbless Secretary, hidden from the panel's approval surfaces by team, and the open-drafts cap counted them forever — the engine self-bricked after ten notes. Vault drafts now ride the intake board-review path instead: a tagged note becomes a PENDING Product-Owner-assigned Board draft (the exact confirm_live_draft board shape), the board reviews it, and only the CEO's approve_and_start makes it deliverable — never-auto-starts now rests on the board gate, proven by tests against the real dispatchers. The cap counts only drafts still awaiting the CEO (team==BOARD, non-terminal), so approval and cancellation both free it. relocate into an existing personal vault now grafts old_root/RoboCo as a direct child (refusing loudly if RoboCo/ already exists there) and adds only absent .obsidian/_meta files — a personal vault's config is never clobbered. An absent destination keeps the whole-tree move. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
124 lines
4.0 KiB
Python
124 lines
4.0 KiB
Python
"""curate_vault content verb — role grant + ContentActions RBAC + flag gating.
|
|
|
|
Mirrors test_playbook_verbs.py's shape: only the Auditor curates; a
|
|
delivery role is refused; the flag off short-circuits before any write.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from roboco.config import settings
|
|
from roboco.services.gateway.content_actions import ContentActions, ContentActionsDeps
|
|
from roboco.services.gateway.role_config import get_role_config
|
|
|
|
|
|
def _actions(role: str) -> ContentActions:
|
|
task = MagicMock()
|
|
agent = MagicMock()
|
|
agent.role = role
|
|
task.agent_for = AsyncMock(return_value=agent)
|
|
task.session = AsyncMock()
|
|
deps = ContentActionsDeps(
|
|
task=task,
|
|
git=MagicMock(),
|
|
a2a=MagicMock(),
|
|
journal=MagicMock(),
|
|
workspace=MagicMock(),
|
|
notifications=MagicMock(),
|
|
)
|
|
return ContentActions(deps)
|
|
|
|
|
|
def test_auditor_can_curate_vault() -> None:
|
|
assert "curate_vault" in get_role_config("auditor").do_tools
|
|
|
|
|
|
def test_developer_cannot_curate_vault() -> None:
|
|
assert "curate_vault" not in get_role_config("developer").do_tools
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_curate_vault_forbidden_for_developer(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(settings, "obsidian_vault_enabled", True)
|
|
env = await _actions("developer").curate_vault(
|
|
agent_id=uuid4(), task_id=uuid4(), narrative="did stuff"
|
|
)
|
|
assert env.error == "not_authorized"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_curate_vault_disabled_when_flag_off(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(settings, "obsidian_vault_enabled", False)
|
|
env = await _actions("auditor").curate_vault(
|
|
agent_id=uuid4(), task_id=uuid4(), narrative="did stuff"
|
|
)
|
|
assert env.error == "invalid_state"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_curate_vault_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(settings, "obsidian_vault_enabled", True)
|
|
actions = _actions("auditor")
|
|
actions.task.get = AsyncMock(return_value=None)
|
|
env = await actions.curate_vault(
|
|
agent_id=uuid4(), task_id=uuid4(), narrative="did stuff"
|
|
)
|
|
assert env.error == "not_found"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_curate_vault_writes_narrative_for_auditor(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(settings, "obsidian_vault_enabled", True)
|
|
actions = _actions("auditor")
|
|
task_id = uuid4()
|
|
task = MagicMock()
|
|
actions.task.get = AsyncMock(return_value=task)
|
|
writer = MagicMock()
|
|
data = MagicMock()
|
|
with (
|
|
patch(
|
|
"roboco.services.vault_assembly.assemble_task_note_data",
|
|
AsyncMock(return_value=data),
|
|
) as assemble,
|
|
patch("roboco.services.vault_writer.get_vault_writer", return_value=writer),
|
|
patch("roboco.services.project.get_project_service"),
|
|
):
|
|
env = await actions.curate_vault(
|
|
agent_id=uuid4(), task_id=task_id, narrative="Shipped after one rework."
|
|
)
|
|
assert env.error is None
|
|
assert env.status == "vault_curated"
|
|
assemble.assert_awaited_once()
|
|
assert assemble.await_args is not None
|
|
assert assemble.await_args.kwargs["narrative"] == "Shipped after one rework."
|
|
writer.write_task.assert_called_once_with(data)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_curate_vault_write_failure_returns_invalid_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(settings, "obsidian_vault_enabled", True)
|
|
actions = _actions("auditor")
|
|
actions.task.get = AsyncMock(return_value=MagicMock())
|
|
with (
|
|
patch(
|
|
"roboco.services.vault_assembly.assemble_task_note_data",
|
|
AsyncMock(side_effect=OSError("disk full")),
|
|
),
|
|
patch("roboco.services.project.get_project_service"),
|
|
):
|
|
env = await actions.curate_vault(
|
|
agent_id=uuid4(), task_id=uuid4(), narrative="x"
|
|
)
|
|
assert env.error == "invalid_state"
|