diff --git a/README.md b/README.md
index b1ca58d8..a772af99 100644
--- a/README.md
+++ b/README.md
@@ -158,8 +158,8 @@ Git Bash runs step 3's bash command and its one line, tested in CI but for a
typed `yes`, which its preflight cannot take there. Printed commands are quoted
for a POSIX shell, so paste them into Git Bash. Only `shape-doctor.py`'s lines,
`setup-project.py`'s family lines and `family.py`'s `NEXT`, `switch -c`, `add`,
-`cd` and `git rm -r --cached` lines are quoted for the shell they are printed
-in, PowerShell or Git Bash, on Windows.
+`cd`, `git rm -r --cached` and `bump` lines are quoted for the shell they are
+printed in, PowerShell or Git Bash, on Windows.
Off Windows and in Git Bash, a path `shape-doctor.py` cannot print as itself is
written `$'…'`: bash, zsh and ksh read that; dash (`/bin/sh` on Debian and
Ubuntu) does not.
diff --git a/docs/handbook.html b/docs/handbook.html
index c3160e3c..0f2be244 100644
--- a/docs/handbook.html
+++ b/docs/handbook.html
@@ -472,7 +472,7 @@
-
+
@@ -612,7 +612,7 @@
The one thing to read before electing it
-
+
Setup
Start here
@@ -737,11 +737,11 @@ The tool offers the rest
Printed commands are quoted for a POSIX shell, so paste them into Git Bash. Only
shape-doctor.py’s lines, setup-project.py’s family
lines and family.py’s NEXT, switch -c,
- add, cd and git rm -r --cached lines are quoted for
- the shell they are printed in, PowerShell or Git Bash, on Windows. A shell started from
- inside Git Bash inherits its MSYSTEM, which is how a tool tells Git Bash
- apart, so a PowerShell started there is handed Git Bash’s quoting, and a cmd.exe
- started anywhere else is handed PowerShell’s. Off Windows and in Git Bash, a path
+ add, cd, git rm -r --cached and bump
+ lines are quoted for the shell they are printed in, PowerShell or Git Bash, on Windows.
+ A shell started from inside Git Bash inherits its MSYSTEM, which is how a tool
+ tells Git Bash apart, so a PowerShell started there is handed Git Bash’s quoting, and a
+ cmd.exe started anywhere else is handed PowerShell’s. Off Windows and in Git Bash, a path
shape-doctor.py cannot print as itself is written $'…':
bash, zsh and ksh read that; dash (/bin/sh on Debian and Ubuntu) does not.
@@ -1092,7 +1092,7 @@ What you have afterwards — the same on every platform
-
+
Setup
If it fails part-way
@@ -1129,7 +1129,7 @@ If it fails part-way
-
+
@@ -1192,7 +1192,7 @@ Reading private legs in CI
-
+
@@ -1448,7 +1448,7 @@ The InkRouter example
-
+
@@ -1747,7 +1747,7 @@ Is this repository compliant? openRepoShape --doctor
-
+
diff --git a/scripts/bump-leg.py b/scripts/bump-leg.py
index 746a75dd..8c5a470e 100755
--- a/scripts/bump-leg.py
+++ b/scripts/bump-leg.py
@@ -219,7 +219,8 @@ def open(cls, root_spec) -> "Project":
"Remediation: --root takes the ASSEMBLY ROOT — the repository "
"an engineer clones — not a leg and not a family holder. A "
"family's member pins are moved with `scripts/family.py "
- "bump`; a leg has no legs of its own to move.")
+ "bump --family-root --member --to "
+ "`; a leg has no legs of its own to move.")
data = load_yaml(path)
if not isinstance(data, dict):
raise Refusal("bump-leg-manifest-unreadable",
@@ -231,7 +232,8 @@ def open(cls, root_spec) -> "Project":
"'project-manifest'",
"Remediation: a family holder declares `kind: "
"family-manifest` and its members move with `scripts/"
- "family.py bump`.")
+ "family.py bump --family-root --member "
+ "--to `.")
return cls(root, data)
@property
@@ -276,6 +278,9 @@ def leg(self, role: str) -> dict:
# branch would never be reached, and the root is not a submodule of
# itself to advance.
if role == "assembly":
+ # `` IS A PLACEHOLDER (#927): `family.py bump` requires
+ # `--family-root`, and this tool is pointed at a root, which
+ # knows no holder, so it is said as `` and `` are.
raise Refusal(
"bump-leg-assembly-is-not-a-leg",
"the assembly root is THIS repository, mounted at `.`, and it "
@@ -283,7 +288,8 @@ def leg(self, role: str) -> dict:
"Remediation: --leg takes `spec` or `code`. The assembly "
"root advances by its own commits; if you meant to move the "
"project's pin inside a FAMILY, that is `scripts/family.py "
- "bump --member --to `.")
+ "bump --family-root --member --to "
+ "`.")
for row in self.legs:
if str(row.get("role")) == role:
return row
diff --git a/scripts/family.py b/scripts/family.py
index 010a24ed..bbcfd3a9 100755
--- a/scripts/family.py
+++ b/scripts/family.py
@@ -6610,13 +6610,17 @@ def _checked_add_target(family: Family, repository: str, project: str,
"""
held = family.holding(repository, project)
if held is not None:
+ # THE `bump` IT NAMES RUNS AS PRINTED (#927): `bump` requires
+ # `--family-root`, so the line names this holder, spelled by
+ # `shell_quoted` as `_add_command`'s is.
raise Refusal(
"member-already-present",
f"{project} is already a member of {family.data.get('name')}: "
f"the row {held.get('project')} pins {held.get('repository')}",
- "Remediation: `family.py bump --member "
- f"{held.get('project')} --to ` moves an existing member; "
- "`remove` takes it out. Adding it twice is not a way to move it.")
+ "Remediation: `family.py bump --family-root "
+ f"{shell_quoted(family.root)} --member {held.get('project')} "
+ "--to ` moves an existing member; `remove` takes it "
+ "out. Adding it twice is not a way to move it.")
# Everything the mount's `--force` would otherwise let past git, but the
# cached store, which is asked below (#242).
_checked_mount_path(family.root, project, path,
@@ -6733,6 +6737,8 @@ def _checked_member_id(family: Family, repository: str, project: str,
row = _row_recording(family, project_id)
if row is not None:
name = row["project"]
+ # THE `bump` IT NAMES RUNS AS PRINTED (#927), with this holder's
+ # `--family-root`, as `member-already-present`'s does.
raise Refusal(
"member-duplicate-id",
f"{project} is already a member of {family.data.get('name')} "
@@ -6744,9 +6750,10 @@ def _checked_member_id(family: Family, repository: str, project: str,
f"{repository} is {row.get('repository')} renamed at the forge, "
f"`family.py remove --member {name}` takes the old row out and "
"this `add` then pins it under the new name; `family.py bump "
- f"--member {name} --to ` moves the row that is there "
- "instead. Two projects that record one id are a fault in one of "
- "their `project.yaml` files, mended in that project, not in the "
+ f"--family-root {shell_quoted(family.root)} --member {name} "
+ "--to ` moves the row that is there instead. Two "
+ "projects that record one id are a fault in one of their "
+ "`project.yaml` files, mended in that project, not in the "
"family.")
diff --git a/tests/test_bump_remedies_family_root.py b/tests/test_bump_remedies_family_root.py
new file mode 100644
index 00000000..e7499657
--- /dev/null
+++ b/tests/test_bump_remedies_family_root.py
@@ -0,0 +1,231 @@
+# SPDX-License-Identifier: Apache-2.0
+"""A remedy that names `family.py bump` names `--family-root`, so the line
+runs as printed (#927).
+
+`bump` requires `--family-root` (its usage line, `family.py`'s head). Five
+remedies named the verb without it. Typed as printed, the three that named
+`--member` and `--to` ended in
+`family.py bump: error: the following arguments are required: --family-root`,
+and the two that named a bare `scripts/family.py bump` in
+`...: --family-root, --member, --to`:
+
+* `member-already-present`, which `add` says of a member the holder already
+ pins, and `member-duplicate-id`, which `add` says of a member that records
+ an id another row records. Both KNOW the holder, so each names it, in the
+ one word `shell_quoted` makes of its path, as `add`'s own line does.
+* `bump-leg-assembly-is-not-a-leg`, `bump-leg-root-not-a-project` and
+ `bump-leg-wrong-kind`, which say a family's member pin moves with
+ `family.py bump`. `bump-leg.py` has no holder to name, so each says
+ ``, the way it says `` and ``.
+
+Each family case is measured three ways, in a holder under a plain folder
+and under two with a space in their names, one of them with an apostrophe
+too (the paths a shell would tear in half): the line names the holder as
+ONE word that comes back as the holder, `family.py`'s own parser accepts it,
+and it is RUN as printed and moves the pin. Each `bump-leg.py` line is read
+the same way once its three placeholders are filled in.
+
+The fixtures are the ones the tests of these codes already use:
+`tests/test_family.py`'s family, `tests/test_family_add_member_id.py`'s
+renamed copy and `tests/test_bump_leg.py`'s project. No network: `gh` is a
+shim that exits non-zero.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+import re
+from pathlib import Path
+
+import pytest
+
+from conftest import git, run_script
+from repo_shape import shell_quoted
+#: The fixtures and helpers of the tests that already trigger these codes,
+#: imported from the files that own them rather than rebuilt: a holder or a
+#: project made any other way would test a different one.
+from test_bump_leg import BUMP, branched, bump
+from test_family import FAMILY, family, manifest # noqa: F401
+from test_family_add_member_id import PINNED, RENAMED, renamed_copy
+from test_family_add_refusals import offline # noqa: F401
+from test_family_printed_quoting import copied_holder, host_words
+
+#: A commit id to put where a line says `` or `` when nothing is
+#: run with it. It is NOT a fixture's `advanced`: that one pushes a branch to
+#: the session's bare repositories, and a second module doing it makes
+#: `tests/test_bump_leg.py`'s own fail non-fast-forward.
+COMMIT = "0123456789abcdef0123456789abcdef01234567"
+
+#: Where the holder stands: a folder a shell reads whole, one with a space,
+#: and one with a space beside an apostrophe, where `sh` and PowerShell
+#: escape differently.
+PLACES = ["plain", "my work", "Dana's work"]
+
+
+@pytest.fixture(params=PLACES)
+def placed(request, family, tmp_path) -> Path:
+ """A private copy of the family's holder, clean, under `request.param`."""
+ base = tmp_path / ("holders" if request.param == "plain"
+ else request.param)
+ return copied_holder(family, base)
+
+
+@pytest.fixture(scope="module")
+def family_module():
+ """`family.py` as a module, for its own argument parser."""
+ spec = importlib.util.spec_from_file_location("family_bump_remedies",
+ FAMILY)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def already_present(family, holder, offline, tmp_path):
+ """`add` of the member the holder already pins: `member-already-present`
+ (the case `tests/test_family.py` refuses)."""
+ return offline.add(holder, PINNED, remotes=family["remotes"])
+
+
+def duplicate_id(family, holder, offline, tmp_path):
+ """`add` of a copy of the pinned member under a new name, whose
+ `project.yaml` records the id the row `IRRS` pins: `member-duplicate-id`
+ (the case `tests/test_family_add_member_id.py` refuses)."""
+ return offline.add(holder, RENAMED,
+ remotes=renamed_copy(family, tmp_path))
+
+
+#: Each code, and the `add` that is refused with it.
+CASES = {"member-already-present": already_present,
+ "member-duplicate-id": duplicate_id}
+
+
+def refused_as(code, family, holder, offline, tmp_path):
+ """The stderr of the `add` that `code` refuses, checked to be it and
+ exit 2."""
+ result = CASES[code](family, holder, offline, tmp_path)
+ assert result.returncode == 2, result.stdout + result.stderr
+ assert f"REFUSED {code}: " in result.stderr
+ return result.stderr
+
+
+def the_bump_line(stderr: str, lead: str = "") -> str:
+ """The one `family.py bump ...` a remedy prints, from `bump` to the
+ closing backtick, with `lead` before `family.py` (a path, for
+ `bump-leg.py`'s)."""
+ found = re.findall(re.escape(f"`{lead}family.py ") + r"(bump [^`]*)`",
+ stderr)
+ assert len(found) == 1, stderr
+ return found[0]
+
+
+def parsed(family_module, words, capsys):
+ """What `family.py`'s own parser makes of `words`; a failure to parse is
+ the failure, said in argparse's own words."""
+ try:
+ return family_module.build_parser().parse_args(words)
+ except SystemExit:
+ pytest.fail(f"family.py {' '.join(words)}\n"
+ + capsys.readouterr().err)
+
+
+@pytest.mark.parametrize("code", CASES)
+def test_the_bump_a_remedy_names_carries_the_holder_whole(
+ code, family, placed, offline, tmp_path):
+ """The line is `bump --family-root --member IRRS --to
+ `, and the holder is ONE word that is the holder's own path, as
+ the host's shell reads it, however many spaces and apostrophes it has."""
+ stderr = refused_as(code, family, placed, offline, tmp_path)
+ line = the_bump_line(stderr)
+ assert line == (f"bump --family-root {shell_quoted(placed)} "
+ f"--member {PINNED} --to ")
+ assert host_words(line) == ["bump", "--family-root", str(placed),
+ "--member", PINNED, "--to", ""]
+
+
+@pytest.mark.parametrize("code", CASES)
+def test_the_bump_a_remedy_names_parses(
+ code, family, placed, offline, tmp_path, family_module, capsys):
+ """Fed to `family.py`'s own parser, with a commit for ``, the
+ printed words are accepted: no `the following arguments are required`."""
+ stderr = refused_as(code, family, placed, offline, tmp_path)
+ words = host_words(the_bump_line(stderr).replace("", COMMIT))
+ capsys.readouterr()
+ args = parsed(family_module, words, capsys)
+ assert args.family_root == str(placed)
+ assert args.member == PINNED
+ assert args.to == COMMIT
+ assert args.func is family_module.cmd_bump
+
+
+@pytest.mark.parametrize("code", CASES)
+def test_the_bump_a_remedy_names_runs_as_printed(
+ code, family, placed, offline, tmp_path):
+ """The remediation is the repair. Typed as printed, with a real commit
+ of the member for ``, `bump` moves the row that is there: exit 0,
+ the pin at that commit, and the holder clean."""
+ stderr = refused_as(code, family, placed, offline, tmp_path)
+ older = git("rev-parse", "main~1",
+ cwd=family["remotes"] / f"{PINNED}.git").stdout.strip()
+ words = host_words(the_bump_line(stderr).replace("", older))
+ done = run_script(FAMILY, *words, env=offline.env)
+ assert "the following arguments are required" not in done.stderr
+ assert done.returncode == 0, done.stdout + done.stderr
+ rows = {row["project"]: row for row in manifest(placed)["members"]}
+ assert rows[PINNED]["pin"]["commit"] == older
+ assert git("status", "--porcelain", cwd=placed).stdout == ""
+
+
+def assembly(project, scaffolded, tmp_path):
+ """`--leg assembly`: `bump-leg-assembly-is-not-a-leg`."""
+ branched(project)
+ return bump(project, scaffolded, COMMIT, leg="assembly")
+
+
+def a_holder(project, scaffolded, tmp_path):
+ """`--root` a family holder, which has a `family.yaml` and no
+ `project.yaml`: `bump-leg-root-not-a-project`."""
+ holder = tmp_path / "Holder"
+ holder.mkdir()
+ (holder / "family.yaml").write_text("kind: family-manifest\n")
+ return run_script(BUMP, "--root", str(holder), "--leg", "spec", "--to",
+ COMMIT)
+
+
+def another_kind(project, scaffolded, tmp_path):
+ """A `project.yaml` that is a family's, not a project's:
+ `bump-leg-wrong-kind`."""
+ root = tmp_path / "Family"
+ root.mkdir()
+ (root / "project.yaml").write_text("kind: family-manifest\n")
+ return run_script(BUMP, "--root", str(root), "--leg", "spec", "--to",
+ COMMIT)
+
+
+#: Each `bump-leg.py` code that points at `family.py bump`, and the run that
+#: is refused with it.
+BUMP_LEG_CASES = {"bump-leg-assembly-is-not-a-leg": assembly,
+ "bump-leg-root-not-a-project": a_holder,
+ "bump-leg-wrong-kind": another_kind}
+
+
+@pytest.mark.parametrize("code", BUMP_LEG_CASES)
+def test_a_bump_leg_remedy_that_names_family_bump_names_the_holder(
+ code, project, scaffolded, family_module, capsys, tmp_path):
+ """`bump-leg.py` has no holder to name, so the line it prints says
+ `` as it says `` and ``: each remedy names
+ `--family-root`, and with its three placeholders filled in,
+ `family.py`'s own parser accepts it."""
+ result = BUMP_LEG_CASES[code](project, scaffolded, tmp_path)
+ assert result.returncode == 2, result.stdout + result.stderr
+ assert f"REFUSED {code}: " in result.stderr
+ line = the_bump_line(result.stderr, "scripts/")
+ assert line == "bump --family-root --member --to "
+ # The holder as a shell is handed it: quoted, so that a basetemp whose
+ # path holds a space (`--basetemp="my work/bt"`) is still one word.
+ filled = (line.replace("", shell_quoted(project.parent))
+ .replace("", PINNED).replace("", COMMIT))
+ capsys.readouterr()
+ args = parsed(family_module, host_words(filled), capsys)
+ assert args.family_root == str(project.parent)
+ assert args.member == PINNED
+ assert args.to == COMMIT
diff --git a/tests/test_family.py b/tests/test_family.py
index ac12a5bf..c4e24926 100644
--- a/tests/test_family.py
+++ b/tests/test_family.py
@@ -47,7 +47,9 @@
from test_update_shape import rendered_shape_line, strip_shape_block
sys.path.insert(0, str(REPO / "scripts"))
-from repo_shape import Refusal, load_yaml, tree_digest # noqa: E402
+from repo_shape import ( # noqa: E402
+ Refusal, load_yaml, shell_quoted, tree_digest,
+)
from shape_materialize import ( # noqa: E402
SHAPE_REPOSITORY, readme_shape_lines, readme_trailing_shape_line,
)
@@ -538,7 +540,9 @@ def test_add_refuses_a_spelling_of_a_member_the_family_already_pins(
assert result.returncode == 2, result.stderr + result.stdout
assert "REFUSED member-already-present" in result.stderr
assert "the row IRRS pins InkRouter/IRRS" in result.stderr
- assert "`family.py bump --member IRRS --to `" in result.stderr
+ assert ("`family.py bump --family-root "
+ f"{shell_quoted(holder.resolve())} --member IRRS --to "
+ "`") in result.stderr
assert "mounting" not in result.stdout
assert git("rev-parse", "HEAD", cwd=holder).stdout == head
assert git("status", "--porcelain", cwd=holder).stdout == status
diff --git a/tests/test_family_add_member_id.py b/tests/test_family_add_member_id.py
index 4409f505..f39dbbc7 100644
--- a/tests/test_family_add_member_id.py
+++ b/tests/test_family_add_member_id.py
@@ -39,6 +39,7 @@
import pytest
from conftest import cloned_just_pushed, git, run_script
+from repo_shape import shell_quoted
#: The family fixture, its names and the offline environment, imported from
#: the modules that own them rather than rebuilt: a holder made any other way
#: would test a different holder.
@@ -50,6 +51,9 @@
gitmodules_with, holder, mount_undone, offline, refusal, snapshot,
take_out_as_printed,
)
+#: The shell's reading of a printed line on this host: the holder `bump`
+#: names is one quoted word of it, whatever its path holds (#927).
+from test_family_printed_quoting import host_words
#: The project the renamed copy is of, and the name it is added under.
PINNED = "IRRS"
@@ -69,10 +73,18 @@
f"Remediation: a family pins each project once. If {ORG}/{{who}} is "
f"{ORG}/{PINNED} renamed at the forge, `family.py remove --member "
f"{PINNED}` takes the old row out and this `add` then pins it under the "
- f"new name; `family.py bump --member {PINNED} --to ` moves the "
- "row that is there instead. Two projects that record one id are a fault "
- "in one of their `project.yaml` files, mended in that project, not in "
- "the family.")
+ "new name; `family.py bump --family-root {holder} --member "
+ f"{PINNED} --to ` moves the row that is there instead. Two "
+ "projects that record one id are a fault in one of their "
+ "`project.yaml` files, mended in that project, not in the family.")
+
+
+def duplicate_id_remediation(who: str, holder: Path) -> str:
+ """What `add` tells the human to do about `member-duplicate-id`: the
+ `bump` it names carries `--family-root` and the holder, as `bump`
+ requires (#927)."""
+ return DUPLICATE_ID_REMEDIATION.format(
+ who=who, holder=shell_quoted(holder.resolve()))
def renamed_copy(family, tmp_path: Path) -> Path:
@@ -101,7 +113,7 @@ def test_a_renamed_repository_is_refused_before_anything_is_mounted(
assert result.stdout == f"checking {ORG}/{RENAMED} before mounting it\n"
assert refusal(result.stderr) == (
DUPLICATE_ID.format(who=RENAMED, after=""),
- DUPLICATE_ID_REMEDIATION.format(who=RENAMED))
+ duplicate_id_remediation(RENAMED, holder))
assert_unchanged(holder, before, RENAMED)
assert validate(holder).returncode == 0
offline.assert_left_nothing()
@@ -141,7 +153,7 @@ def test_a_section_already_at_the_mount_path_is_refused_first_then_the_id(
assert second.stdout == f"checking {ORG}/{RENAMED} before mounting it\n"
assert refusal(second.stderr) == (
DUPLICATE_ID.format(who=RENAMED, after=""),
- DUPLICATE_ID_REMEDIATION.format(who=RENAMED))
+ duplicate_id_remediation(RENAMED, holder))
assert snapshot(holder) == cleared
assert not (holder / "members" / RENAMED).exists()
assert not (holder / ".git" / "modules" / "members" / RENAMED).exists()
@@ -152,11 +164,12 @@ def test_a_section_already_at_the_mount_path_is_refused_first_then_the_id(
@pytest.mark.parametrize("verb", ["remove", "bump"])
def test_each_command_the_refusal_names_runs_on_the_row_it_names(
family, holder, offline, tmp_path, verb):
- """The remediation is the repair. Typed as printed — with the
- `--family-root` every verb takes, and a real commit for `` —
- `remove` takes the old row out and the refused `add` then pins the
- project under its new name, and `bump` moves the row that is there; the
- gate is green after either, with one row recording id `irrs`."""
+ """The remediation is the repair. Typed as printed, with a real commit
+ for ``, `remove` takes the old row out and the refused `add` then
+ pins the project under its new name, and `bump` moves the row that is
+ there; the gate is green after either, with one row recording id `irrs`.
+ `bump` is run exactly as printed, its own `--family-root` and all
+ (#927); `remove` is still given the `--family-root` it does not name."""
remotes = renamed_copy(family, tmp_path)
refused = offline.add(holder, RENAMED, remotes=remotes)
assert refused.returncode == 2, refused.stdout + refused.stderr
@@ -164,10 +177,10 @@ def test_each_command_the_refusal_names_runs_on_the_row_it_names(
assert len(commands) == 1, refused.stderr
older = git("rev-parse", "main~1",
cwd=family["remotes"] / f"{PINNED}.git").stdout.strip()
- argv = commands[0].replace("", older).split()
+ argv = host_words(commands[0].replace("", older))
assert argv[argv.index("--member") + 1] == PINNED
- done = run_script(FAMILY, *argv, "--family-root", str(holder),
- env=offline.env)
+ unnamed = [] if verb == "bump" else ["--family-root", str(holder)]
+ done = run_script(FAMILY, *argv, *unnamed, env=offline.env)
assert done.returncode == 0, done.stdout + done.stderr
if verb == "remove":
added = offline.add(holder, RENAMED, remotes=remotes)
@@ -222,7 +235,7 @@ def test_a_linked_manifest_with_a_pinned_id_is_refused_by_the_mount_unwound(
assert refusal(result.stderr) == (
DUPLICATE_ID.format(who="IRLN",
after=". " + mount_undone("IRLN")),
- DUPLICATE_ID_REMEDIATION.format(who="IRLN"))
+ duplicate_id_remediation("IRLN", holder))
assert_unchanged(holder, before, "IRLN")
assert validate(holder).returncode == 0
offline.assert_left_nothing()
@@ -271,7 +284,7 @@ def renumbered(*args, **kwargs):
f"mounting {ORG}/{SPARE} at members/{SPARE}\n")
assert refusal(err) == (
DUPLICATE_ID.format(who=SPARE, after=". " + mount_undone(SPARE)),
- DUPLICATE_ID_REMEDIATION.format(who=SPARE))
+ duplicate_id_remediation(SPARE, holder))
assert_unchanged(holder, before, SPARE)
offline.assert_left_nothing()
diff --git a/tests/test_readme_windows_quoting_sentence.py b/tests/test_readme_windows_quoting_sentence.py
index 487c293f..ef8fa025 100644
--- a/tests/test_readme_windows_quoting_sentence.py
+++ b/tests/test_readme_windows_quoting_sentence.py
@@ -61,14 +61,16 @@
"shape-doctor.py": EVERY_LINE,
# "`setup-project.py`'s family lines": the `switch -c` and `add` it prints.
"setup-project.py": {"family_next_command": 2},
- # "`family.py`'s `NEXT`, `switch -c`, `add` and `git rm -r --cached`
- # lines".
+ # "`family.py`'s `NEXT`, `switch -c`, `add`, `cd`, `git rm -r --cached`
+ # and `bump` lines".
"scripts/family.py": {
"_print_next": 1, # the `NEXT` line: `git push -u origin ...`
"_switch_command": 1, # the `git switch -c ` line
"_add_command": 3, # the `family.py add --family-root ...` line
"_print_next_steps": 2, # `init`'s block: its `switch -c` and `cd`
"_checked_mount_path": 1, # the `git rm -r --cached -- ` remedy
+ "_checked_add_target": 1, # `member-already-present`'s `bump` line
+ "_checked_member_id": 1, # `member-duplicate-id`'s `bump` line (#927)
},
}