claude-dev-env 1.36.2 → 1.37.1
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.
- package/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
- package/_shared/pr-loop/scripts/preflight.py +242 -20
- package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
- package/hooks/blocking/code_rules_enforcer.py +269 -23
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
- package/hooks/config/test_unused_module_import_constants.py +48 -0
- package/hooks/config/unused_module_import_constants.py +41 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +4 -50
- package/rules/no-historical-clutter.md +36 -0
- package/skills/bg-agent/SKILL.md +69 -0
- package/skills/bugteam/CONSTRAINTS.md +10 -19
- package/skills/bugteam/PROMPTS.md +21 -14
- package/skills/bugteam/SKILL.md +122 -208
- package/skills/bugteam/SKILL_EVALS.md +75 -114
- package/skills/bugteam/reference/README.md +2 -4
- package/skills/bugteam/reference/audit-and-teammates.md +21 -48
- package/skills/bugteam/reference/audit-contract.md +7 -7
- package/skills/bugteam/reference/design-rationale.md +3 -8
- package/skills/bugteam/reference/team-setup.md +11 -19
- package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
- package/skills/bugteam/scripts/config/__init__.py +0 -0
- package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
- package/skills/bugteam/sources.md +1 -25
- package/skills/bugteam/test_skill_additions.py +4 -13
- package/skills/fresh-branch/SKILL.md +71 -0
- package/skills/gotcha/SKILL.md +73 -0
- package/skills/monitor-open-prs/SKILL.md +4 -37
- package/skills/monitor-open-prs/test_skill_contract.py +0 -5
- package/skills/pr-converge/SKILL.md +60 -1298
- package/skills/pr-converge/reference/convergence-gates.md +122 -0
- package/skills/pr-converge/reference/examples.md +76 -0
- package/skills/pr-converge/reference/fix-protocol.md +56 -0
- package/skills/pr-converge/reference/ground-rules.md +13 -0
- package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
- package/skills/pr-converge/reference/per-tick.md +204 -0
- package/skills/pr-converge/reference/state-schema.md +19 -0
- package/skills/pr-converge/reference/stop-conditions.md +26 -0
- package/skills/pr-converge/scripts/README.md +36 -9
- package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +74 -5
- package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
- package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
- package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
- package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
- package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
- package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
- package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
- package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
- package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
- package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
- package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +44 -0
- package/skills/pr-converge/scripts/view_pr_context.py +35 -4
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
- package/skills/bugteam/test_team_lifecycle.py +0 -103
- package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
- package/skills/pr-converge/test_team_lifecycle.py +0 -56
- 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
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
|
62
|
-
assert constants_module.
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|