|
| 1 | +"""board_create_feature dedup — the same class of bug portfolio_dispatch's #25 |
| 2 | +dedup fixed one tier up, but here: an agent's OWN reasoning (onboarding, or its |
| 3 | +own read of a dispatched task) calling this tool twice in one turn for the same |
| 4 | +work, live-caught in dogfooding (two board_create_feature calls, ~9s apart, same |
| 5 | +title, no force) with no guard to stop it.""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import json |
| 10 | + |
| 11 | +import project_board as pb |
| 12 | + |
| 13 | + |
| 14 | +def test_norm_title_collapses_whitespace_and_case(): |
| 15 | + assert pb._norm_title(" Fix the Thing ") == "fix the thing" |
| 16 | + assert pb._norm_title("Fix the Thing") == pb._norm_title("fix the thing") |
| 17 | + |
| 18 | + |
| 19 | +def test_open_duplicate_finds_same_title_open_feature(): |
| 20 | + features = [{"id": "bd-1", "title": "Add X", "board_state": "backlog"}] |
| 21 | + dup = pb._open_duplicate(features, "add x") |
| 22 | + assert dup is not None and dup["id"] == "bd-1" |
| 23 | + |
| 24 | + |
| 25 | +def test_open_duplicate_ignores_terminal_states(): |
| 26 | + features = [ |
| 27 | + {"id": "bd-1", "title": "Add X", "board_state": "done"}, |
| 28 | + {"id": "bd-2", "title": "Add X", "board_state": "cancelled"}, |
| 29 | + ] |
| 30 | + assert pb._open_duplicate(features, "Add X") is None |
| 31 | + |
| 32 | + |
| 33 | +def test_open_duplicate_none_when_no_match(): |
| 34 | + features = [{"id": "bd-1", "title": "Add X", "board_state": "backlog"}] |
| 35 | + assert pb._open_duplicate(features, "Add Y") is None |
| 36 | + |
| 37 | + |
| 38 | +class _FakeStore: |
| 39 | + """Records create_feature calls; list_features returns whatever's been created |
| 40 | + so far — enough to exercise board_create_feature's dedup decision without a |
| 41 | + real BeadsBoard/br CLI (store.py's own projection is tested in test_store.py).""" |
| 42 | + |
| 43 | + def __init__(self): |
| 44 | + self.created: list[dict] = [] |
| 45 | + self._next = 1 |
| 46 | + |
| 47 | + def list_features(self, state=None): |
| 48 | + return list(self.created) |
| 49 | + |
| 50 | + def create_feature(self, title, **kw): |
| 51 | + fid = f"bd-{self._next}" |
| 52 | + self._next += 1 |
| 53 | + f = {"id": fid, "title": title, "board_state": "backlog", **kw} |
| 54 | + self.created.append(f) |
| 55 | + return f |
| 56 | + |
| 57 | + |
| 58 | +def _get_tool(name: str, cfg: dict | None = None): |
| 59 | + tools = {t.name: t for t in pb._board_tools(cfg or {})} |
| 60 | + return tools[name] |
| 61 | + |
| 62 | + |
| 63 | +def test_board_create_feature_refuses_a_same_title_open_dup(monkeypatch): |
| 64 | + fake = _FakeStore() |
| 65 | + monkeypatch.setattr("project_board.store.get_store", lambda **_kw: fake) |
| 66 | + create = _get_tool("board_create_feature") |
| 67 | + |
| 68 | + first = json.loads(create.invoke({"title": "Rollup surfacing", "spec": "s1"})) |
| 69 | + assert first["id"] == "bd-1" |
| 70 | + |
| 71 | + second = create.invoke({"title": "Rollup surfacing", "spec": "s2 — reconsidered"}) |
| 72 | + assert "Skipped" in second |
| 73 | + assert "bd-1" in second |
| 74 | + assert len(fake.created) == 1 # the dup was refused, not created |
| 75 | + |
| 76 | + |
| 77 | +def test_board_create_feature_force_true_creates_a_second_copy(monkeypatch): |
| 78 | + fake = _FakeStore() |
| 79 | + monkeypatch.setattr("project_board.store.get_store", lambda **_kw: fake) |
| 80 | + create = _get_tool("board_create_feature") |
| 81 | + |
| 82 | + create.invoke({"title": "Rollup surfacing", "spec": "s1"}) |
| 83 | + second = create.invoke({"title": "Rollup surfacing", "spec": "s2", "force": True}) |
| 84 | + assert json.loads(second)["id"] == "bd-2" |
| 85 | + assert len(fake.created) == 2 |
| 86 | + |
| 87 | + |
| 88 | +def test_board_create_feature_allows_recreating_after_the_first_is_done(monkeypatch): |
| 89 | + fake = _FakeStore() |
| 90 | + monkeypatch.setattr("project_board.store.get_store", lambda **_kw: fake) |
| 91 | + create = _get_tool("board_create_feature") |
| 92 | + |
| 93 | + create.invoke({"title": "Rollup surfacing", "spec": "s1"}) |
| 94 | + fake.created[0]["board_state"] = "done" # the first shipped — this is legit new work |
| 95 | + |
| 96 | + second = create.invoke({"title": "Rollup surfacing", "spec": "s2"}) |
| 97 | + assert json.loads(second)["id"] == "bd-2" |
0 commit comments