claude-dev-env 1.36.2 → 1.37.0

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 (70) hide show
  1. package/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
  2. package/_shared/pr-loop/scripts/preflight.py +242 -20
  3. package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
  4. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
  5. package/hooks/blocking/code_rules_enforcer.py +269 -23
  6. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
  7. package/hooks/config/test_unused_module_import_constants.py +48 -0
  8. package/hooks/config/unused_module_import_constants.py +41 -0
  9. package/package.json +1 -1
  10. package/skills/bg-agent/SKILL.md +69 -0
  11. package/skills/bugteam/CONSTRAINTS.md +10 -19
  12. package/skills/bugteam/PROMPTS.md +3 -3
  13. package/skills/bugteam/SKILL.md +103 -202
  14. package/skills/bugteam/SKILL_EVALS.md +75 -114
  15. package/skills/bugteam/reference/README.md +2 -4
  16. package/skills/bugteam/reference/design-rationale.md +3 -8
  17. package/skills/bugteam/reference/team-setup.md +11 -19
  18. package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
  19. package/skills/bugteam/scripts/config/__init__.py +0 -0
  20. package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
  21. package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
  22. package/skills/bugteam/sources.md +1 -25
  23. package/skills/bugteam/test_skill_additions.py +4 -13
  24. package/skills/fresh-branch/SKILL.md +71 -0
  25. package/skills/gotcha/SKILL.md +73 -0
  26. package/skills/monitor-open-prs/SKILL.md +4 -37
  27. package/skills/monitor-open-prs/test_skill_contract.py +0 -5
  28. package/skills/pr-converge/SKILL.md +60 -1298
  29. package/skills/pr-converge/reference/convergence-gates.md +118 -0
  30. package/skills/pr-converge/reference/examples.md +76 -0
  31. package/skills/pr-converge/reference/fix-protocol.md +54 -0
  32. package/skills/pr-converge/reference/ground-rules.md +13 -0
  33. package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
  34. package/skills/pr-converge/reference/per-tick.md +201 -0
  35. package/skills/pr-converge/reference/state-schema.md +19 -0
  36. package/skills/pr-converge/reference/stop-conditions.md +26 -0
  37. package/skills/pr-converge/scripts/README.md +36 -9
  38. package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
  39. package/skills/pr-converge/scripts/config/pr_converge_constants.py +58 -5
  40. package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
  41. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
  42. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
  43. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
  44. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
  45. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
  46. package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
  47. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
  48. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
  49. package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
  50. package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
  51. package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
  52. package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
  53. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
  54. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
  55. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
  56. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
  57. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
  58. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
  59. package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
  60. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
  61. package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
  62. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
  63. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
  64. package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
  65. package/skills/bugteam/test_team_lifecycle.py +0 -103
  66. package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
  67. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
  68. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
  69. package/skills/pr-converge/test_team_lifecycle.py +0 -56
  70. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
@@ -9,11 +9,12 @@ Covers:
9
9
 
10
10
  import importlib.util
11
11
  import inspect
12
+ import os
12
13
  import subprocess
13
14
  import sys
14
15
  from pathlib import Path
15
16
  from types import ModuleType
16
- from unittest.mock import MagicMock, patch
17
+ from unittest.mock import ANY, MagicMock, patch
17
18
 
18
19
  import pytest
19
20
 
@@ -30,7 +31,10 @@ def _load_preflight_module() -> ModuleType:
30
31
 
31
32
  preflight = _load_preflight_module()
32
33
 
33
- from config.preflight_constants import PYTEST_NO_TESTS_COLLECTED_EXIT_CODE # noqa: E402
34
+ from config.preflight_constants import ( # noqa: E402
35
+ PYTEST_INI_FILENAME,
36
+ PYTEST_NO_TESTS_COLLECTED_EXIT_CODE,
37
+ )
34
38
 
35
39
 
