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.
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/PKG-INFO +1 -1
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/pyproject.toml +1 -1
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/cli.py +142 -6
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/config.py +14 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/PKG-INFO +1 -1
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/SOURCES.txt +1 -0
- codetour_cli-0.2.0/tests/test_review_workflow_options.py +170 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/LICENSE +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/README.md +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/setup.cfg +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/__init__.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/description_validator.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/detection.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/discovery.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/lint.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/logging/__init__.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/logging/setup.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/__init__.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/direct.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/git_diff.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/migration/stepwise.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/review_parser.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/schema_models.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/tour/__init__.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/tour/schema.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli/tour/updater.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/dependency_links.txt +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/entry_points.txt +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/requires.txt +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/src/codetour_cli.egg-info/top_level.txt +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_advanced_scenarios.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_apply_review_surgical_integration.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_description_validation.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_detection.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_direct_migration.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_fuzzy_repo_calibration.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_git_diff_parser.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_harness.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_integration.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_lint.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_lint_cli.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_logging_setup.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_pattern_validation.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_post_migration_lint.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_review_roundtrip.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_scenarios.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_schema_validation.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_stepwise_migration.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_synthetic_repo_migration_engine.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_templates.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_tour_schema.py +0 -0
- {codetour_cli-0.1.0 → codetour_cli-0.2.0}/tests/test_tour_updater.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codetour-cli"
|
|
7
|
-
version = "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
|
-
#
|
|
1455
|
-
|
|
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
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|