tools/bugreport: --bundle/--chronology tauscht die Chronologie eines vorhandenen Bündels - Geheimnisse, Titelschutz und Pseudonyme wie beim Sammeln, kein wikitool-Aufruf, kein zweites Bündel; bug-report.md prüft die Chronologie vor dem Sammeln und nennt den Austausch als Entscheidungspunkt (#220); 10.1.0-beta.15
Files changed: - CHANGES.md - VERSION - docs/how-to/troubleshoot.md - instructions/bug-report.md - tools/README.md - tools/bugreport.py - tools/chemenu/tests/test_bugreport.py Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
b219adde91
commit
b48a35b224
7 files changed
+371
-27
No files matched your search
@@ -148,6 +148,12 @@ exits 0 or 1 - never 42, since it opens no gate. It has a second mode: `--pseudo
|
||||
identities it can read from the machine (stage 1), and `--bundle`/`--candidates` applies names a
|
||||
model found in the result (stage 2), both word by word so that length, separators and depth
|
||||
survive. The mapping, the review list and the candidate file stay beside the bundle directory.
|
||||
A third mode, `--bundle`/`--chronology` (with `--root` if the checkout is not the one holding
|
||||
`tools/`), swaps the chronology of a bundle that exists already: it runs no `wikitool` call and
|
||||
writes no second bundle, changes `CHRONOLOGY.md` and the zip (and the review list of a
|
||||
pseudonymised bundle), and puts the new text through the same secret scrub, title shield and
|
||||
pseudonyms the collection would have. A bundle collected without a chronology gets one, and its
|
||||
manifest changes in three fixed places.
|
||||
`dist export` ships it with the rest of `tools/`;
|
||||
its tests (`tests/test_bugreport.py`, in the origin repository) run it on a bare interpreter (`-I -S`) to keep that promise.
|
||||
|
||||
|
||||
+148
-23
@@ -10,6 +10,7 @@ Runs on the base Python with the standard library only, and imports nothing from
|
||||
[--session ID | --no-trace] [--titles]
|
||||
[--pseudonymise] [--root DIR] [--out DIR]
|
||||
tools/bugreport --bundle DIR --candidates FILE
|
||||
tools/bugreport --bundle DIR --chronology FILE [--root DIR]
|
||||
|
||||
`tools/bugreport` (and `tools/bugreport.ps1` under PowerShell) finds a Python 3.8
|
||||
or newer and runs this file with it, skipping the Microsoft Store's aliases that
|
||||
@@ -21,7 +22,9 @@ layers: the environment, the stack, what `wikitool` prints (if it starts), and
|
||||
the agent's chronology. Secrets are always removed. Page titles are kept out of
|
||||
everything this script generates unless `--titles` is given. With `--pseudonymise`
|
||||
known identities are replaced by consistent, shape-preserving placeholders (stage 1);
|
||||
`--bundle`/`--candidates` applies the names a model found beyond them (stage 2). The
|
||||
`--bundle`/`--candidates` applies the names a model found beyond them (stage 2).
|
||||
`--bundle`/`--chronology` swaps the chronology of a bundle that exists already, with the
|
||||
same processing and without a second run: no `wikitool` call, no second bundle. The
|
||||
mapping stays beside the bundle, never in it. Nothing is uploaded.
|
||||
|
||||
Exit 0 = bundle written, 1 = it could not be written. No exit 42: this script
|
||||
@@ -1171,15 +1174,25 @@ def build_manifest(args, root, stamp, bundle, wikitool, trace, gaps) -> str:
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def clean_text(text: str, red: Redactor, shield) -> str:
|
||||
"""What every generated or supplied text goes through: secrets, then page titles."""
|
||||
cleaned = red.scrub(text)
|
||||
if shield is not None:
|
||||
cleaned = shield.apply(cleaned)
|
||||
return cleaned
|
||||
|
||||
|
||||
def title_shield(content_paths):
|
||||
return TitleShield(rel for rel in content_paths if rel.rsplit("/", 1)[-1] not in STACK_NAMES)
|
||||
|
||||
|
||||
def final_pass(bundle_dir: Path, red: Redactor, shield, skip) -> None:
|
||||
for path in sorted(bundle_dir.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(bundle_dir).as_posix()
|
||||
text = read_text(path)
|
||||
cleaned = red.scrub(text)
|
||||
if shield is not None and rel not in skip:
|
||||
cleaned = shield.apply(cleaned)
|
||||
cleaned = clean_text(text, red, None if rel in skip else shield)
|
||||
if cleaned != text:
|
||||
write_text(path, cleaned)
|
||||
|
||||
@@ -1210,22 +1223,32 @@ def parse_args(argv):
|
||||
"placeholders (stage 1); the mapping stays beside the bundle")
|
||||
parser.add_argument("--root", help="checkout root (default: parent of tools/)")
|
||||
parser.add_argument("--out", help="output directory (default: <root>/reports)")
|
||||
parser.add_argument("--bundle", help="stage 2: an existing pseudonymised bundle directory")
|
||||
parser.add_argument("--bundle", help="an existing bundle directory: stage 2 with --candidates, "
|
||||
"a new chronology with --chronology")
|
||||
parser.add_argument("--candidates", help="stage 2: file with one further name per line "
|
||||
"('#' starts a comment)")
|
||||
args = parser.parse_args(argv)
|
||||
if bool(args.bundle) != bool(args.candidates):
|
||||
parser.error("--bundle and --candidates belong together")
|
||||
if args.bundle and (args.chronology or args.transcript or args.session or args.no_trace
|
||||
or args.titles or args.pseudonymise or args.root or args.out):
|
||||
parser.error("--bundle/--candidates excludes every collection option")
|
||||
if args.candidates and not args.bundle:
|
||||
parser.error("--candidates belongs with --bundle")
|
||||
if args.bundle:
|
||||
if args.candidates and args.chronology:
|
||||
parser.error("--bundle takes --candidates or --chronology, not both")
|
||||
if not (args.candidates or args.chronology):
|
||||
parser.error("--bundle needs --candidates (stage 2) or --chronology (swap)")
|
||||
if (args.transcript or args.session or args.no_trace or args.titles
|
||||
or args.pseudonymise or args.out or (args.candidates and args.root)):
|
||||
parser.error("--bundle excludes every collection option "
|
||||
"(--root is allowed with --chronology only)")
|
||||
return args
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
args = parse_args(argv)
|
||||
if args.bundle:
|
||||
if args.bundle and args.candidates:
|
||||
return run_stage2(Path(args.bundle), Path(args.candidates))
|
||||
if args.bundle:
|
||||
root = Path(args.root).resolve() if args.root else Path(__file__).resolve().parent.parent
|
||||
return run_exchange(Path(args.bundle), Path(args.chronology), root)
|
||||
root = Path(args.root).resolve() if args.root else Path(__file__).resolve().parent.parent
|
||||
out = Path(args.out).resolve() if args.out else root / "reports"
|
||||
|
||||
@@ -1308,9 +1331,7 @@ def _collect(args, root: Path, out: Path, stamp: str, bundle_dir: Path) -> int:
|
||||
|
||||
shield = None
|
||||
if not args.titles:
|
||||
shield = TitleShield(
|
||||
rel for rel in content_paths if rel.rsplit("/", 1)[-1] not in STACK_NAMES
|
||||
)
|
||||
shield = title_shield(content_paths)
|
||||
final_pass(bundle_dir, red, shield, {"tree-paths.txt"})
|
||||
|
||||
pz = None
|
||||
@@ -1354,25 +1375,30 @@ def parse_candidates(text: str):
|
||||
return names
|
||||
|
||||
|
||||
def load_pseudonymiser(mapping: Path):
|
||||
"""The pseudonymiser stage 1 left beside a bundle, or the reason it cannot be had."""
|
||||
if not mapping.is_file():
|
||||
return None, ("no mapping beside the bundle (%s): stage 2 needs the salt stage 1 used, so "
|
||||
"collect the report again with --pseudonymise" % mapping.name)
|
||||
try:
|
||||
return Pseudonymiser.from_json(json.loads(read_text(mapping))), None
|
||||
except (ValueError, KeyError) as exc:
|
||||
return None, "the mapping cannot be read: %s" % exc
|
||||
|
||||
|
||||
def run_stage2(bundle_dir: Path, candidates: Path) -> int:
|
||||
bundle_dir = bundle_dir.resolve()
|
||||
mapping = _sibling(bundle_dir, ".pseudonyms.json")
|
||||
problem = None
|
||||
pz, problem = None, None
|
||||
if not bundle_dir.is_dir():
|
||||
problem = "bundle directory not found: %s" % bundle_dir
|
||||
elif not candidates.is_file():
|
||||
problem = "candidates file not found: %s" % candidates
|
||||
elif not mapping.is_file():
|
||||
problem = ("no mapping beside the bundle (%s): stage 2 needs the salt stage 1 used, so "
|
||||
"collect the report again with --pseudonymise" % mapping.name)
|
||||
else:
|
||||
pz, problem = load_pseudonymiser(mapping)
|
||||
if problem:
|
||||
sys.stderr.write("bugreport: %s\n" % problem)
|
||||
return 1
|
||||
try:
|
||||
pz = Pseudonymiser.from_json(json.loads(read_text(mapping)))
|
||||
except (ValueError, KeyError) as exc:
|
||||
sys.stderr.write("bugreport: the mapping cannot be read: %s\n" % exc)
|
||||
return 1
|
||||
|
||||
files = text_files(bundle_dir)
|
||||
corpus = "\n".join(read_text(path) for path in files)
|
||||
@@ -1424,6 +1450,105 @@ def run_stage2(bundle_dir: Path, candidates: Path) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
TITLES_LINE = re.compile(r"(?m)^- page titles \(`--titles`\): (included|kept out)$")
|
||||
PSEUDONYMISATION_LINE = re.compile(r"(?m)^- pseudonymisation \(`--pseudonymise`\): (.*)$")
|
||||
CHRONOLOGY_ROW = ("| `CHRONOLOGY.md` | 4 | the agent's chronology | %s |" % MAY_CONTAIN_CONTENT)
|
||||
NO_CHRONOLOGY_GAP = "- no chronology was supplied"
|
||||
|
||||
|
||||
def manifest_with_chronology(manifest: str) -> str:
|
||||
"""The three fixed places a bundle collected without a chronology changes in."""
|
||||
manifest = manifest.replace("- chronology: not supplied", "- chronology: supplied", 1)
|
||||
lines = manifest.split("\n")
|
||||
if CHRONOLOGY_ROW not in lines:
|
||||
end = lines.index("| `MANIFEST.md` | - | this file | |")
|
||||
at = end
|
||||
for index in range(end - 1, -1, -1):
|
||||
if not lines[index].startswith("| `"):
|
||||
break
|
||||
if lines[index].split("`")[1] > "CHRONOLOGY.md":
|
||||
at = index
|
||||
lines.insert(at, CHRONOLOGY_ROW)
|
||||
sections = re.split(r"(?m)^(?=## )", "\n".join(lines))
|
||||
kept = []
|
||||
for section in sections:
|
||||
if section.startswith("## Gaps\n"):
|
||||
bullets = [ln for ln in section.splitlines()[1:] if ln.strip() and ln != NO_CHRONOLOGY_GAP]
|
||||
if not bullets:
|
||||
continue
|
||||
section = "\n".join(["## Gaps", ""] + bullets) + "\n"
|
||||
kept.append(section)
|
||||
return "\n\n".join(part.strip("\n") for part in kept if part.strip()) + "\n"
|
||||
|
||||
|
||||
def run_exchange(bundle_dir: Path, chronology: Path, root: Path) -> int:
|
||||
"""Swap (or add) CHRONOLOGY.md of an existing bundle: no wikitool call, no second bundle."""
|
||||
bundle_dir = bundle_dir.resolve()
|
||||
manifest_path = bundle_dir / "MANIFEST.md"
|
||||
problem, manifest, titles = None, "", None
|
||||
if not bundle_dir.is_dir():
|
||||
problem = "bundle directory not found: %s" % bundle_dir
|
||||
elif not chronology.is_file():
|
||||
problem = "chronology file not found: %s" % chronology
|
||||
elif not manifest_path.is_file():
|
||||
problem = "no MANIFEST.md in %s: not a bundle of this script" % bundle_dir
|
||||
else:
|
||||
manifest = read_text(manifest_path)
|
||||
titles = TITLES_LINE.search(manifest)
|
||||
if titles is None:
|
||||
problem = "MANIFEST.md in %s has no page titles line: not a bundle of this script" % bundle_dir
|
||||
pz, pseudonymisation = None, PSEUDONYMISATION_LINE.search(manifest)
|
||||
if problem is None and pseudonymisation is not None:
|
||||
pz, problem = load_pseudonymiser(_sibling(bundle_dir, ".pseudonyms.json"))
|
||||
if problem:
|
||||
sys.stderr.write("bugreport: %s\n" % problem)
|
||||
return 1
|
||||
|
||||
# Everything is worked out in memory first; nothing is written before it all succeeded.
|
||||
red = Redactor()
|
||||
guarded(collect_environment, root, red)
|
||||
guarded(collect_stack, root, red)
|
||||
shield = None
|
||||
if titles.group(1) == "kept out":
|
||||
shield = title_shield(f for name in CONTENT_DIRS for f in _safe_scan(root, name)[1])
|
||||
text = clean_text(read_text(chronology), red, shield)
|
||||
if pz is not None:
|
||||
text = pz.apply(text)
|
||||
target = bundle_dir / "CHRONOLOGY.md"
|
||||
added = not target.is_file()
|
||||
new_manifest = manifest_with_chronology(manifest) if added else None
|
||||
|
||||
archive = bundle_dir.with_name(bundle_dir.name + ".zip")
|
||||
review = _sibling(bundle_dir, ".review.txt")
|
||||
try:
|
||||
write_text(target, text)
|
||||
if new_manifest is not None:
|
||||
write_text(manifest_path, new_manifest)
|
||||
if pz is not None:
|
||||
write_text(review, build_review(bundle_dir, pz))
|
||||
temporary = archive.with_name(archive.name + ".tmp")
|
||||
make_zip(bundle_dir, temporary)
|
||||
os.replace(str(temporary), str(archive))
|
||||
except OSError as exc:
|
||||
sys.stderr.write("bugreport: cannot write the bundle: %s\n" % exc)
|
||||
return 1
|
||||
|
||||
print("Bundle: %s" % bundle_dir)
|
||||
print("Archive: %s" % archive)
|
||||
if pz is not None:
|
||||
print("Review: %s" % review)
|
||||
print("Read CHRONOLOGY.md and the review list again, then run stage 2: "
|
||||
"tools/bugreport --bundle %s --candidates <file>" % bundle_dir)
|
||||
print()
|
||||
if pz is None:
|
||||
print(PRIVACY_NOTICE)
|
||||
elif "stage 1 and stage 2 applied" in pseudonymisation.group(1):
|
||||
print(RESIDUAL_NOTICE)
|
||||
else:
|
||||
print(STAGE1_NOTICE)
|
||||
return 0
|
||||
|
||||
|
||||
def _safe_scan(root: Path, name: str):
|
||||
try:
|
||||
return scan_tree(root, name)
|
||||
|
||||
@@ -557,3 +557,171 @@ def test_bundle_and_candidates_exclude_the_collection_options(checkout, tmp_path
|
||||
def test_without_the_flag_nothing_but_the_bundle_and_its_archive_is_written(checkout, tmp_path):
|
||||
collect(checkout, tmp_path, "--no-trace")
|
||||
assert sorted(p.suffix for p in (tmp_path / "out").iterdir()) == ["", ".zip"]
|
||||
|
||||
|
||||
# ------------------------------------------------------------ swapping the chronology
|
||||
|
||||
|
||||
def swap(bundle: Path, chronology: Path, *extra: str) -> int:
|
||||
return bugreport.main(["--bundle", str(bundle), "--chronology", str(chronology), *extra])
|
||||
|
||||
|
||||
def entries(out: Path) -> list:
|
||||
return sorted(p.name for p in out.iterdir())
|
||||
|
||||
|
||||
def archive_bytes(bundle: Path) -> bytes:
|
||||
return bundle.with_name(bundle.name + ".zip").read_bytes()
|
||||
|
||||
|
||||
def test_a_swapped_chronology_changes_nothing_else_and_makes_no_second_bundle(checkout, tmp_path):
|
||||
old = tmp_path / "old.md"
|
||||
old.write_text("1. `types describe <entity|concept|...>`\n")
|
||||
bundle = collect(checkout, tmp_path, "--no-trace", "--chronology", str(old))
|
||||
before, listing = snapshot(bundle), entries(tmp_path / "out")
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text("1. `types describe entity` - exit 0\n")
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 0
|
||||
after = snapshot(bundle)
|
||||
assert after.pop("CHRONOLOGY.md") == b"1. `types describe entity` - exit 0\n"
|
||||
assert before.pop("CHRONOLOGY.md") != b"1. `types describe entity` - exit 0\n"
|
||||
assert after == before # wikitool/*.txt and MANIFEST.md included
|
||||
assert entries(tmp_path / "out") == listing
|
||||
assert "types describe entity` - exit 0" in zip_text(bundle)
|
||||
assert "<entity|concept" not in zip_text(bundle)
|
||||
|
||||
|
||||
def test_a_swapped_chronology_is_scrubbed_of_a_secret_from_a_config_file(checkout, tmp_path):
|
||||
bundle = collect(checkout, tmp_path, "--no-trace")
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text(f"1. Setup failed with {TOKEN} in the header\n")
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 0
|
||||
text = (bundle / "CHRONOLOGY.md").read_text()
|
||||
assert TOKEN not in text and "1. Setup failed" in text
|
||||
assert TOKEN not in zip_text(bundle)
|
||||
|
||||
|
||||
def test_a_swapped_chronology_keeps_titles_out_unless_the_bundle_was_collected_with_them(
|
||||
checkout, tmp_path
|
||||
):
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text(f"see kb/Concepts/{TITLE}.md and [[{OTHER_TITLE}]]\n")
|
||||
hidden = collect(checkout, tmp_path / "a", "--no-trace")
|
||||
assert swap(hidden, new, "--root", str(checkout)) == 0
|
||||
text = (hidden / "CHRONOLOGY.md").read_text()
|
||||
assert TITLE not in text and OTHER_TITLE not in text and "[[<title" in text
|
||||
|
||||
shown = collect(checkout, tmp_path / "b", "--no-trace", "--titles")
|
||||
assert swap(shown, new, "--root", str(checkout)) == 0
|
||||
assert TITLE in (shown / "CHRONOLOGY.md").read_text()
|
||||
|
||||
|
||||
def test_a_swapped_chronology_in_a_pseudonymised_bundle_uses_both_stages_and_the_same_mapping(
|
||||
checkout, tmp_path, monkeypatch, capsys
|
||||
):
|
||||
first = tmp_path / "first.md"
|
||||
first.write_text("Kunde Acme Corp zahlt.\n")
|
||||
bundle = pseudonymised(checkout, tmp_path, monkeypatch, "--chronology", str(first))
|
||||
candidates = tmp_path / "candidates.txt"
|
||||
candidates.write_text("Acme Corp\n")
|
||||
assert bugreport.main(["--bundle", str(bundle), "--candidates", str(candidates)]) == 0
|
||||
stage_two_word = (bundle / "CHRONOLOGY.md").read_text().split()[1]
|
||||
mapping, review = mapping_of(bundle).read_bytes(), bundle.with_name(bundle.name + ".review.txt")
|
||||
review.write_text("stale\n")
|
||||
manifest_before = (bundle / "MANIFEST.md").read_bytes()
|
||||
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text("Acme Corp hat Erika Mustermann angerufen\n")
|
||||
capsys.readouterr()
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 0
|
||||
text = (bundle / "CHRONOLOGY.md").read_text()
|
||||
for original in ("Acme", "Erika", "Mustermann"):
|
||||
assert original not in text and original not in zip_text(bundle)
|
||||
assert text.startswith(stage_two_word + " ") # same placeholder as the stage 2 run gave
|
||||
assert mapping_of(bundle).read_bytes() == mapping
|
||||
assert (bundle / "MANIFEST.md").read_bytes() == manifest_before
|
||||
assert review.read_text() != "stale\n"
|
||||
printed = capsys.readouterr().out
|
||||
assert "Review:" in printed and "stage 1 and stage 2 applied" in printed
|
||||
|
||||
|
||||
def test_a_pseudonymised_bundle_without_its_mapping_is_left_as_it_was(
|
||||
checkout, tmp_path, monkeypatch, capsys
|
||||
):
|
||||
bundle = pseudonymised(checkout, tmp_path, monkeypatch)
|
||||
mapping_of(bundle).unlink()
|
||||
before, archive = snapshot(bundle), archive_bytes(bundle)
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text("Erika\n")
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 1
|
||||
assert "collect the report again" in capsys.readouterr().err
|
||||
assert snapshot(bundle) == before and archive_bytes(bundle) == archive
|
||||
|
||||
|
||||
def test_a_swap_adds_a_missing_chronology_and_touches_the_manifest_in_three_places(
|
||||
checkout, tmp_path
|
||||
):
|
||||
bundle = collect(checkout, tmp_path, "--no-trace")
|
||||
assert not (bundle / "CHRONOLOGY.md").exists()
|
||||
before = snapshot(bundle)
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text("1. `doctor` - exit 1\n")
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 0
|
||||
after = snapshot(bundle)
|
||||
assert after.pop("CHRONOLOGY.md") == b"1. `doctor` - exit 1\n"
|
||||
manifest_before, manifest_after = before.pop("MANIFEST.md"), after.pop("MANIFEST.md")
|
||||
assert after == before
|
||||
old_lines, new_lines = manifest_before.decode().splitlines(), manifest_after.decode().splitlines()
|
||||
removed = [ln for ln in old_lines if ln not in new_lines]
|
||||
added = [ln for ln in new_lines if ln not in old_lines]
|
||||
assert sorted(removed) == ["## Gaps", "- chronology: not supplied", "- no chronology was supplied"]
|
||||
assert sorted(added) == [
|
||||
"- chronology: supplied",
|
||||
"| `CHRONOLOGY.md` | 4 | the agent's chronology | may contain page content and titles |",
|
||||
]
|
||||
files = new_lines[new_lines.index("## Files"):]
|
||||
assert files[4].startswith("| `CHRONOLOGY.md`") # sorted like build_manifest sorts
|
||||
assert new_lines[-1] == "| `MANIFEST.md` | - | this file | |"
|
||||
assert "`doctor` - exit 1" in zip_text(bundle)
|
||||
|
||||
|
||||
def test_a_swap_keeps_the_other_gaps(checkout, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(bugreport, "launcher_command", lambda root: None)
|
||||
bundle = collect(checkout, tmp_path, "--no-trace")
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text("text\n")
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 0
|
||||
manifest = (bundle / "MANIFEST.md").read_text()
|
||||
assert "## Gaps" in manifest and "no launcher found" in manifest
|
||||
assert "no chronology was supplied" not in manifest
|
||||
|
||||
|
||||
def test_a_swap_with_a_missing_file_or_no_manifest_changes_nothing(checkout, tmp_path, capsys):
|
||||
bundle = collect(checkout, tmp_path, "--no-trace")
|
||||
before, archive = snapshot(bundle), archive_bytes(bundle)
|
||||
assert swap(bundle, tmp_path / "nowhere.md", "--root", str(checkout)) == 1
|
||||
assert "chronology file not found" in capsys.readouterr().err
|
||||
new = tmp_path / "new.md"
|
||||
new.write_text("text\n")
|
||||
(bundle / "MANIFEST.md").unlink()
|
||||
before.pop("MANIFEST.md")
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 1
|
||||
assert "MANIFEST.md" in capsys.readouterr().err
|
||||
(bundle / "MANIFEST.md").write_text("# Bug report bundle\n")
|
||||
assert swap(bundle, new, "--root", str(checkout)) == 1
|
||||
assert "not a bundle of this script" in capsys.readouterr().err
|
||||
assert not (bundle / "CHRONOLOGY.md").exists()
|
||||
assert archive_bytes(bundle) == archive
|
||||
|
||||
|
||||
def test_bundle_takes_exactly_one_of_candidates_and_chronology(checkout, tmp_path):
|
||||
with pytest.raises(SystemExit):
|
||||
bugreport.main(["--bundle", "b", "--candidates", "c", "--chronology", "x"])
|
||||
with pytest.raises(SystemExit):
|
||||
bugreport.main(["--bundle", "b"])
|
||||
for extra in (["--no-trace"], ["--titles"], ["--pseudonymise"], ["--session", "s"],
|
||||
["--transcript", "t"], ["--out", str(tmp_path)]):
|
||||
with pytest.raises(SystemExit):
|
||||
bugreport.main(["--bundle", "b", "--chronology", "x", *extra])
|
||||
with pytest.raises(SystemExit):
|
||||
bugreport.main(["--bundle", "b", "--candidates", "c", "--root", str(checkout)])
|
||||
Reference in new issue
Block a user