codetour-cli 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/PKG-INFO +1 -1
  2. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/pyproject.toml +1 -1
  3. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/cli.py +142 -6
  4. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/config.py +14 -0
  5. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/PKG-INFO +1 -1
  6. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/SOURCES.txt +1 -0
  7. codetour_cli-0.2.0/tests/test_review_workflow_options.py +170 -0
  8. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/LICENSE +0 -0
  9. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/README.md +0 -0
  10. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/setup.cfg +0 -0
  11. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/__init__.py +0 -0
  12. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/description_validator.py +0 -0
  13. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/detection.py +0 -0
  14. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/discovery.py +0 -0
  15. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/lint.py +0 -0
  16. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/logging/__init__.py +0 -0
  17. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/logging/setup.py +0 -0
  18. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/__init__.py +0 -0
  19. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/direct.py +0 -0
  20. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/git_diff.py +0 -0
  21. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/stepwise.py +0 -0
  22. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/review_parser.py +0 -0
  23. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/schema_models.py +0 -0
  24. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/tour/__init__.py +0 -0
  25. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/tour/schema.py +0 -0
  26. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/tour/updater.py +0 -0
  27. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/dependency_links.txt +0 -0
  28. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/entry_points.txt +0 -0
  29. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/requires.txt +0 -0
  30. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/top_level.txt +0 -0
  31. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_advanced_scenarios.py +0 -0
  32. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_apply_review_surgical_integration.py +0 -0
  33. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_description_validation.py +0 -0
  34. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_detection.py +0 -0
  35. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_direct_migration.py +0 -0
  36. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_fuzzy_repo_calibration.py +0 -0
  37. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_git_diff_parser.py +0 -0
  38. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_harness.py +0 -0
  39. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_integration.py +0 -0
  40. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_lint.py +0 -0
  41. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_lint_cli.py +0 -0
  42. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_logging_setup.py +0 -0
  43. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_pattern_validation.py +0 -0
  44. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_post_migration_lint.py +0 -0
  45. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_review_roundtrip.py +0 -0
  46. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_scenarios.py +0 -0
  47. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_schema_validation.py +0 -0
  48. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_stepwise_migration.py +0 -0
  49. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_synthetic_repo_migration_engine.py +0 -0
  50. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_templates.py +0 -0
  51. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_tour_schema.py +0 -0
  52. {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_tour_updater.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codetour-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension
5
5
  Author: Jérémie Lumbroso
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codetour-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension"
9
9
  authors = [{name = "Jérémie Lumbroso"}]
10
10
  readme = "README.md"
@@ -319,6 +319,77 @@ def load_parsing_schema(report_content: str) -> ParsingSchema:
319
319
  return schema
320
320
 
321
321
 
322
+ def select_review_steps(review, auto_approve_above: Optional[float] = None):
323
+ """
324
+ Decide which parsed review steps to apply.
325
+
326
+ Base set: steps the human checked (the report workflow's contract).
327
+ With auto_approve_above set (0.0-1.0), UNCHECKED steps whose confidence
328
+ clears the threshold are promoted in as well -- except steps marked for
329
+ deletion, which are never auto-approved: removing a step from a tour is
330
+ destructive and stays behind an explicit human checkbox regardless of
331
+ confidence (the same never-auto principle the migrate path applies).
332
+
333
+ Returns (selected_steps, promoted_steps) -- promoted ⊆ selected.
334
+ """
335
+ selected = list(review.get_corrected_steps())
336
+ promoted = []
337
+ if auto_approve_above is not None:
338
+ cutoff = auto_approve_above * 100.0
339
+ already = {s.step_number for s in selected}
340
+ for r in review.step_reviews:
341
+ if r.step_number in already or r.status_checked:
342
+ continue
343
+ if r.is_marked_for_deletion():
344
+ continue
345
+ if r.confidence is not None and r.confidence >= cutoff:
346
+ promoted.append(r)
347
+ selected.extend(promoted)
348
+ selected.sort(key=lambda s: s.step_number)
349
+ return selected, promoted
350
+
351
+
352
+ def clean_stale_review_reports(
353
+ tours_dir: Path,
354
+ tour_name: str,
355
+ keep: Optional[Path],
356
+ dry_run: bool = False,
357
+ ) -> list:
358
+ """
359
+ Remove MIGRATION-REVIEW-{tour_name}-*.md reports for OTHER target
360
+ commits than the current run's (whose report, if any, is `keep`).
361
+
362
+ Matching is by literal prefix/suffix on the filename, deliberately not
363
+ glob (tour names are user-controlled; glob metacharacters in a name
364
+ must not widen the match). In dry_run, returns the candidates without
365
+ deleting. Returns the list of removed (or would-remove) paths.
366
+ """
367
+ prefix = f"MIGRATION-REVIEW-{tour_name}-"
368
+ removed = []
369
+ if not tours_dir.is_dir():
370
+ return removed
371
+ for p in sorted(tours_dir.iterdir()):
372
+ if not (p.is_file() and p.name.startswith(prefix) and p.name.endswith(".md")):
373
+ continue
374
+ if keep is not None and p.name == keep.name:
375
+ continue
376
+ if not dry_run:
377
+ p.unlink()
378
+ removed.append(p)
379
+ return removed
380
+
381
+
382
+ def resolve_context_lines(args, config: dict) -> int:
383
+ """CLI flag wins over config review.context_lines; default 7; floor 0."""
384
+ cli_val = getattr(args, 'context_lines', None)
385
+ if cli_val is not None:
386
+ return max(0, cli_val)
387
+ try:
388
+ return max(0, int(config.get('review', {}).get('context_lines', 7)))
389
+ except (TypeError, ValueError):
390
+ return 7
391
+
392
+
322
393
  def generate_review_report(
323
394
  tour_path: Path,
324
395
  result,
@@ -327,7 +398,8 @@ def generate_review_report(
327
398
  repo_root: Path,
328
399
  is_dry_run: bool = False,
329
400
  no_guidance: bool = False,
330
- threshold: Optional[float] = None
401
+ threshold: Optional[float] = None,
402
+ context_lines: int = 7
331
403
  ) -> Optional[Path]:
332
404
  """
333
405
  Generate a markdown review report for migrations with low-confidence steps.
@@ -469,8 +541,8 @@ def generate_review_report(
469
541
  new_line = None
470
542
 
471
543
  # Get code context for old and new locations (from specific commits)
472
- old_context = get_code_context(repo_root, old_file, old_line, source_commit) if old_line else "(No line number)"
473
- new_context = get_code_context(repo_root, new_file, new_line, target_commit) if new_line and update.new_location else "(File deleted)"
544
+ old_context = get_code_context(repo_root, old_file, old_line, source_commit, context_lines=context_lines) if old_line else "(No line number)"
545
+ new_context = get_code_context(repo_root, new_file, new_line, target_commit, context_lines=context_lines) if new_line and update.new_location else "(File deleted)"
474
546
 
475
547
  # Quote the description with chevrons
476
548
  quoted_description = quote_with_chevrons(step.description)
@@ -794,6 +866,22 @@ Examples:
794
866
  dest='auto_approve',
795
867
  help='Automatically approve all changes (skip confirmation prompts)'
796
868
  )
869
+ apply_review_parser.add_argument(
870
+ '--auto-approve-above',
871
+ type=float,
872
+ dest='auto_approve_above',
873
+ metavar='X',
874
+ help=(
875
+ 'Additionally apply UNCHECKED steps whose confidence >= X '
876
+ '(0.0-1.0), on top of the checked ones. Steps marked for '
877
+ 'deletion are never auto-approved -- destructive actions stay '
878
+ 'behind an explicit human checkbox. Note: confidence only ever '
879
+ 'takes 5 exact values (see docs/adr/0016-*.md), so a threshold '
880
+ 'between quanta behaves like the nearest quantum above it. '
881
+ 'ADR-0003\'s "batch approve all above confidence X", in the '
882
+ 'report-based workflow that ADR-0007 shipped.'
883
+ )
884
+ )
797
885
 
798
886
  # migrate command
799
887
  migrate_parser = subparsers.add_parser(
@@ -860,6 +948,27 @@ Examples:
860
948
  dest='no_lint',
861
949
  help='Skip the advisory post-migration lint pass (ADR-0014)'
862
950
  )
951
+ migrate_parser.add_argument(
952
+ '--clean-reviews',
953
+ action='store_true',
954
+ dest='clean_reviews',
955
+ help=(
956
+ 'After migrating each tour, delete stale MIGRATION-REVIEW-'
957
+ '{tour}-*.md reports left over from earlier target commits '
958
+ '(the report for THIS run, if any, is kept). In --dry-run, '
959
+ 'lists what would be cleaned without deleting.'
960
+ )
961
+ )
962
+ migrate_parser.add_argument(
963
+ '--context-lines',
964
+ type=int,
965
+ dest='context_lines',
966
+ metavar='N',
967
+ help=(
968
+ 'Lines of code context above/below each reviewed line in the '
969
+ 'review report (default: from config review.context_lines, 7)'
970
+ )
971
+ )
863
972
 
864
973
  args = parser.parse_args()
865
974
 
@@ -1451,13 +1560,24 @@ def apply_review_command(args, config: dict) -> int:
1451
1560
  print(f"❌ Failed to load tour: {e}", file=sys.stderr)
1452
1561
  return 2
1453
1562
 
1454
- # Get only reviewed steps (checkbox checked)
1455
- corrected_steps = review.get_corrected_steps()
1563
+ # Reviewed steps (checkbox checked), plus -- with --auto-approve-above X --
1564
+ # unchecked steps whose confidence clears the threshold (never deletions).
1565
+ auto_above = getattr(args, 'auto_approve_above', None)
1566
+ if auto_above is not None and not (0.0 <= auto_above <= 1.0):
1567
+ print(f"❌ --auto-approve-above must be between 0.0 and 1.0 (got {auto_above})", file=sys.stderr)
1568
+ return 2
1569
+
1570
+ corrected_steps, promoted_steps = select_review_steps(review, auto_above)
1571
+ if promoted_steps:
1572
+ nums = ", ".join(str(s.step_number) for s in promoted_steps)
1573
+ print(f"🔓 Auto-approving {len(promoted_steps)} unchecked step(s) at confidence ≥ {auto_above:.0%}: {nums}")
1456
1574
 
1457
1575
  if not corrected_steps:
1458
1576
  print("ℹ️ No corrections found in review report")
1459
1577
  print(" The report may not have been edited, or no checkboxes were marked")
1460
1578
  print(" Tip: Check the 'Status' checkbox for steps you want to apply")
1579
+ if auto_above is not None:
1580
+ print(f" (--auto-approve-above {auto_above} promoted nothing: no unchecked, non-deletion step at that confidence)")
1461
1581
  return 0
1462
1582
 
1463
1583
  # Apply corrections
@@ -1849,6 +1969,7 @@ def migrate_all_tours(args, config: dict) -> int:
1849
1969
  print_migration_summary(result, tour_name, is_dry_run)
1850
1970
 
1851
1971
  # Generate review report if needed
1972
+ report_path = None
1852
1973
  if result.num_needs_review > 0:
1853
1974
  report_path = generate_review_report(
1854
1975
  tour_path=tour_info.path,
@@ -1858,7 +1979,8 @@ def migrate_all_tours(args, config: dict) -> int:
1858
1979
  repo_root=repo_info.repo_path,
1859
1980
  is_dry_run=is_dry_run,
1860
1981
  no_guidance=getattr(args, 'no_guidance', False),
1861
- threshold=threshold
1982
+ threshold=threshold,
1983
+ context_lines=resolve_context_lines(args, config)
1862
1984
  )
1863
1985
  if report_path:
1864
1986
  review_reports.append(report_path)
@@ -1867,6 +1989,20 @@ def migrate_all_tours(args, config: dict) -> int:
1867
1989
  else:
1868
1990
  print(f" 📄 Review report: {report_path.relative_to(repo_info.repo_path)}")
1869
1991
 
1992
+ # Optionally clean stale reports from earlier target commits
1993
+ # (keyed on the just-generated report's actual filename -- never
1994
+ # re-derived, since to_commit may be symbolic like "HEAD").
1995
+ if getattr(args, 'clean_reviews', False):
1996
+ stale = clean_stale_review_reports(
1997
+ tours_dir=tour_info.path.parent,
1998
+ tour_name=tour_info.path.stem,
1999
+ keep=report_path,
2000
+ dry_run=is_dry_run,
2001
+ )
2002
+ if stale:
2003
+ verb = "Would clean" if is_dry_run else "Cleaned"
2004
+ print(f" 🧹 {verb} {len(stale)} stale review report(s): " + ", ".join(p.name for p in stale))
2005
+
1870
2006
  except Exception as e:
1871
2007
  log.error("migration_failed", tour=tour_name, error=str(e))
1872
2008
  print(f" ❌ Failed: {e}\n")
@@ -24,6 +24,14 @@ DEFAULT_CONFIG = {
24
24
  "display": {
25
25
  "color": True,
26
26
  "verbosity": "normal",
27
+ },
28
+ "review": {
29
+ # Lines of code context above/below each reviewed line in review
30
+ # reports. Ratified at 7 (the shipped default) over ADR-0007's
31
+ # original ±3 recommendation, made configurable per Jérémie's
32
+ # 2026-08-29 answer ("so we don't leave people... without a way
33
+ # to tweak the tool to their context").
34
+ "context_lines": 7,
27
35
  }
28
36
  }
29
37
 
@@ -57,6 +65,12 @@ display:
57
65
 
58
66
  # Verbosity level (quiet, normal, verbose)
59
67
  verbosity: normal
68
+
69
+ # Review report preferences
70
+ review:
71
+ # Lines of code context above/below each reviewed line (override per
72
+ # run with `migrate --context-lines N`)
73
+ context_lines: 7
60
74
  """
61
75
 
62
76
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codetour-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension
5
5
  Author: Jérémie Lumbroso
6
6
  License: MIT
@@ -40,6 +40,7 @@ tests/test_logging_setup.py
40
40
  tests/test_pattern_validation.py
41
41
  tests/test_post_migration_lint.py
42
42
  tests/test_review_roundtrip.py
43
+ tests/test_review_workflow_options.py
43
44
  tests/test_scenarios.py
44
45
  tests/test_schema_validation.py
45
46
  tests/test_stepwise_migration.py
@@ -0,0 +1,170 @@
1
+ """Tests for the answer-driven review-workflow options (2026-08-29).
2
+
3
+ ADR-0003's QST-REVIEW-INTERFACE-FUTURE and ADR-0007's three QSTs were
4
+ answered by Jérémie on 2026-08-29; this file covers the three features
5
+ those answers approved:
6
+
7
+ - `apply-review --auto-approve-above X` -- ADR-0003's decided "batch
8
+ approve all above confidence X", delivered inside the report-based
9
+ workflow ADR-0007 shipped. Selection semantics tested at the function
10
+ seam (`select_review_steps`) with the real parser dataclasses.
11
+ - `migrate --clean-reviews` -- stale-report cleanup (ADR-0007
12
+ QST "Report persistence", Jérémie: "the excellent idea of a
13
+ `--clean-reviews` option"). Tested against real files on disk.
14
+ - configurable review context lines (ADR-0007 QST "Code snippet
15
+ length", Jérémie: "would love if this could be customizable").
16
+ """
17
+
18
+ from types import SimpleNamespace
19
+
20
+ from codetour_cli.cli import (
21
+ clean_stale_review_reports,
22
+ resolve_context_lines,
23
+ select_review_steps,
24
+ )
25
+ from codetour_cli.review_parser import ReviewReport, StepReview
26
+
27
+
28
+ def _step(n, checked=False, confidence=None, file="src/x.py", line=10):
29
+ return StepReview(
30
+ step_number=n,
31
+ status_checked=checked,
32
+ tentative_file=file,
33
+ tentative_line=line,
34
+ confidence=confidence,
35
+ )
36
+
37
+
38
+ def _report(steps):
39
+ return ReviewReport(
40
+ tour_name="demo.tour",
41
+ source_commit="aaaa1111",
42
+ target_commit="bbbb2222",
43
+ mode="live",
44
+ step_reviews=steps,
45
+ )
46
+
47
+
48
+ class TestSelectReviewSteps:
49
+ def test_without_threshold_only_checked_steps_apply(self):
50
+ review = _report([_step(1, checked=True), _step(2, confidence=95)])
51
+ selected, promoted = select_review_steps(review, None)
52
+ assert [s.step_number for s in selected] == [1]
53
+ assert promoted == []
54
+
55
+ def test_promotes_unchecked_steps_at_or_above_cutoff(self):
56
+ review = _report([
57
+ _step(1, checked=True, confidence=50),
58
+ _step(2, confidence=85), # promoted at 0.8
59
+ _step(3, confidence=50), # below cutoff, stays out
60
+ _step(4, confidence=80), # exactly at cutoff: >= semantics
61
+ ])
62
+ selected, promoted = select_review_steps(review, 0.8)
63
+ assert [s.step_number for s in selected] == [1, 2, 4]
64
+ assert [s.step_number for s in promoted] == [2, 4]
65
+
66
+ def test_deletion_marked_steps_are_never_auto_promoted(self):
67
+ # Removing a step from a tour is destructive -- confidence must not
68
+ # bypass the explicit human checkbox, no matter how high.
69
+ deletion = _step(2, confidence=99, file="-", line=None)
70
+ assert deletion.is_marked_for_deletion()
71
+ review = _report([_step(1, confidence=90), deletion])
72
+ selected, promoted = select_review_steps(review, 0.5)
73
+ assert [s.step_number for s in selected] == [1]
74
+ assert deletion not in selected
75
+
76
+ def test_checked_deletion_still_applies_as_before(self):
77
+ # The human explicitly checked it: pre-existing behavior unchanged.
78
+ deletion = _step(2, checked=True, file="-", line=None)
79
+ review = _report([deletion])
80
+ selected, _ = select_review_steps(review, 0.9)
81
+ assert selected == [deletion]
82
+
83
+ def test_confidence_none_is_never_promoted(self):
84
+ review = _report([_step(1, confidence=None)])
85
+ selected, promoted = select_review_steps(review, 0.0)
86
+ assert selected == [] and promoted == []
87
+
88
+ def test_selection_is_ordered_by_step_number(self):
89
+ review = _report([
90
+ _step(5, confidence=90),
91
+ _step(1, checked=True),
92
+ _step(3, confidence=90),
93
+ ])
94
+ selected, _ = select_review_steps(review, 0.5)
95
+ assert [s.step_number for s in selected] == [1, 3, 5]
96
+
97
+
98
+ class TestCleanStaleReviewReports:
99
+ def _seed(self, tours_dir):
100
+ names = [
101
+ "MIGRATION-REVIEW-demo-aaaa1111.md", # stale (other commit)
102
+ "MIGRATION-REVIEW-demo-bbbb2222.md", # current run's report
103
+ "MIGRATION-REVIEW-demo-cccc3333.md", # stale (other commit)
104
+ "MIGRATION-REVIEW-other-dddd4444.md", # different tour: untouched
105
+ "MIGRATION-REVIEW-demo-notes.txt", # not .md: untouched
106
+ ]
107
+ for n in names:
108
+ (tours_dir / n).write_text("x", encoding="utf-8")
109
+ return names
110
+
111
+ def test_removes_only_this_tours_stale_md_reports(self, tmp_path):
112
+ self._seed(tmp_path)
113
+ keep = tmp_path / "MIGRATION-REVIEW-demo-bbbb2222.md"
114
+ removed = clean_stale_review_reports(tmp_path, "demo", keep=keep)
115
+ assert sorted(p.name for p in removed) == [
116
+ "MIGRATION-REVIEW-demo-aaaa1111.md",
117
+ "MIGRATION-REVIEW-demo-cccc3333.md",
118
+ ]
119
+ assert keep.exists()
120
+ assert (tmp_path / "MIGRATION-REVIEW-other-dddd4444.md").exists()
121
+ assert (tmp_path / "MIGRATION-REVIEW-demo-notes.txt").exists()
122
+ assert not (tmp_path / "MIGRATION-REVIEW-demo-aaaa1111.md").exists()
123
+
124
+ def test_keep_none_cleans_every_report_for_the_tour(self, tmp_path):
125
+ # A run that needed no review generates no report: everything
126
+ # stale for that tour goes.
127
+ self._seed(tmp_path)
128
+ removed = clean_stale_review_reports(tmp_path, "demo", keep=None)
129
+ assert len(removed) == 3
130
+ assert (tmp_path / "MIGRATION-REVIEW-other-dddd4444.md").exists()
131
+
132
+ def test_dry_run_lists_candidates_without_deleting(self, tmp_path):
133
+ self._seed(tmp_path)
134
+ keep = tmp_path / "MIGRATION-REVIEW-demo-bbbb2222.md"
135
+ removed = clean_stale_review_reports(tmp_path, "demo", keep=keep, dry_run=True)
136
+ assert len(removed) == 2
137
+ for p in removed:
138
+ assert p.exists()
139
+
140
+ def test_prefix_match_is_literal_not_glob(self, tmp_path):
141
+ # A tour named with glob metacharacters must only match itself --
142
+ # glob would let "a[b]" match "ab" and delete another tour's report.
143
+ (tmp_path / "MIGRATION-REVIEW-a[b]-aaaa1111.md").write_text("x", encoding="utf-8")
144
+ (tmp_path / "MIGRATION-REVIEW-ab-aaaa1111.md").write_text("x", encoding="utf-8")
145
+ removed = clean_stale_review_reports(tmp_path, "a[b]", keep=None)
146
+ assert [p.name for p in removed] == ["MIGRATION-REVIEW-a[b]-aaaa1111.md"]
147
+ assert (tmp_path / "MIGRATION-REVIEW-ab-aaaa1111.md").exists()
148
+
149
+ def test_missing_tours_dir_is_a_noop(self, tmp_path):
150
+ assert clean_stale_review_reports(tmp_path / "absent", "demo", keep=None) == []
151
+
152
+
153
+ class TestResolveContextLines:
154
+ def test_cli_flag_wins_over_config(self):
155
+ args = SimpleNamespace(context_lines=3)
156
+ assert resolve_context_lines(args, {"review": {"context_lines": 12}}) == 3
157
+
158
+ def test_config_used_when_no_flag(self):
159
+ args = SimpleNamespace(context_lines=None)
160
+ assert resolve_context_lines(args, {"review": {"context_lines": 12}}) == 12
161
+
162
+ def test_default_is_the_ratified_seven(self):
163
+ assert resolve_context_lines(SimpleNamespace(context_lines=None), {}) == 7
164
+
165
+ def test_negative_values_floor_to_zero(self):
166
+ assert resolve_context_lines(SimpleNamespace(context_lines=-4), {}) == 0
167
+
168
+ def test_garbage_config_falls_back_to_seven(self):
169
+ args = SimpleNamespace(context_lines=None)
170
+ assert resolve_context_lines(args, {"review": {"context_lines": "many"}}) == 7
File without changes
File without changes
File without changes