36
40
  def _make_completed_process(
@@ -259,38 +263,18 @@ def test_loop_variables_use_each_prefix_in_preflight_module() -> None:
259
263
  assert "for each_candidate in" in find_root_source
260
264
 
261
265
  discover_tests_source = inspect.getsource(preflight.has_discoverable_tests)
262
- assert "for each_path in" in discover_tests_source
263
- assert "for each_part in" in discover_tests_source
266
+ assert "ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND" in discover_tests_source
264
267
 
265
268
 
266
269
  def test_preflight_uses_extracted_directory_marker_constants() -> None:
267
- """preflight.py must reference extracted constants instead of inline string literals.
268
-
269
- The CODE_RULES magic-values rule treats inline ``.git`` and ``.venv``
270
- string literals in production function bodies as violations. Confirm
271
- preflight.py imports them (or a frozenset that contains ``.venv``) from
272
- config.preflight_constants instead.
273
- """
274
270
  preflight_source = inspect.getsource(preflight)
275
271
  assert "GIT_DIRECTORY_NAME" in preflight_source
276
- assert "ALL_TESTS_DIRECTORY_IGNORE_PARTS" in preflight_source
272
+ assert "ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND" in preflight_source
277
273
  find_root_source = inspect.getsource(preflight.find_repository_root)
278
274
  assert "'.git'" not in find_root_source
279
275
  assert '".git"' not in find_root_source
280
276
  discover_tests_source = inspect.getsource(preflight.has_discoverable_tests)
281
- assert "'.venv'" not in discover_tests_source
282
- assert '".venv"' not in discover_tests_source
283
-
284
-
285
- def test_preflight_does_not_import_unused_venv_directory_name_constant() -> None:
286
- """The ``VENV_DIRECTORY_NAME`` constant is not consumed by preflight.py
287
- (``.venv`` reaches the function body via ``ALL_TESTS_DIRECTORY_IGNORE_PARTS``).
288
- Importing the standalone name is dead code per the unused-imports rule."""
289
- preflight_source = inspect.getsource(preflight)
290
- assert "VENV_DIRECTORY_NAME" not in preflight_source, (
291
- "Dead import must be removed; preflight.py reaches `.venv` via "
292
- "ALL_TESTS_DIRECTORY_IGNORE_PARTS instead"
293
- )
277
+ assert "ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND" in discover_tests_source
294
278
 
295
279
 
296
280
  def test_preflight_stderr_uses_bugteam_preflight_prefix(
@@ -331,3 +315,356 @@ def test_pytest_no_tests_collected_helper_returns_named_constant() -> None:
331
315
  assert "PYTEST_NO_TESTS_COLLECTED_EXIT_CODE" in helper_source, (
332
316
  "Helper body must return the named constant, not the bare literal 5"
333
317
  )
318
+
319
+
320
+ def test_preflight_bootstrap_moves_script_directory_to_front() -> None:
321
+ """Import bootstrap keeps exactly one script directory entry at the front."""
322
+ module_path = Path(__file__).parent.parent / "preflight.py"
323
+ script_directory_resolved = str(module_path.parent.resolve())
324
+ script_directory_absolute = str(module_path.parent.absolute())
325
+ original_sys_path = list(sys.path)
326
+ try:
327
+ sys.path.insert(0, script_directory_resolved)
328
+ sys.path.insert(0, script_directory_resolved)
329
+ sys.path.insert(0, str(module_path.parents[4]))
330
+ _load_preflight_module()
331
+ assert os.path.samefile(sys.path[0], script_directory_resolved)
332
+ equivalent_count = sum(
333
+ 1
334
+ for each_entry in sys.path
335
+ if os.path.exists(each_entry)
336
+ and os.path.samefile(each_entry, script_directory_resolved)
337
+ )
338
+ assert equivalent_count == 1
339
+ assert sys.path[0] == script_directory_absolute
340
+ finally:
341
+ sys.path[:] = original_sys_path
342
+
343
+
344
+ def test_main_uses_correct_changed_files_function_name() -> None:
345
+ """main() must call get_changed_files, not the undefined get_all_changed_files."""
346
+ main_source = inspect.getsource(preflight.main)
347
+ assert "get_all_changed_files(" not in main_source
348
+
349
+
350
+ def test_should_not_return_nonexistent_test_file(tmp_path: Path) -> None:
351
+ """A deleted test file path from git diff --name-only must not be returned.
352
+ Before the fix, _find_related_test_files returned paths without checking
353
+ whether the file exists on disk, which caused pytest to receive
354
+ nonexistent paths for deleted files.
355
+ """
356
+ repo_root = tmp_path
357
+ deleted_test_path = Path("test_deleted_module.py")
358
+ result = preflight._find_related_test_files(deleted_test_path, repo_root)
359
+ assert result == []
360
+
361
+
362
+ def test_should_not_return_test_files_for_non_python_file(tmp_path: Path) -> None:
363
+ """A non-.py file must return an empty list regardless of file existence."""
364
+ repo_root = tmp_path
365
+ non_python_path = Path("readme.txt")
366
+ (repo_root / non_python_path).touch()
367
+ result = preflight._find_related_test_files(non_python_path, repo_root)
368
+ assert result == []
369
+
370
+
371
+ def test_should_find_test_file_in_adjacent_tests_directory(tmp_path: Path) -> None:
372
+ """A source file with a matching test in the adjacent tests/ directory
373
+ must return that test file path."""
374
+ repo_root = tmp_path
375
+ source_path = Path("src/module.py")
376
+ (repo_root / source_path).parent.mkdir(parents=True)
377
+ (repo_root / source_path).touch()
378
+ adjacent_tests = repo_root / "src" / "tests"
379
+ adjacent_tests.mkdir(parents=True)
380
+ expected_test = adjacent_tests / "test_module.py"
381
+ expected_test.touch()
382
+ result = preflight._find_related_test_files(source_path, repo_root)
383
+ assert expected_test in result
384
+
385
+
386
+ def test_should_find_test_file_in_top_level_tests_directory(tmp_path: Path) -> None:
387
+ """A source file with a matching test in the top-level tests/ directory
388
+ must return that test file path."""
389
+ repo_root = tmp_path
390
+ source_path = Path("src/module.py")
391
+ (repo_root / source_path).parent.mkdir(parents=True)
392
+ (repo_root / source_path).touch()
393
+ top_tests = repo_root / "tests" / "src"
394
+ top_tests.mkdir(parents=True)
395
+ expected_test = top_tests / "test_module.py"
396
+ expected_test.touch()
397
+ result = preflight._find_related_test_files(source_path, repo_root)
398
+ assert expected_test in result
399
+
400
+
401
+ def test_main_should_warn_when_scope_changed_without_base_ref(
402
+ capsys: pytest.CaptureFixture[str],
403
+ ) -> None:
404
+ """--scope changed with no --base-ref must warn and fall back to full suite."""
405
+ with (
406
+ patch.object(preflight, "verify_git_hooks_path", return_value=0),
407
+ patch.object(preflight, "has_pytest_configuration", return_value=True),
408
+ patch.object(preflight, "has_discoverable_tests", return_value=True),
409
+ patch.object(preflight, "run_pytest", return_value=0),
410
+ ):
411
+ exit_code = preflight.main(["--scope", "changed"])
412
+ assert exit_code == 0
413
+ captured = capsys.readouterr()
414
+ assert "requires --base-ref" in captured.err, (
415
+ "Missing warning when --scope changed is used without --base-ref"
416
+ )
417
+
418
+
419
+ def test_has_discoverable_tests_should_not_re_raise_on_git_failure(
420
+ capsys: pytest.CaptureFixture[str],
421
+ tmp_path: Path,
422
+ ) -> None:
423
+ """has_discoverable_tests must return None instead of re-raising on git failure."""
424
+ (tmp_path / ".git").mkdir()
425
+ with patch("subprocess.run", side_effect=subprocess.CalledProcessError(128, ["git"])):
426
+ result = preflight.has_discoverable_tests(tmp_path)
427
+ captured = capsys.readouterr()
428
+ assert result is None, (
429
+ "Should return None instead of propagating the exception"
430
+ )
431
+ assert "bugteam_preflight:" in captured.err
432
+ assert "git ls-files failed" in captured.err
433
+
434
+
435
+ def test_main_should_not_double_print_when_git_ls_fails(
436
+ capsys: pytest.CaptureFixture[str],
437
+ ) -> None:
438
+ """When git ls-files fails, main() must print a distinct failure warning and
439
+ run the full pytest suite instead of silently skipping tests."""
440
+ mock_hooks_result = _make_completed_process(
441
+ "/home/user/.claude/hooks/git-hooks\n", returncode=0
442
+ )
443
+ with (
444
+ patch("subprocess.run") as mock_run,
445
+ patch.object(preflight, "run_pytest", return_value=0) as mock_pytest,
446
+ ):
447
+ mock_run.side_effect = [
448
+ mock_hooks_result,
449
+ subprocess.CalledProcessError(128, ["git", "ls-files"]),
450
+ ]
451
+ exit_code = preflight.main([])
452
+ captured = capsys.readouterr()
453
+ assert "bugteam_preflight: test discovery failed" in captured.err, (
454
+ "Must print a distinct warning when discovery fails, not the 'no tests found' message"
455
+ )
456
+ assert "bugteam_preflight: pytest configured but no tests found" not in captured.err, (
457
+ "Must not print the 'no tests found' skip message when discovery fails"
458
+ )
459
+ mock_pytest.assert_called_once_with(ANY, False)
460
+
461
+
462
+ def test_should_default_to_changed_scope_when_base_ref_provided() -> None:
463
+ """--base-ref without --scope must default to 'changed', not 'all'.
464
+
465
+ The help text says 'Defaults to changed when --base-ref is provided'.
466
+ Before the fix, the None -> PYTEST_SCOPE_ALL conversion ran before
467
+ checking --base-ref, so providing --base-ref without --scope still
468
+ ran the full suite without calling get_changed_files.
469
+ """
470
+ with (
471
+ patch.object(preflight, "verify_git_hooks_path", return_value=0),
472
+ patch.object(preflight, "has_pytest_configuration", return_value=True),
473
+ patch.object(preflight, "has_discoverable_tests", return_value=True),
474
+ patch.object(preflight, "get_changed_files") as mock_get_changed,
475
+ patch.object(preflight, "discover_related_tests", return_value=[]),
476
+ patch.object(preflight, "run_pytest", return_value=0),
477
+ ):
478
+ exit_code = preflight.main(["--base-ref", "origin/main"])
479
+ assert exit_code == 0
480
+ mock_get_changed.assert_called_once_with(
481
+ ANY, "origin/main"
482
+ )
483
+
484
+
485
+ def test_should_default_to_all_scope_when_no_base_ref_no_scope(
486
+ capsys: pytest.CaptureFixture[str],
487
+ ) -> None:
488
+ """Omitting both --scope and --base-ref must default to 'all'."""
489
+ with (
490
+ patch.object(preflight, "verify_git_hooks_path", return_value=0),
491
+ patch.object(preflight, "has_pytest_configuration", return_value=True),
492
+ patch.object(preflight, "has_discoverable_tests", return_value=True),
493
+ patch.object(preflight, "run_pytest", return_value=0),
494
+ ):
495
+ exit_code = preflight.main([])
496
+ assert exit_code == 0
497
+ captured = capsys.readouterr()
498
+ assert "running full suite" not in captured.err, (
499
+ "Default scope=all should run directly without changed-scope messages"
500
+ )
501
+
502
+
503
+ def test_explicit_scope_all_with_base_ref_should_not_call_get_changed_files(
504
+ capsys: pytest.CaptureFixture[str],
505
+ ) -> None:
506
+ """Explicit --scope all with --base-ref must not auto-convert to 'changed'.
507
+
508
+ Before the fix, ``argparse`` defaulted ``--scope`` to ``PYTEST_SCOPE_ALL``
509
+ (``"all"``), making it impossible to distinguish "user typed --scope all"
510
+ versus "user omitted --scope". The code then auto-converted
511
+ ``effective_scope == "all"`` to ``"changed"`` whenever ``--base-ref`` was
512
+ present, silently overriding an explicit ``--scope all``.
513
+
514
+ After the fix, ``--scope`` defaults to ``None`` and is resolved to ``"all"``
515
+ only after argparse, so the user's explicit ``--scope all`` stays ``"all"``
516
+ and the full suite runs regardless of ``--base-ref``.
517
+ """
518
+ with (
519
+ patch.object(preflight, "verify_git_hooks_path", return_value=0),
520
+ patch.object(preflight, "has_pytest_configuration", return_value=True),
521
+ patch.object(preflight, "has_discoverable_tests", return_value=True),
522
+ patch.object(preflight, "get_changed_files") as mock_get_changed,
523
+ patch.object(preflight, "discover_related_tests", return_value=[]),
524
+ patch.object(preflight, "run_pytest", return_value=0),
525
+ ):
526
+ exit_code = preflight.main(["--scope", "all", "--base-ref", "origin/main"])
527
+ assert exit_code == 0
528
+ mock_get_changed.assert_not_called()
529
+
530
+
531
+ def test_preflight_bootstrap_matches_code_rules_sys_path_pattern() -> None:
532
+ """Bootstrap must clear duplicate script_directory entries, then guard insert."""
533
+ module_path = Path(__file__).parent.parent / "preflight.py"
534
+ source = module_path.read_text(encoding="utf-8")
535
+ assert "_entry_points_at_preflight_script_directory" in source, (
536
+ "Bootstrap must remove script_directory entries using path equivalence"
537
+ )
538
+ assert "for each_index in range(len(sys.path) - 1, -1, -1):" in source, (
539
+ "Bootstrap must walk sys.path to drop duplicate script directory entries"
540
+ )
541
+ assert "_preflight_scripts_path_entry not in sys.path:" in source, (
542
+ "Bootstrap insert must be guarded for code_rules_gate compliance"
543
+ )
544
+ assert "sys.path.insert(0, _preflight_scripts_path_entry)" in source, (
545
+ "Bootstrap must insert the absolute script directory at index 0"
546
+ )
547
+
548
+
549
+ def test_has_discoverable_tests_should_include_untracked_test_files(
550
+ tmp_path: Path,
551
+ ) -> None:
552
+ """has_discoverable_tests must include --others --exclude-standard
553
+ to discover untracked test files not yet in the git index."""
554
+ (tmp_path / ".git").mkdir()
555
+ with patch("subprocess.run") as mock_run:
556
+ mock_run.return_value = _make_completed_process("untracked_test.py\n", returncode=0)
557
+ preflight.has_discoverable_tests(tmp_path)
558
+ called_command = mock_run.call_args[0][0]
559
+ assert "--others" in called_command, (
560
+ "--others flag required to include untracked files in ls-files output"
561
+ )
562
+ assert "--exclude-standard" in called_command, (
563
+ "--exclude-standard flag required to respect .gitignore for untracked files"
564
+ )
565
+
566
+
567
+ def test_run_pytest_should_use_positional_separator_before_test_paths() -> None:
568
+ """run_pytest must pass '--' before test paths so pytest does not misinterpret
569
+ paths starting with '-' as command-line options."""
570
+ with patch("subprocess.run") as mock_run:
571
+ mock_run.return_value = _make_completed_process("", returncode=0)
572
+ preflight.run_pytest(
573
+ Path("/fake/repository"),
574
+ verbose=False,
575
+ all_test_paths=[Path("test_copilot_finding.py")],
576
+ )
577
+ called_command = mock_run.call_args[0][0]
578
+ separator_index = called_command.index("--")
579
+ assert called_command[separator_index + 1:] == ["test_copilot_finding.py"], (
580
+ "All test paths must follow the '--' positional separator"
581
+ )
582
+
583
+
584
+ # ---- Copilot finding 1: has_discoverable_tests in non-git directories ----
585
+
586
+
587
+ def test_has_discoverable_tests_returns_true_when_no_git_marker(
588
+ tmp_path: Path,
589
+ ) -> None:
590
+ """has_discoverable_tests must return True without running git when the root
591
+ has no .git marker (e.g., repo root found via pytest.ini)."""
592
+ (tmp_path / PYTEST_INI_FILENAME).touch()
593
+ result = preflight.has_discoverable_tests(tmp_path)
594
+ assert result is True
595
+
596
+
597
+ # ---- Copilot finding 2: base_ref command injection ----
598
+
599
+
600
+ def test_get_changed_files_returns_none_when_base_ref_starts_with_hyphen(
601
+ capsys: pytest.CaptureFixture[str],
602
+ ) -> None:
603
+ """get_changed_files must return None and print a warning when base_ref
604
+ starts with '-', preventing option injection into git diff."""
605
+ result = preflight.get_changed_files(Path("/fake"), "-oMalicious")
606
+ assert result is None
607
+ captured = capsys.readouterr()
608
+ assert "base_ref" in captured.err
609
+ assert "hyphen" in captured.err
610
+
611
+
612
+ # ---- Copilot finding 3: duplicate git failures when discovery_result is None ----
613
+
614
+
615
+ def test_main_skips_changed_scope_when_discovery_result_is_none(
616
+ capsys: pytest.CaptureFixture[str],
617
+ ) -> None:
618
+ """When has_discoverable_tests returns None (git unavailable), main must
619
+ not call get_changed_files even when --base-ref is provided."""
620
+ with (
621
+ patch.object(preflight, "verify_git_hooks_path", return_value=0),
622
+ patch.object(preflight, "has_pytest_configuration", return_value=True),
623
+ patch.object(preflight, "has_discoverable_tests", return_value=None),
624
+ patch.object(preflight, "get_changed_files") as mock_get_changed,
625
+ patch.object(preflight, "run_pytest", return_value=0),
626
+ ):
627
+ exit_code = preflight.main(["--base-ref", "origin/main"])
628
+ assert exit_code == 0
629
+ mock_get_changed.assert_not_called()
630
+
631
+
632
+ # ---- Copilot finding 4: misleading no-related-tests message on git diff failure ----
633
+
634
+
635
+ def test_main_does_not_print_no_related_tests_when_get_changed_files_returns_none(
636
+ capsys: pytest.CaptureFixture[str],
637
+ ) -> None:
638
+ """When get_changed_files returns None (git diff failed), main must not
639
+ print the misleading 'no related tests found' message."""
640
+ with (
641
+ patch.object(preflight, "verify_git_hooks_path", return_value=0),
642
+ patch.object(preflight, "has_pytest_configuration", return_value=True),
643
+ patch.object(preflight, "has_discoverable_tests", return_value=True),
644
+ patch.object(preflight, "get_changed_files", return_value=None),
645
+ patch.object(preflight, "discover_related_tests", return_value=[]),
646
+ patch.object(preflight, "run_pytest", return_value=0),
647
+ ):
648
+ exit_code = preflight.main(["--scope", "changed", "--base-ref", "origin/main"])
649
+ assert exit_code == 0
650
+ captured = capsys.readouterr()
651
+ assert "no related tests found" not in captured.err
652
+
653
+
654
+ def test_main_prints_no_related_tests_when_get_changed_files_returns_empty(
655
+ capsys: pytest.CaptureFixture[str],
656
+ ) -> None:
657
+ """When get_changed_files returns [] (no changed files, git succeeded),
658
+ main must print the 'no related tests found' message and run full suite."""
659
+ with (
660
+ patch.object(preflight, "verify_git_hooks_path", return_value=0),
661
+ patch.object(preflight, "has_pytest_configuration", return_value=True),
662
+ patch.object(preflight, "has_discoverable_tests", return_value=True),
663
+ patch.object(preflight, "get_changed_files", return_value=[]),
664
+ patch.object(preflight, "discover_related_tests", return_value=[]),
665
+ patch.object(preflight, "run_pytest", return_value=0),
666
+ ):
667
+ exit_code = preflight.main(["--scope", "changed", "--base-ref", "origin/main"])
668
+ assert exit_code == 0
669
+ captured = capsys.readouterr()
670
+ assert "no related tests found" in captured.err
@@ -38,10 +38,6 @@ def test_claude_directory_name() -> None:
38
38
  assert constants_module.CLAUDE_DIRECTORY_NAME == ".claude"
39
39
 
40
40
 
41
- def test_venv_directory_name() -> None:
42
- assert constants_module.VENV_DIRECTORY_NAME == ".venv"
43
-
44
-
45
41
  def test_pytest_ini_filename() -> None:
46
42
  assert constants_module.PYTEST_INI_FILENAME == "pytest.ini"
47
43
 
@@ -58,16 +54,15 @@ def test_pre_commit_config_yaml_filename() -> None:
58
54
  assert constants_module.PRE_COMMIT_CONFIG_YAML_FILENAME == ".pre-commit-config.yaml"
59
55
 
60
56
 
61
- def test_all_test_file_patterns_for_discovery() -> None:
62
- assert constants_module.ALL_TEST_FILE_PATTERNS_FOR_DISCOVERY == (
63
- "test_*.py",
64
- "*_test.py",
65
- )
66
-
67
-
68
- def test_all_tests_directory_ignore_parts_includes_venv_marker() -> None:
69
- assert constants_module.VENV_DIRECTORY_NAME in (
70
- constants_module.ALL_TESTS_DIRECTORY_IGNORE_PARTS
57
+ def test_all_git_ls_files_test_discovery_command() -> None:
58
+ assert constants_module.ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND == (
59
+ "ls-files",
60
+ "--cached",
61
+ "--others",
62
+ "--exclude-standard",
63
+ "--",
64
+ "**/test_*.py",
65
+ "**/*_test.py",
71
66
  )
72
67
 
73
68