claude-dev-env 2.31.1 → 2.33.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.
- package/.agents/agents/clean-coder.md +8 -7
- package/.agents/agents/code-quality-agent.md +4 -2
- package/.agents/agents/pr-description-writer.md +8 -3
- package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
- package/.agents/agents/test_agent_frontmatter.py +0 -9
- package/.agents/agents/test_pr_description_writer_style.py +24 -1
- package/.agents/skills/e-simplify/SKILL.md +1 -15
- package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
- package/.agents/skills/pr-refinement/SKILL.md +2 -2
- package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
- package/.agents/skills/pr-title-description/SKILL.md +6 -2
- package/.agents/skills/review-tier/SKILL.md +1 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
- package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
- package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
- package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
- package/AGENTS.md +83 -0
- package/_shared/pr-loop/code-rules-gate.md +5 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
- package/audit-rubrics/audit-categories.json +2 -2
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
- package/audit-rubrics/prompts/category-e-dead-code.md +6 -6
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
- package/bin/install.test.mjs +11 -3
- package/docs/CODE_RULES.md +8 -9
- package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
- package/hooks/blocking/code_rules_comments.py +118 -59
- package/hooks/blocking/code_rules_duplicate_body.py +13 -7
- package/hooks/blocking/code_rules_enforcer.py +19 -7
- package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
- package/hooks/blocking/code_rules_type_escape.py +16 -23
- package/hooks/blocking/javascript_comment_scanner.py +99 -0
- package/hooks/blocking/shell_substitution_blocker.py +175 -8
- package/hooks/blocking/stop_dispatcher.py +6 -9
- package/hooks/blocking/tdd_enforcer.py +3 -1
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
- package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
- package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +8 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +7 -2
- package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
- package/hooks/blocking/test_state_description_blocker.py +56 -71
- package/hooks/blocking/test_tdd_enforcer.py +67 -1
- package/hooks/git-hooks/pull_request_handoff.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +8 -0
- package/hooks/hooks.json +2 -2
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
- package/hooks/hooks_constants/state_file_lock.py +76 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
- package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
- package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
- package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
- package/hooks/observability/session_file_edit_tracker.py +2 -55
- package/hooks/observability/test_failure_recorder.py +219 -0
- package/hooks/observability/test_test_failure_recorder.py +173 -0
- package/hooks/validators/comment_checks.py +2 -2
- package/hooks/validators/config/directory_exemption_constants.py +0 -9
- package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
- package/hooks/validators/fast_save_validators.py +0 -9
- package/hooks/validators/git_checks.py +0 -84
- package/hooks/validators/mypy_integration.py +10 -36
- package/hooks/validators/project_roots.py +58 -0
- package/hooks/validators/run_all_validators.py +89 -42
- package/hooks/validators/system_temporary_roots.py +1 -1
- package/hooks/validators/test_fast_save_validators.py +2 -8
- package/hooks/validators/test_git_checks.py +0 -249
- package/hooks/validators/test_run_all_validators.py +22 -0
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
- package/hooks/validators/test_run_all_validators_reporting.py +66 -0
- package/hooks/validators/test_todo_checks.py +1 -0
- package/hooks/validators/todo_checks.py +1 -1
- package/package.json +1 -1
- package/rules/git-workflow.md +0 -8
- package/rules/shell-invocation.md +1 -1
- package/rules/testing.md +4 -0
- package/scripts/check.ps1 +35 -1
- package/scripts/codex_compat_materializer.py +1 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +9 -5
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
- package/scripts/shared_tree_paths.py +59 -25
- package/scripts/test_shared_tree_paths.py +30 -0
- package/scripts/tests/test_rules.py +111 -0
- package/system-prompts/software-engineer.xml +6 -19
- package/.agents/skills-archived/split-pr/SKILL.md +0 -85
- package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
- package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
- package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
- package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
- package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
- package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
- package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
- package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
- package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
- package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
- package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
- package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
- package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
- package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
- package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
- package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
- package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
- package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
- package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
- package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
- package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
- package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
- package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
- package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
- package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Resolve the project root that contains a path, if any.
|
|
2
|
+
|
|
3
|
+
A project root is the nearest ancestor holding a ``.git`` entry or a
|
|
4
|
+
``pyproject.toml``. Callers need that anchor for unrelated reasons: binding a
|
|
5
|
+
first-party import, deciding which directory segments a project owns. One
|
|
6
|
+
module answers it rather than each re-coding the walk.
|
|
7
|
+
|
|
8
|
+
::
|
|
9
|
+
|
|
10
|
+
target_repo/.git + target_repo/tools/x.py -> target_repo
|
|
11
|
+
/tmp/detached/x.py (home .git above temp) -> None
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_validators_directory = str(Path(__file__).resolve().parent)
|
|
20
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
21
|
+
for each_directory in (_validators_directory, _hooks_directory):
|
|
22
|
+
if each_directory not in sys.path:
|
|
23
|
+
sys.path.insert(0, each_directory)
|
|
24
|
+
|
|
25
|
+
from pyproject_config_discovery import ancestor_directories # noqa: E402
|
|
26
|
+
from system_temporary_roots import ( # noqa: E402
|
|
27
|
+
enclosing_system_temporary_root,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from hooks_constants.mypy_integration_constants import ( # noqa: E402
|
|
31
|
+
GIT_DIRECTORY_NAME,
|
|
32
|
+
PYPROJECT_FILENAME,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def enclosing_project_root(starting_file: Path) -> Path | None:
|
|
37
|
+
"""Return the nearest ancestor directory that roots a project, else None.
|
|
38
|
+
|
|
39
|
+
The walk stops at the system temp root. A staging copy lives under a temp
|
|
40
|
+
directory, and a ``.git`` in the user home above that root roots no
|
|
41
|
+
project of the staged file's.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
starting_file: The file (or directory) the walk begins from.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The nearest ancestor holding ``.git`` or ``pyproject.toml``, or None
|
|
48
|
+
when the walk reaches the temp root or runs out of ancestors.
|
|
49
|
+
"""
|
|
50
|
+
enclosing_temporary_root = enclosing_system_temporary_root(starting_file)
|
|
51
|
+
for each_candidate_directory in ancestor_directories(starting_file):
|
|
52
|
+
has_git_entry = (each_candidate_directory / GIT_DIRECTORY_NAME).exists()
|
|
53
|
+
has_pyproject = (each_candidate_directory / PYPROJECT_FILENAME).is_file()
|
|
54
|
+
if has_git_entry or has_pyproject:
|
|
55
|
+
return each_candidate_directory
|
|
56
|
+
if each_candidate_directory == enclosing_temporary_root:
|
|
57
|
+
return None
|
|
58
|
+
return None
|
|
@@ -16,6 +16,7 @@ import tempfile
|
|
|
16
16
|
import time
|
|
17
17
|
from collections import Counter
|
|
18
18
|
from dataclasses import dataclass
|
|
19
|
+
from functools import lru_cache
|
|
19
20
|
from datetime import datetime
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Callable, Dict, List, Optional, Tuple
|
|
@@ -35,16 +36,24 @@ from .config.directory_exemption_constants import (
|
|
|
35
36
|
)
|
|
36
37
|
from .fast_save_validators import run_fast_save_validators
|
|
37
38
|
from .health_check import get_system_health, get_validator_version, print_health_report
|
|
38
|
-
from .mypy_integration import
|
|
39
|
-
check_mypy_available,
|
|
40
|
-
find_module_resolution_root,
|
|
41
|
-
run_mypy_check,
|
|
42
|
-
)
|
|
39
|
+
from .mypy_integration import check_mypy_available, run_mypy_check
|
|
43
40
|
from .output_formatter import OutputFormatter, OutputMode, ValidatorResultDict
|
|
41
|
+
from .project_roots import enclosing_project_root
|
|
44
42
|
from .python_style_checks import fix_file
|
|
45
43
|
from .ruff_integration import run_ruff_check
|
|
44
|
+
from .config.validator_base_constants import SOURCE_CACHE_ENTRIES
|
|
46
45
|
from .validator_base import ValidatorResult
|
|
47
46
|
from blocking.code_rules_shared import is_ephemeral_path
|
|
47
|
+
from blocking.codex_apply_patch import (
|
|
48
|
+
CODEX_DELETE_OPERATION,
|
|
49
|
+
CodexPatchError,
|
|
50
|
+
CodexPatchFile,
|
|
51
|
+
parse_codex_apply_patch,
|
|
52
|
+
payload_patch_command,
|
|
53
|
+
)
|
|
54
|
+
from hooks_constants.pre_tool_use_dispatcher_constants import (
|
|
55
|
+
APPLY_PATCH_TOOL_NAME,
|
|
56
|
+
)
|
|
48
57
|
from hooks_constants.hook_block_logger import log_hook_block
|
|
49
58
|
from hooks_constants.multi_edit_reconstruction import (
|
|
50
59
|
apply_edits,
|
|
@@ -401,18 +410,12 @@ def run_git_checks() -> ValidatorResult:
|
|
|
401
410
|
|
|
402
411
|
|
|
403
412
|
def run_comment_checks(files: List[Path]) -> ValidatorResult:
|
|
404
|
-
"""
|
|
405
|
-
|
|
406
|
-
The hook compares old vs new content to block NEW comments and
|
|
407
|
-
print a stderr advisory when an existing comment is removed. This
|
|
408
|
-
standalone validator is disabled because it flags ALL comments in
|
|
409
|
-
existing files, which forces agents to remove them to pass validation.
|
|
410
|
-
"""
|
|
413
|
+
"""Report that comment policy runs through the explicit diff-aware linter."""
|
|
411
414
|
return ValidatorResult(
|
|
412
415
|
name="No Comments",
|
|
413
416
|
checks="26",
|
|
414
417
|
passed=True,
|
|
415
|
-
output="
|
|
418
|
+
output="Run code_rules_gate.py with --comment-policy",
|
|
416
419
|
)
|
|
417
420
|
|
|
418
421
|
|
|
@@ -793,8 +796,8 @@ def _path_parts_the_project_owns(destination_path: Path) -> tuple[str, ...]:
|
|
|
793
796
|
|
|
794
797
|
::
|
|
795
798
|
|
|
796
|
-
ok: "/
|
|
797
|
-
ok: "/
|
|
799
|
+
ok: "/srv/tests/proj/src/worker.py" (proj is the root) -> ("src", "worker.py")
|
|
800
|
+
ok: "/srv/anna/.claude/hooks/gate.py" (no root) -> (".claude", "hooks", "gate.py")
|
|
798
801
|
ok: "config/x.py" (relative) -> ("config", "x.py")
|
|
799
802
|
|
|
800
803
|
Directory names above the project root were chosen by whoever laid out the
|
|
@@ -819,13 +822,13 @@ def _path_parts_the_project_owns(destination_path: Path) -> tuple[str, ...]:
|
|
|
819
822
|
if not destination_path.anchor:
|
|
820
823
|
return all_path_parts
|
|
821
824
|
all_path_parts = all_path_parts[1:]
|
|
822
|
-
project_root =
|
|
825
|
+
project_root = enclosing_project_root(destination_path)
|
|
823
826
|
if project_root is None:
|
|
824
827
|
return all_path_parts
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
except ValueError:
|
|
828
|
+
resolved_destination = destination_path.resolve()
|
|
829
|
+
if not resolved_destination.is_relative_to(project_root):
|
|
828
830
|
return all_path_parts
|
|
831
|
+
return resolved_destination.relative_to(project_root).parts
|
|
829
832
|
|
|
830
833
|
|
|
831
834
|
def _temporary_path_preserving_directory_signal(
|
|
@@ -1158,6 +1161,7 @@ def _line_identity(
|
|
|
1158
1161
|
)
|
|
1159
1162
|
|
|
1160
1163
|
|
|
1164
|
+
@lru_cache(maxsize=SOURCE_CACHE_ENTRIES)
|
|
1161
1165
|
def _scope_agnostic_message(identity_scope: str, violation_message: str) -> str:
|
|
1162
1166
|
"""Return the message with its own function's name and shape metrics masked.
|
|
1163
1167
|
|
|
@@ -1199,22 +1203,6 @@ def _scope_agnostic_message(identity_scope: str, violation_message: str) -> str:
|
|
|
1199
1203
|
)
|
|
1200
1204
|
|
|
1201
1205
|
|
|
1202
|
-
def _shape_key(violation_identity: ViolationIdentity) -> tuple[str, str]:
|
|
1203
|
-
"""Return an identity's ``(validator, scope-agnostic message)`` shape key.
|
|
1204
|
-
|
|
1205
|
-
Args:
|
|
1206
|
-
violation_identity: One violation's full identity key.
|
|
1207
|
-
|
|
1208
|
-
Returns:
|
|
1209
|
-
The key a violation keeps when only its enclosing function's name
|
|
1210
|
-
changes, so a rename, a split, or a move still finds its baseline.
|
|
1211
|
-
"""
|
|
1212
|
-
return (
|
|
1213
|
-
violation_identity[0],
|
|
1214
|
-
_scope_agnostic_message(violation_identity[1], violation_identity[2]),
|
|
1215
|
-
)
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
1206
|
def _violation_identities(
|
|
1219
1207
|
failed_results: List[ValidatorResult], content: str
|
|
1220
1208
|
) -> Counter[ViolationIdentity]:
|
|
@@ -1283,9 +1271,12 @@ def _compatible_baseline_identity(
|
|
|
1283
1271
|
for each_identity in all_available_identities:
|
|
1284
1272
|
if each_identity[1] == line_identity[1]:
|
|
1285
1273
|
return each_identity
|
|
1286
|
-
|
|
1274
|
+
line_shape_message = _scope_agnostic_message(line_identity[1], line_identity[2])
|
|
1287
1275
|
for each_identity in all_available_identities:
|
|
1288
|
-
if
|
|
1276
|
+
if (
|
|
1277
|
+
_scope_agnostic_message(each_identity[1], each_identity[2])
|
|
1278
|
+
== line_shape_message
|
|
1279
|
+
):
|
|
1289
1280
|
return each_identity
|
|
1290
1281
|
return None
|
|
1291
1282
|
|
|
@@ -1448,26 +1439,80 @@ def _emit_pre_existing_warning(all_preexisting_results: List[ValidatorResult]) -
|
|
|
1448
1439
|
sys.stderr.flush()
|
|
1449
1440
|
|
|
1450
1441
|
|
|
1451
|
-
def _decide_pre_tool_use(file_path: str, proposed_content: str) ->
|
|
1442
|
+
def _decide_pre_tool_use(file_path: str, proposed_content: str) -> bool:
|
|
1452
1443
|
"""Deny only violations absent from the baseline; warn on the ones that persist.
|
|
1453
1444
|
|
|
1454
1445
|
Args:
|
|
1455
1446
|
file_path: The write's target path.
|
|
1456
1447
|
proposed_content: The reconstructed post-edit content of that file.
|
|
1448
|
+
|
|
1449
|
+
Returns:
|
|
1450
|
+
True when this target denies, so a caller holding several targets stops
|
|
1451
|
+
at the first one and emits a single decision.
|
|
1457
1452
|
"""
|
|
1458
1453
|
all_proposed_failed = _failed_results(
|
|
1459
1454
|
validate_proposed_file(file_path, proposed_content)
|
|
1460
1455
|
)
|
|
1461
1456
|
if not all_proposed_failed:
|
|
1462
|
-
return
|
|
1457
|
+
return False
|
|
1463
1458
|
baseline_identities = _baseline_violation_identities(file_path)
|
|
1464
1459
|
all_new_results, all_preexisting_results = _scope_new_and_preexisting(
|
|
1465
1460
|
all_proposed_failed, proposed_content, baseline_identities
|
|
1466
1461
|
)
|
|
1467
1462
|
if all_preexisting_results:
|
|
1468
1463
|
_emit_pre_existing_warning(all_preexisting_results)
|
|
1469
|
-
if all_new_results:
|
|
1470
|
-
|
|
1464
|
+
if not all_new_results:
|
|
1465
|
+
return False
|
|
1466
|
+
_emit_pre_tool_use_deny(_proposed_content_deny_reason(all_new_results))
|
|
1467
|
+
return True
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def _decide_codex_patch_file(
|
|
1471
|
+
each_patch_file: CodexPatchFile, pre_tool_use_payload: dict
|
|
1472
|
+
) -> bool:
|
|
1473
|
+
"""Grade one path a Codex patch writes, skipping a deletion and an exemption.
|
|
1474
|
+
|
|
1475
|
+
Args:
|
|
1476
|
+
each_patch_file: One parsed path with its projected post-edit content.
|
|
1477
|
+
pre_tool_use_payload: The whole payload, read for its working directory.
|
|
1478
|
+
|
|
1479
|
+
Returns:
|
|
1480
|
+
True when this path denies the patch.
|
|
1481
|
+
"""
|
|
1482
|
+
if each_patch_file.operation == CODEX_DELETE_OPERATION:
|
|
1483
|
+
return False
|
|
1484
|
+
if is_ephemeral_path(each_patch_file.file_path, pre_tool_use_payload):
|
|
1485
|
+
return False
|
|
1486
|
+
return _decide_pre_tool_use(each_patch_file.file_path, each_patch_file.post_content)
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def _evaluate_codex_apply_patch(pre_tool_use_payload: dict, tool_input: dict) -> None:
|
|
1490
|
+
"""Grade every path a Codex apply_patch writes, stopping at the first denial.
|
|
1491
|
+
|
|
1492
|
+
::
|
|
1493
|
+
|
|
1494
|
+
one patch adding two files, the second violating -> deny, names file two
|
|
1495
|
+
one patch deleting a file -> allow, nothing to grade
|
|
1496
|
+
|
|
1497
|
+
A patch carries several paths in one call, so each is graded on its own
|
|
1498
|
+
projected post-edit content and the first denial is the decision.
|
|
1499
|
+
|
|
1500
|
+
Args:
|
|
1501
|
+
pre_tool_use_payload: The whole PreToolUse payload.
|
|
1502
|
+
tool_input: The payload's tool input mapping.
|
|
1503
|
+
"""
|
|
1504
|
+
patch_command, working_directory = payload_patch_command(
|
|
1505
|
+
pre_tool_use_payload, tool_input
|
|
1506
|
+
)
|
|
1507
|
+
if not patch_command:
|
|
1508
|
+
return
|
|
1509
|
+
try:
|
|
1510
|
+
all_patch_files = parse_codex_apply_patch(patch_command, working_directory)
|
|
1511
|
+
except CodexPatchError:
|
|
1512
|
+
return
|
|
1513
|
+
for each_patch_file in all_patch_files:
|
|
1514
|
+
if _decide_codex_patch_file(each_patch_file, pre_tool_use_payload):
|
|
1515
|
+
return
|
|
1471
1516
|
|
|
1472
1517
|
|
|
1473
1518
|
def _evaluate_pre_tool_use_payload() -> None:
|
|
@@ -1491,6 +1536,9 @@ def _evaluate_pre_tool_use_payload() -> None:
|
|
|
1491
1536
|
tool_input = pre_tool_use_payload.get("tool_input", {})
|
|
1492
1537
|
if not isinstance(tool_name, str) or not isinstance(tool_input, dict):
|
|
1493
1538
|
return
|
|
1539
|
+
if tool_name == APPLY_PATCH_TOOL_NAME:
|
|
1540
|
+
_evaluate_codex_apply_patch(pre_tool_use_payload, tool_input)
|
|
1541
|
+
return
|
|
1494
1542
|
file_path = tool_input.get("file_path", "")
|
|
1495
1543
|
if not isinstance(file_path, str) or not file_path:
|
|
1496
1544
|
return
|
|
@@ -1621,7 +1669,6 @@ def main() -> int:
|
|
|
1621
1669
|
("Security", lambda: run_security_checks(files)),
|
|
1622
1670
|
("Code Quality", lambda: run_code_quality_checks(files)),
|
|
1623
1671
|
("Python Anti-patterns", lambda: run_python_antipattern_checks(files)),
|
|
1624
|
-
("TODO Tracking", lambda: run_todo_checks(files)),
|
|
1625
1672
|
("Type Safety", lambda: run_type_safety_checks(files)),
|
|
1626
1673
|
]
|
|
1627
1674
|
|
|
@@ -22,7 +22,7 @@ _hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
|
22
22
|
if _hooks_directory not in sys.path:
|
|
23
23
|
sys.path.insert(0, _hooks_directory)
|
|
24
24
|
|
|
25
|
-
from validators.config.
|
|
25
|
+
from validators.config.system_temporary_roots_constants import ( # noqa: E402
|
|
26
26
|
ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES,
|
|
27
27
|
)
|
|
28
28
|
|
|
@@ -9,7 +9,6 @@ from .fast_save_validators import run_fast_save_validators
|
|
|
9
9
|
from .magic_value_checks import validate_file as read_magic_literal_violations
|
|
10
10
|
from .react_checks import check_no_class_components
|
|
11
11
|
from .test_safety_checks import check_no_skip_decorators
|
|
12
|
-
from .todo_checks import validate_file as read_todo_violations
|
|
13
12
|
|
|
14
13
|
MAGIC_LITERAL_SOURCE = "def compute_total(unit_price: int) -> int:\n return unit_price * 199\n"
|
|
15
14
|
_UNTRACKED_TODO_MARKER = "#" + " TODO"
|
|
@@ -24,7 +23,6 @@ _ALL_EXPECTED_FAST_SAVE_VALIDATOR_NAMES = frozenset(
|
|
|
24
23
|
"Python Style",
|
|
25
24
|
"Abbreviations",
|
|
26
25
|
"Magic Values",
|
|
27
|
-
"TODO Tracking",
|
|
28
26
|
"Security",
|
|
29
27
|
"Code Quality",
|
|
30
28
|
"Python Anti-patterns",
|
|
@@ -103,16 +101,12 @@ def test_magic_values_outcome_matches_direct_validate_file_call(
|
|
|
103
101
|
assert magic_values_outcome.output == "\n".join(expected_lines)
|
|
104
102
|
|
|
105
103
|
|
|
106
|
-
def
|
|
104
|
+
def test_todo_comments_do_not_run_on_save_path(tmp_path: Path) -> None:
|
|
107
105
|
target_file = tmp_path / "legacy_module.py"
|
|
108
106
|
target_file.write_text(TODO_SOURCE, encoding="utf-8")
|
|
109
|
-
expected_lines = [str(each) for each in read_todo_violations(target_file)]
|
|
110
|
-
assert expected_lines != []
|
|
111
|
-
|
|
112
107
|
all_outcomes = run_fast_save_validators([target_file])
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
assert todo_outcome.output == "\n".join(expected_lines)
|
|
109
|
+
assert all(each.name != "TODO Tracking" for each in all_outcomes)
|
|
116
110
|
|
|
117
111
|
|
|
118
112
|
def test_abbreviations_outcome_reports_clean_for_a_clean_file(tmp_path: Path) -> None:
|
|
@@ -1,215 +1,17 @@
|
|
|
1
1
|
"""Tests for git and GitHub validation checks."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import subprocess
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import List
|
|
7
4
|
from unittest.mock import MagicMock, patch
|
|
8
5
|
|
|
9
6
|
import pytest
|
|
10
7
|
|
|
11
8
|
from .git_checks import (
|
|
12
9
|
Violation,
|
|
13
|
-
check_single_commit_when_pr_exists,
|
|
14
10
|
check_draft_pr_state,
|
|
15
11
|
main,
|
|
16
12
|
)
|
|
17
13
|
|
|
18
14
|
|
|
19
|
-
class TestSingleCommitWhenPrExists:
|
|
20
|
-
"""Test that PR branches have exactly 1 commit ahead of base."""
|
|
21
|
-
|
|
22
|
-
@patch("validators.git_checks.subprocess.run")
|
|
23
|
-
def test_no_pr_returns_empty(self, mock_run: MagicMock) -> None:
|
|
24
|
-
"""When no PR exists, check should return empty list."""
|
|
25
|
-
mock_run.side_effect = [
|
|
26
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
27
|
-
MagicMock(returncode=0, stdout="[]", stderr=""),
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
violations = check_single_commit_when_pr_exists()
|
|
31
|
-
|
|
32
|
-
assert violations == []
|
|
33
|
-
|
|
34
|
-
@patch("validators.git_checks.subprocess.run")
|
|
35
|
-
def test_single_commit_ahead_passes(self, mock_run: MagicMock) -> None:
|
|
36
|
-
"""Exactly 1 commit ahead should pass."""
|
|
37
|
-
mock_run.side_effect = [
|
|
38
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
39
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
|
|
40
|
-
MagicMock(returncode=0, stdout="1", stderr=""),
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
violations = check_single_commit_when_pr_exists()
|
|
44
|
-
|
|
45
|
-
assert violations == []
|
|
46
|
-
|
|
47
|
-
@patch("validators.git_checks.subprocess.run")
|
|
48
|
-
def test_zero_commits_ahead_fails(self, mock_run: MagicMock) -> None:
|
|
49
|
-
"""Zero commits ahead should fail."""
|
|
50
|
-
mock_run.side_effect = [
|
|
51
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
52
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
|
|
53
|
-
MagicMock(returncode=0, stdout="0", stderr=""),
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
violations = check_single_commit_when_pr_exists()
|
|
57
|
-
|
|
58
|
-
assert len(violations) == 1
|
|
59
|
-
assert violations[0].file == ""
|
|
60
|
-
assert violations[0].line == 0
|
|
61
|
-
assert "exactly 1 commit" in violations[0].message
|
|
62
|
-
assert "0 commits" in violations[0].message
|
|
63
|
-
|
|
64
|
-
@patch("validators.git_checks.subprocess.run")
|
|
65
|
-
def test_multiple_commits_ahead_fails(self, mock_run: MagicMock) -> None:
|
|
66
|
-
"""More than 1 commit ahead should fail."""
|
|
67
|
-
mock_run.side_effect = [
|
|
68
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
69
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
|
|
70
|
-
MagicMock(returncode=0, stdout="3", stderr=""),
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
violations = check_single_commit_when_pr_exists()
|
|
74
|
-
|
|
75
|
-
assert len(violations) == 1
|
|
76
|
-
assert "exactly 1 commit" in violations[0].message
|
|
77
|
-
assert "3 commits" in violations[0].message
|
|
78
|
-
|
|
79
|
-
@patch("validators.git_checks.subprocess.run")
|
|
80
|
-
def test_gh_cli_not_available_returns_empty(self, mock_run: MagicMock) -> None:
|
|
81
|
-
"""When gh CLI not available, should return empty (warning, not failure)."""
|
|
82
|
-
mock_run.side_effect = [
|
|
83
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
84
|
-
FileNotFoundError("gh not found"),
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
violations = check_single_commit_when_pr_exists()
|
|
88
|
-
|
|
89
|
-
assert violations == []
|
|
90
|
-
|
|
91
|
-
@patch("validators.git_checks.subprocess.run")
|
|
92
|
-
def test_git_not_available_returns_empty(self, mock_run: MagicMock) -> None:
|
|
93
|
-
"""When git not available, should return empty."""
|
|
94
|
-
mock_run.side_effect = [
|
|
95
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
96
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
|
|
97
|
-
FileNotFoundError("git not found"),
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
violations = check_single_commit_when_pr_exists()
|
|
101
|
-
|
|
102
|
-
assert violations == []
|
|
103
|
-
|
|
104
|
-
@patch("validators.git_checks.subprocess.run")
|
|
105
|
-
def test_extracts_base_branch_from_pr_info(self, mock_run: MagicMock) -> None:
|
|
106
|
-
"""Should extract base branch name from gh pr list JSON output, falling back to main when absent."""
|
|
107
|
-
mock_run.side_effect = [
|
|
108
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
109
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "develop", "number": 123}]', stderr=""),
|
|
110
|
-
MagicMock(returncode=0, stdout="2", stderr=""),
|
|
111
|
-
]
|
|
112
|
-
|
|
113
|
-
violations = check_single_commit_when_pr_exists()
|
|
114
|
-
|
|
115
|
-
assert len(violations) == 1
|
|
116
|
-
mock_run.assert_any_call(
|
|
117
|
-
["git", "rev-list", "--count", "develop..HEAD"],
|
|
118
|
-
capture_output=True,
|
|
119
|
-
text=True,
|
|
120
|
-
check=True,
|
|
121
|
-
timeout=30,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
mock_run.reset_mock()
|
|
125
|
-
mock_run.side_effect = [
|
|
126
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
127
|
-
MagicMock(returncode=0, stdout='[{"number": 123}]', stderr=""),
|
|
128
|
-
MagicMock(returncode=0, stdout="2", stderr=""),
|
|
129
|
-
]
|
|
130
|
-
|
|
131
|
-
fallback_violations = check_single_commit_when_pr_exists()
|
|
132
|
-
|
|
133
|
-
assert len(fallback_violations) == 1
|
|
134
|
-
assert "main" in fallback_violations[0].message
|
|
135
|
-
mock_run.assert_any_call(
|
|
136
|
-
["git", "rev-list", "--count", "main..HEAD"],
|
|
137
|
-
capture_output=True,
|
|
138
|
-
text=True,
|
|
139
|
-
check=True,
|
|
140
|
-
timeout=30,
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
@patch("validators.git_checks.subprocess.run")
|
|
144
|
-
def test_non_numeric_commit_count_returns_empty(self, mock_run: MagicMock) -> None:
|
|
145
|
-
"""When git rev-list returns non-numeric output, should return empty."""
|
|
146
|
-
mock_run.side_effect = [
|
|
147
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
148
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
|
|
149
|
-
MagicMock(returncode=0, stdout="not a number\n", stderr=""),
|
|
150
|
-
]
|
|
151
|
-
|
|
152
|
-
violations = check_single_commit_when_pr_exists()
|
|
153
|
-
|
|
154
|
-
assert violations == []
|
|
155
|
-
|
|
156
|
-
@patch("validators.git_checks.subprocess.run")
|
|
157
|
-
def test_gh_timeout_returns_empty(self, mock_run: MagicMock) -> None:
|
|
158
|
-
"""When gh CLI times out, should return empty (warning, not failure)."""
|
|
159
|
-
mock_run.side_effect = [
|
|
160
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
161
|
-
subprocess.TimeoutExpired(cmd=["gh", "pr", "list"], timeout=30),
|
|
162
|
-
]
|
|
163
|
-
|
|
164
|
-
violations = check_single_commit_when_pr_exists()
|
|
165
|
-
|
|
166
|
-
assert violations == []
|
|
167
|
-
|
|
168
|
-
@patch("validators.git_checks.subprocess.run")
|
|
169
|
-
def test_git_timeout_returns_empty(self, mock_run: MagicMock) -> None:
|
|
170
|
-
"""When git times out, should return empty (warning, not failure)."""
|
|
171
|
-
mock_run.side_effect = [
|
|
172
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
173
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
|
|
174
|
-
subprocess.TimeoutExpired(cmd=["git", "rev-list"], timeout=30),
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
violations = check_single_commit_when_pr_exists()
|
|
178
|
-
|
|
179
|
-
assert violations == []
|
|
180
|
-
|
|
181
|
-
@patch("validators.git_checks.subprocess.run")
|
|
182
|
-
def test_passes_resolved_branch_name_to_gh(self, mock_run: MagicMock) -> None:
|
|
183
|
-
"""gh pr list must receive the resolved branch name, never the literal 'HEAD'."""
|
|
184
|
-
mock_run.side_effect = [
|
|
185
|
-
MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
|
|
186
|
-
MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
|
|
187
|
-
MagicMock(returncode=0, stdout="1", stderr=""),
|
|
188
|
-
]
|
|
189
|
-
|
|
190
|
-
check_single_commit_when_pr_exists()
|
|
191
|
-
|
|
192
|
-
mock_run.assert_any_call(
|
|
193
|
-
["gh", "pr", "list", "--head", "feature/my-branch", "--json", "baseRefName,number"],
|
|
194
|
-
capture_output=True,
|
|
195
|
-
text=True,
|
|
196
|
-
check=True,
|
|
197
|
-
timeout=30,
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
@patch("validators.git_checks.subprocess.run")
|
|
201
|
-
def test_unresolved_branch_returns_empty(self, mock_run: MagicMock) -> None:
|
|
202
|
-
"""When current branch cannot be resolved, should return empty."""
|
|
203
|
-
mock_run.side_effect = [
|
|
204
|
-
MagicMock(returncode=0, stdout="\n", stderr=""),
|
|
205
|
-
]
|
|
206
|
-
|
|
207
|
-
violations = check_single_commit_when_pr_exists()
|
|
208
|
-
|
|
209
|
-
assert violations == []
|
|
210
|
-
mock_run.assert_called_once()
|
|
211
|
-
|
|
212
|
-
|
|
213
15
|
class TestDraftPrState:
|
|
214
16
|
"""Test that PR is in draft state when pushing review fixes."""
|
|
215
17
|
|
|
@@ -274,16 +76,13 @@ class TestDraftPrState:
|
|
|
274
76
|
class TestMain:
|
|
275
77
|
"""Test main function integration."""
|
|
276
78
|
|
|
277
|
-
@patch("validators.git_checks.check_single_commit_when_pr_exists")
|
|
278
79
|
@patch("validators.git_checks.check_draft_pr_state")
|
|
279
80
|
def test_main_no_violations_exits_zero(
|
|
280
81
|
self,
|
|
281
82
|
mock_draft: MagicMock,
|
|
282
|
-
mock_commit: MagicMock,
|
|
283
83
|
capsys,
|
|
284
84
|
) -> None:
|
|
285
85
|
"""main() should exit 0 when no violations found."""
|
|
286
|
-
mock_commit.return_value = []
|
|
287
86
|
mock_draft.return_value = []
|
|
288
87
|
|
|
289
88
|
with pytest.raises(SystemExit) as exc_info:
|
|
@@ -293,37 +92,13 @@ class TestMain:
|
|
|
293
92
|
captured = capsys.readouterr()
|
|
294
93
|
assert captured.out == ""
|
|
295
94
|
|
|
296
|
-
@patch("validators.git_checks.check_single_commit_when_pr_exists")
|
|
297
|
-
@patch("validators.git_checks.check_draft_pr_state")
|
|
298
|
-
def test_main_with_violations_exits_one(
|
|
299
|
-
self,
|
|
300
|
-
mock_draft: MagicMock,
|
|
301
|
-
mock_commit: MagicMock,
|
|
302
|
-
capsys,
|
|
303
|
-
) -> None:
|
|
304
|
-
"""main() should exit 1 and print violations when found."""
|
|
305
|
-
mock_commit.return_value = [
|
|
306
|
-
Violation(file="", line=0, message="Branch has 3 commits ahead")
|
|
307
|
-
]
|
|
308
|
-
mock_draft.return_value = []
|
|
309
|
-
|
|
310
|
-
with pytest.raises(SystemExit) as exc_info:
|
|
311
|
-
main()
|
|
312
|
-
|
|
313
|
-
assert exc_info.value.code == 1
|
|
314
|
-
captured = capsys.readouterr()
|
|
315
|
-
assert "Branch has 3 commits ahead" in captured.out
|
|
316
|
-
|
|
317
|
-
@patch("validators.git_checks.check_single_commit_when_pr_exists")
|
|
318
95
|
@patch("validators.git_checks.check_draft_pr_state")
|
|
319
96
|
def test_main_prints_violations_without_file_line(
|
|
320
97
|
self,
|
|
321
98
|
mock_draft: MagicMock,
|
|
322
|
-
mock_commit: MagicMock,
|
|
323
99
|
capsys,
|
|
324
100
|
) -> None:
|
|
325
101
|
"""main() should print git violations without file:line: prefix."""
|
|
326
|
-
mock_commit.return_value = []
|
|
327
102
|
mock_draft.return_value = [
|
|
328
103
|
Violation(file="", line=0, message="PR must be in draft state")
|
|
329
104
|
]
|
|
@@ -335,27 +110,3 @@ class TestMain:
|
|
|
335
110
|
captured = capsys.readouterr()
|
|
336
111
|
assert captured.out == "PR must be in draft state\n"
|
|
337
112
|
assert ":0:" not in captured.out
|
|
338
|
-
|
|
339
|
-
@patch("validators.git_checks.check_single_commit_when_pr_exists")
|
|
340
|
-
@patch("validators.git_checks.check_draft_pr_state")
|
|
341
|
-
def test_main_prints_all_violations(
|
|
342
|
-
self,
|
|
343
|
-
mock_draft: MagicMock,
|
|
344
|
-
mock_commit: MagicMock,
|
|
345
|
-
capsys,
|
|
346
|
-
) -> None:
|
|
347
|
-
"""main() should print all violations."""
|
|
348
|
-
mock_commit.return_value = [
|
|
349
|
-
Violation(file="", line=0, message="Branch has 2 commits")
|
|
350
|
-
]
|
|
351
|
-
mock_draft.return_value = [
|
|
352
|
-
Violation(file="", line=0, message="PR not in draft")
|
|
353
|
-
]
|
|
354
|
-
|
|
355
|
-
with pytest.raises(SystemExit) as exc_info:
|
|
356
|
-
main()
|
|
357
|
-
|
|
358
|
-
assert exc_info.value.code == 1
|
|
359
|
-
captured = capsys.readouterr()
|
|
360
|
-
assert "Branch has 2 commits" in captured.out
|
|
361
|
-
assert "PR not in draft" in captured.out
|
|
@@ -398,3 +398,25 @@ class TestHooksSubprocessWorkingDirectory:
|
|
|
398
398
|
)
|
|
399
399
|
assert working_directory_string == tempfile.gettempdir()
|
|
400
400
|
assert not working_directory_string.startswith("\\\\")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestDocstringExamplePaths:
|
|
404
|
+
def test_module_source_carries_no_home_directory_example_paths(self) -> None:
|
|
405
|
+
"""Example paths avoid /home so the privacy gate reads no personal data.
|
|
406
|
+
|
|
407
|
+
The gate allowlists a fixed set of placeholder home usernames, so an
|
|
408
|
+
invented name outside that set reads as somebody's real home directory
|
|
409
|
+
and denies the commit. Rooting an example somewhere other than /home
|
|
410
|
+
keeps the illustration and leaves nothing for the gate to flag.
|
|
411
|
+
"""
|
|
412
|
+
module_source = (Path(__file__).parent / "run_all_validators.py").read_text(
|
|
413
|
+
encoding="utf-8"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
all_home_example_lines = [
|
|
417
|
+
each_line
|
|
418
|
+
for each_line in module_source.splitlines()
|
|
419
|
+
if '"/home/' in each_line
|
|
420
|
+
]
|
|
421
|
+
|
|
422
|
+
assert all_home_example_lines == []
|