mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Boards gain an optional project_id. When set, the board's default_workdir mirrors the project's primary repo and every new task inherits the project — a deterministic worktree + branch per task — unless it names its own. New GET /projects; board create/patch/list carry project_id + resolved name; the create dialog defaults its workspace to the board's and allows a per-task path override. Desktop: "Board settings…" gains a project picker.
87 lines
3.1 KiB
Python
87 lines
3.1 KiB
Python
"""Board→project scoping in kanban_db.
|
|
|
|
A kanban board can be scoped to a first-class Hermes project so every task on
|
|
it anchors to that project (deterministic worktree + branch). Covers the
|
|
metadata round-trip and the create-time inheritance.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_WORKTREE = Path(__file__).resolve().parents[2]
|
|
if str(_WORKTREE) not in sys.path:
|
|
sys.path.insert(0, str(_WORKTREE))
|
|
|
|
from hermes_cli import kanban_db as kb
|
|
from hermes_cli import projects_db as pdb
|
|
|
|
|
|
@pytest.fixture
|
|
def fresh_home(tmp_path, monkeypatch):
|
|
home = tmp_path / "hermes_home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
for var in ("HERMES_KANBAN_DB", "HERMES_KANBAN_WORKSPACES_ROOT", "HERMES_KANBAN_HOME", "HERMES_KANBAN_BOARD"):
|
|
monkeypatch.delenv(var, raising=False)
|
|
try:
|
|
import hermes_constants
|
|
hermes_constants._cached_default_hermes_root = None # type: ignore[attr-defined]
|
|
except Exception:
|
|
pass
|
|
kb._INITIALIZED_PATHS.clear()
|
|
return home
|
|
|
|
|
|
def test_board_metadata_project_id_roundtrip(fresh_home):
|
|
assert kb.read_board_metadata("default").get("project_id") is None
|
|
|
|
kb.write_board_metadata("default", project_id="p_abc123")
|
|
assert kb.read_board_metadata("default")["project_id"] == "p_abc123"
|
|
|
|
# None leaves unchanged; "" clears.
|
|
kb.write_board_metadata("default", name="Still Here")
|
|
assert kb.read_board_metadata("default")["project_id"] == "p_abc123"
|
|
kb.write_board_metadata("default", project_id="")
|
|
assert kb.read_board_metadata("default")["project_id"] is None
|
|
|
|
|
|
def test_create_board_accepts_project_id(fresh_home):
|
|
meta = kb.create_board("proj-board", name="Proj Board", project_id="p_xyz")
|
|
assert meta["project_id"] == "p_xyz"
|
|
assert kb.read_board_metadata("proj-board")["project_id"] == "p_xyz"
|
|
|
|
|
|
def test_create_task_inherits_board_project(fresh_home, tmp_path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
with pdb.connect_closing() as pconn:
|
|
proj_id = pdb.create_project(pconn, name="Widget", primary_path=str(repo))
|
|
|
|
kb.create_board("scoped", name="Scoped", project_id=proj_id)
|
|
conn = kb.connect(board="scoped")
|
|
try:
|
|
tid = kb.create_task(conn, title="inherit me", board="scoped")
|
|
assert kb.get_task(conn, tid).project_id == proj_id
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_create_task_explicit_project_beats_board(fresh_home, tmp_path):
|
|
(tmp_path / "a").mkdir()
|
|
(tmp_path / "b").mkdir()
|
|
with pdb.connect_closing() as pconn:
|
|
board_proj = pdb.create_project(pconn, name="BoardProj", primary_path=str(tmp_path / "a"))
|
|
task_proj = pdb.create_project(pconn, name="TaskProj", primary_path=str(tmp_path / "b"))
|
|
|
|
kb.create_board("scoped2", name="Scoped2", project_id=board_proj)
|
|
conn = kb.connect(board="scoped2")
|
|
try:
|
|
tid = kb.create_task(conn, title="explicit", board="scoped2", project_id=task_proj)
|
|
assert kb.get_task(conn, tid).project_id == task_proj
|
|
finally:
|
|
conn.close()
|