mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(kanban): scope boards to a project
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.
This commit is contained in:
parent
9be67b7762
commit
027ef381a4
8 changed files with 518 additions and 20 deletions
119
tests/plugins/test_kanban_board_project_api.py
Normal file
119
tests/plugins/test_kanban_board_project_api.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""Kanban dashboard plugin: project listing + project-scoped boards.
|
||||
|
||||
Attaches the plugin router to a bare FastAPI app (as in
|
||||
test_kanban_dashboard_plugin.py) and exercises the project surface:
|
||||
GET /projects, board create/patch/list carrying project scope, and a task
|
||||
on a scoped board inheriting the project.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from hermes_cli import kanban_db as kb
|
||||
from hermes_cli import projects_db as pdb
|
||||
|
||||
|
||||
def _load_plugin_router():
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
plugin_file = repo_root / "plugins" / "kanban" / "dashboard" / "plugin_api.py"
|
||||
spec = importlib.util.spec_from_file_location("hermes_kanban_plugin_proj_test", plugin_file)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod.router
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kanban_home(tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
kb.init_db()
|
||||
return home
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(kanban_home):
|
||||
app = FastAPI()
|
||||
app.include_router(_load_plugin_router(), prefix="/api/plugins/kanban")
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project(tmp_path):
|
||||
repo = tmp_path / "widget-repo"
|
||||
repo.mkdir()
|
||||
with pdb.connect_closing() as conn:
|
||||
pid = pdb.create_project(conn, name="Widget", primary_path=str(repo))
|
||||
return {"id": pid, "primary_path": str(repo)}
|
||||
|
||||
|
||||
def test_list_projects(client, project):
|
||||
r = client.get("/api/plugins/kanban/projects")
|
||||
assert r.status_code == 200
|
||||
hit = next(p for p in r.json()["projects"] if p["id"] == project["id"])
|
||||
assert hit["name"] == "Widget"
|
||||
assert hit["primary_path"] == project["primary_path"]
|
||||
|
||||
|
||||
def test_create_board_with_project_mirrors_workdir(client, project):
|
||||
r = client.post(
|
||||
"/api/plugins/kanban/boards",
|
||||
json={"slug": "widget", "name": "Widget", "project_id": project["id"]},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
board = r.json()["board"]
|
||||
assert board["project_id"] == project["id"]
|
||||
assert board["project_name"] == "Widget"
|
||||
assert board["default_workdir"] == project["primary_path"]
|
||||
|
||||
|
||||
def test_create_board_rejects_unknown_project(client):
|
||||
r = client.post("/api/plugins/kanban/boards", json={"slug": "bad", "project_id": "p_nope"})
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_patch_board_set_and_clear_project(client, project):
|
||||
client.post("/api/plugins/kanban/boards", json={"slug": "widget", "name": "Widget"})
|
||||
|
||||
r = client.patch("/api/plugins/kanban/boards/widget", json={"project_id": project["id"]})
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["board"]["project_id"] == project["id"]
|
||||
|
||||
r = client.patch("/api/plugins/kanban/boards/widget", json={"project_id": ""})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["board"]["project_id"] is None
|
||||
|
||||
|
||||
def test_boards_list_surfaces_project(client, project):
|
||||
client.post(
|
||||
"/api/plugins/kanban/boards",
|
||||
json={"slug": "widget", "name": "Widget", "project_id": project["id"]},
|
||||
)
|
||||
widget = next(b for b in client.get("/api/plugins/kanban/boards").json()["boards"] if b["slug"] == "widget")
|
||||
assert widget["project_id"] == project["id"]
|
||||
assert widget["project_name"] == "Widget"
|
||||
|
||||
|
||||
def test_task_on_scoped_board_inherits_project(client, project):
|
||||
client.post(
|
||||
"/api/plugins/kanban/boards",
|
||||
json={"slug": "widget", "name": "Widget", "project_id": project["id"]},
|
||||
)
|
||||
r = client.post("/api/plugins/kanban/tasks?board=widget", json={"title": "do the thing"})
|
||||
assert r.status_code == 200, r.text
|
||||
task_id = r.json()["task"]["id"]
|
||||
|
||||
conn = kb.connect(board="widget")
|
||||
try:
|
||||
assert kb.get_task(conn, task_id).project_id == project["id"]
|
||||
finally:
|
||||
conn.close()
|
||||
Loading…
Add table
Add a link
Reference in a new issue