项目文件夹

文件
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

231 行
7.3 KiB
Python

"""Tests for kb.decompose_triage_task — the DB-layer atomic fan-out
from the triage column. LLM-free by design.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from hermes_cli import kanban_db as kb
@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
def _create_triage(conn, title="rough idea", body=None, assignee=None, tenant=None):
return kb.create_task(
conn,
title=title,
body=body,
assignee=assignee,
tenant=tenant,
triage=True,
)
def test_decompose_creates_children_and_promotes_root(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn, title="ship a feature")
assert kb.get_task(conn, tid).status == "triage"
children = [
{"title": "research", "body": "look at prior art", "assignee": "researcher", "parents": []},
{"title": "build it", "body": "write code", "assignee": "engineer", "parents": [0]},
]
with kb.connect() as conn:
child_ids = kb.decompose_triage_task(
conn,
tid,
root_assignee="orchestrator",
children=children,
author="decomposer",
)
assert child_ids is not None
assert len(child_ids) == 2
with kb.connect() as conn:
root = kb.get_task(conn, tid)
c0 = kb.get_task(conn, child_ids[0])
c1 = kb.get_task(conn, child_ids[1])
# Root flipped to todo with orchestrator assignee, gated by children.
assert root.status == "todo"
assert root.assignee == "orchestrator"
# First child has no internal parents → ready on recompute_ready.
assert c0.status == "ready"
assert c0.assignee == "researcher"
# Second child has parents=[0] → stays in todo until c0 completes.
assert c1.status == "todo"
assert c1.assignee == "engineer"
def test_decompose_returns_none_when_task_missing(kanban_home):
with kb.connect() as conn:
result = kb.decompose_triage_task(
conn,
"nonexistent",
root_assignee="orch",
children=[{"title": "x"}],
author="me",
)
assert result is None
def test_decompose_returns_none_when_task_not_in_triage(kanban_home):
with kb.connect() as conn:
tid = kb.create_task(conn, title="already a real task") # not triage
result = kb.decompose_triage_task(
conn,
tid,
root_assignee="orch",
children=[{"title": "x"}],
author="me",
)
assert result is None
def test_decompose_empty_children_returns_none(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn)
result = kb.decompose_triage_task(
conn,
tid,
root_assignee="orch",
children=[],
author="me",
)
assert result is None
def test_decompose_rejects_self_parent(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn)
with pytest.raises(ValueError, match="cannot list itself"):
kb.decompose_triage_task(
conn,
tid,
root_assignee="orch",
children=[{"title": "x", "parents": [0]}],
author="me",
)
def test_decompose_rejects_out_of_range_parent(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn)
with pytest.raises(ValueError, match="not a valid index"):
kb.decompose_triage_task(
conn,
tid,
root_assignee="orch",
children=[{"title": "x", "parents": [5]}],
author="me",
)
def test_decompose_rejects_cyclic_parents(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn)
with pytest.raises(ValueError, match="cyclic dependency"):
kb.decompose_triage_task(
conn,
tid,
root_assignee="orch",
children=[
{"title": "A", "parents": [1]},
{"title": "B", "parents": [0]},
],
author="me",
)
def test_decompose_records_audit_comment_and_event(kanban_home):
with kb.connect() as conn:
tid = _create_triage(conn)
child_ids = kb.decompose_triage_task(
conn,
tid,
root_assignee="orch",
children=[{"title": "task A", "assignee": "researcher"}],
author="alice",
)
assert child_ids is not None
with kb.connect() as conn:
comments = kb.list_comments(conn, tid)
events = kb.list_events(conn, tid)
assert any("Decomposed into" in (c.body or "") for c in comments)
assert any(ev.kind == "decomposed" for ev in events)
def test_decompose_children_inherit_dir_workspace(kanban_home):
"""Fan-out children inherit the root's dir workspace, not scratch."""
proj = "/home/teknium/myproject"
with kb.connect() as conn:
tid = kb.create_task(
conn, title="codegen root", assignee="worker",
workspace_kind="dir", workspace_path=proj, triage=True,
)
child_ids = kb.decompose_triage_task(
conn, tid, root_assignee="orchestrator",
children=[{"title": "part A"}, {"title": "part B", "parents": [0]}],
author="decomposer",
)
assert child_ids and len(child_ids) == 2
with kb.connect() as conn:
for cid in child_ids:
t = kb.get_task(conn, cid)
assert t.workspace_kind == "dir"
assert t.workspace_path == proj
def test_decompose_children_stay_scratch_when_root_scratch(kanban_home):
"""No regression: a scratch root still fans out into scratch children."""
with kb.connect() as conn:
tid = kb.create_task(
conn, title="scratch root", assignee="worker",
workspace_kind="scratch", triage=True,
)
child_ids = kb.decompose_triage_task(
conn, tid, root_assignee="orchestrator",
children=[{"title": "s1"}], author="decomposer",
)
with kb.connect() as conn:
t = kb.get_task(conn, child_ids[0])
assert t.workspace_kind == "scratch"
assert t.workspace_path is None
def test_decompose_per_child_workspace_override(kanban_home):
"""An explicit per-child workspace beats inheritance."""
proj = "/home/teknium/myproject"
with kb.connect() as conn:
tid = kb.create_task(
conn, title="root", assignee="worker",
workspace_kind="dir", workspace_path=proj, triage=True,
)
child_ids = kb.decompose_triage_task(
conn, tid, root_assignee="orchestrator",
children=[
{"title": "override", "workspace_kind": "dir",
"workspace_path": "/other/repo"},
{"title": "inherit"},
],
author="decomposer",
)
with kb.connect() as conn:
over = kb.get_task(conn, child_ids[0])
inh = kb.get_task(conn, child_ids[1])
assert over.workspace_path == "/other/repo"
assert inh.workspace_path == proj