Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,8 @@ def board_update_feature(
this tool never removes the flag). `source_issue` (a full GitHub issue URL or
`owner/repo#N`, stored normalized off-label in the bead's notes metadata)
sets/replaces the originating issue the feature's PR will reference as
`Fixes #N`. `project` RE-HOMES the card to another entry of the board's
`Fixes #N`; `"none"` clears it (move a split's closing edge to its last slice).
`project` RE-HOMES the card to another entry of the board's
`projects:` map (#454) — e.g. the cards left on `default` after a board moved from
the flat `repo:` binding to projects. A card's project decides which repo its
worktree/branch/PR target, so the move is allowed only while the card is backlog
Expand Down
2 changes: 1 addition & 1 deletion api.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ async def _progress(fid: str):
async def _patch_feature(fid: str, body: dict = Body(default={})):
"""In-place spec edit — the REST complement of ``board_update_feature``.
Accepts ``title``, ``spec``, ``acceptance_criteria``, ``design``,
``files_to_modify``, ``difficulty``, ``source_issue``, ``waits_for`` (replaces the
``files_to_modify``, ``difficulty``, ``source_issue`` (``"none"`` clears it), ``waits_for`` (replaces the
publish gates; ``[]`` clears), ``project`` (re-homes the card, #454 — backlog/ready,
no PR, never dispatched; refused with the reason otherwise); only non-null
fields are written. Refuses edits to an ``in_progress`` feature unless
Expand Down
11 changes: 9 additions & 2 deletions store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2222,7 +2222,11 @@ def update_feature(
args += [f"--design={design}"]
if priority is not None:
args += ["-p", str(validate_priority(priority))]
set_source = source_issue is not None and str(source_issue).strip()
# "none" / "clear" / "-" CLEARS the source issue (the `waits_for` convention): a
# split whose closing edge moves to a later slice must be able to drop it here, or
# two slices would both say "Fixes #N" and the first merge closes the issue early.
clear_source = isinstance(source_issue, str) and source_issue.strip().lower() in ("none", "clear", "-")
set_source = (source_issue is not None and str(source_issue).strip()) or clear_source
if files_to_modify is not None or set_source or new_waits is not None:
# files_to_modify + source_issue + the requirement ledger SHARE the bead
# `notes` field (labels can't carry the source's `/`/`#`, #101; the
Expand All @@ -2238,7 +2242,10 @@ def update_feature(
if files_to_modify is not None
else f.get("files_to_modify") or []
)
src = normalize_source_issue(source_issue) if set_source else str(f.get("source_issue") or "")
if clear_source:
src = ""
else:
src = normalize_source_issue(source_issue) if set_source else str(f.get("source_issue") or "")
waits = new_waits if new_waits is not None else list(f.get("waits_for") or [])
args += [f"--notes={_render_notes(files, src, f.get('requirements') or [], waits)}"]
if difficulty is not None:
Expand Down
17 changes: 17 additions & 0 deletions tests/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3179,6 +3179,23 @@ def test_update_feature_replaces_a_stale_source_line_and_keeps_files(make_board,
assert call == ("update", "bd-1", "--notes=a.py\nsource-issue: acme/widgets#9")


@pytest.mark.parametrize("sentinel", ["none", "CLEAR", " - "])
def test_update_feature_none_clears_the_source_line_and_keeps_files(make_board, monkeypatch, sentinel):
"""A split whose closing edge moves to a later slice drops the earlier slice's source
issue; otherwise two slices both say "Fixes #N" and the first merge closes it early."""
br = Br()
b = make_board(br)
monkeypatch.setattr(
b,
"_require",
lambda fid: {"id": fid, "files_to_modify": ["a.py"], "source_issue": "acme/widgets#9", "labels": []},
)
monkeypatch.setattr(b, "get_feature", lambda fid: {"id": fid, "labels": []})
b.update_feature("bd-1", source_issue=sentinel)
(call,) = br.cmds("update")
assert call == ("update", "bd-1", "--notes=a.py")


def test_update_feature_files_update_preserves_the_source_line(make_board, monkeypatch):
"""The mirror image: a files-only update must never drop the stored source."""
br = Br()
Expand Down
Loading