claude-dev-env 7.5.0 → 7.6.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 (121) hide show
  1. package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
  2. package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
  3. package/.agents/skills/fresh-branch/SKILL.md +1 -0
  4. package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
  5. package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
  6. package/.agents/skills/syncing-submodules/SKILL.md +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
  8. package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
  9. package/.agents/skills/test-runner/SKILL.md +1 -0
  10. package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
  11. package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
  12. package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
  13. package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
  14. package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
  15. package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
  16. package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
  23. package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
  24. package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
  25. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
  26. package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
  27. package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
  28. package/_shared/pr-loop/scripts/preflight.py +7 -0
  29. package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
  30. package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
  31. package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
  33. package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
  34. package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
  35. package/bin/install-constants.mjs +4 -2
  36. package/bin/install.cursor-rules.test.mjs +57 -15
  37. package/bin/install.mjs +10 -15
  38. package/bin/pstack-model-preferences.claude.json +23 -0
  39. package/hooks/advisory/pr_done_reminder.py +7 -0
  40. package/hooks/advisory/refactor_guard.py +8 -0
  41. package/hooks/advisory/test_pr_done_reminder.py +21 -0
  42. package/hooks/advisory/test_refactor_guard.py +40 -0
  43. package/hooks/blocking/block_main_commit.py +4 -0
  44. package/hooks/blocking/code_rules_comments.py +39 -5
  45. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  46. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  47. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  48. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  49. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  50. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  51. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  52. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  53. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  54. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  55. package/hooks/git-hooks/test_verification_start.py +4 -0
  56. package/hooks/git-hooks/verification_notice_context.py +3 -0
  57. package/hooks/git-hooks/verification_start.py +7 -8
  58. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  59. package/hooks/hooks_constants/subprocess_window.py +141 -0
  60. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  61. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  62. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  63. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  64. package/hooks/validation/mypy_validator.py +4 -0
  65. package/hooks/validators/README.md +1 -0
  66. package/hooks/validators/git_checks.py +4 -0
  67. package/hooks/validators/health_check.py +3 -0
  68. package/hooks/validators/hook_timing_harness.py +3 -0
  69. package/hooks/validators/mypy_integration.py +4 -0
  70. package/hooks/validators/ruff_integration.py +6 -0
  71. package/hooks/validators/run_all_validators.py +7 -0
  72. package/hooks/validators/subprocess_window_access.py +41 -0
  73. package/hooks/workflow/auto_formatter.py +8 -0
  74. package/hooks/workflow/test_auto_formatter.py +39 -0
  75. package/package.json +1 -1
  76. package/rules/pstack-models.md +4 -3
  77. package/scripts/automatic_advisory/checkout.py +8 -1
  78. package/scripts/automatic_advisory/cli.py +9 -3
  79. package/scripts/automatic_advisory/config/constants.py +0 -2
  80. package/scripts/automatic_advisory/execution.py +7 -1
  81. package/scripts/automatic_advisory/git.py +9 -1
  82. package/scripts/automatic_advisory/process_host.py +3 -0
  83. package/scripts/claude_chain_runner.py +3 -0
  84. package/scripts/gh_artifact_upload.py +3 -0
  85. package/scripts/grok_headless_runner.py +3 -0
  86. package/scripts/grok_patch_artifacts.py +4 -0
  87. package/scripts/grok_worker_preflight.py +3 -0
  88. package/scripts/invoke_code_review.py +3 -0
  89. package/scripts/local_verification/command_runner.py +3 -0
  90. package/scripts/local_verification/git_state.py +3 -0
  91. package/scripts/policy_lint/adapter_detectors.py +48 -2
  92. package/scripts/policy_lint/adapter_pairing.py +150 -2
  93. package/scripts/policy_lint/adapters.py +46 -4
  94. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  95. package/scripts/policy_lint/config/constants.py +3 -0
  96. package/scripts/policy_lint/registry.py +1 -1
  97. package/scripts/policy_lint/selection_git.py +11 -0
  98. package/scripts/repository_checks/config/constants.py +5 -0
  99. package/scripts/repository_checks/config/policy_document.py +19 -0
  100. package/scripts/repository_checks/email_exemptions.py +14 -71
  101. package/scripts/repository_checks/path_exemptions.py +35 -0
  102. package/scripts/repository_checks/policy_document.py +120 -0
  103. package/scripts/repository_checks/tracked_secrets.py +27 -4
  104. package/scripts/setup_project_paths.py +3 -0
  105. package/scripts/spawn_grok_batch.py +3 -0
  106. package/scripts/subprocess_window_access.py +41 -0
  107. package/scripts/test_dispatcher_profile_import.py +4 -0
  108. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  109. package/scripts/tests/test_email_exemptions.py +37 -0
  110. package/scripts/tests/test_path_exemptions.py +164 -0
  111. package/scripts/tests/test_policy_document.py +104 -0
  112. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  113. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  114. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  115. package/scripts/tests/test_policy_lint_selection.py +34 -0
  116. package/scripts/tests/test_subprocess_window_access.py +118 -0
  117. package/scripts/tests/test_tracked_secrets.py +79 -0
  118. package/scripts/validate_instruction_pairs.py +3 -0
  119. package/scripts/verify_installable_package.py +5 -0
  120. package/scripts/automatic_advisory/window_flags.py +0 -41
  121. package/scripts/repository_checks/config/email_exemptions.py +0 -10
@@ -0,0 +1,164 @@
1
+ """Frozen-file path exceptions preserve every other scanner finding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+ from repository_checks.path_exemptions import load_path_exemptions
11
+ from repository_checks.tracked_secrets import collect_tracked_secret_findings
12
+
13
+ FROZEN_RELATIVE_PATH = "evidence/manifest.json"
14
+ OTHER_RELATIVE_PATH = "evidence/other.json"
15
+ HOME_PATH = "C:/Users/fixture-owner/source-document.md"
16
+ FROZEN_CONTENT = f'{{"source": "{HOME_PATH}"}}\n'
17
+ STALE_DIGEST = hashlib.sha256(b"different bytes").hexdigest()
18
+
19
+
20
+ def _write_config(root: Path, document: dict[str, object]) -> None:
21
+ directory = root / "config"
22
+ directory.mkdir(exist_ok=True)
23
+ (directory / "repository-policy.json").write_text(
24
+ json.dumps(document), encoding="utf-8"
25
+ )
26
+
27
+
28
+ def _write_frozen_file(root: Path, relative_path: str) -> str:
29
+ file_path = root / relative_path
30
+ file_path.parent.mkdir(parents=True, exist_ok=True)
31
+ file_path.write_text(FROZEN_CONTENT, encoding="utf-8")
32
+ return hashlib.sha256(file_path.read_bytes()).hexdigest()
33
+
34
+
35
+ def _entry(**changes: object) -> dict[str, object]:
36
+ return {
37
+ "path": FROZEN_RELATIVE_PATH,
38
+ "sha256": STALE_DIGEST,
39
+ "reason": "Frozen evidence file pinned by its consumers",
40
+ **changes,
41
+ }
42
+
43
+
44
+ def test_matching_digest_removes_every_finding(tmp_path: Path) -> None:
45
+ digest = _write_frozen_file(tmp_path, FROZEN_RELATIVE_PATH)
46
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [_entry(sha256=digest)]})
47
+ assert collect_tracked_secret_findings(tmp_path, [FROZEN_RELATIVE_PATH]) == []
48
+
49
+
50
+ def test_stale_digest_keeps_reporting_the_path(tmp_path: Path) -> None:
51
+ _write_frozen_file(tmp_path, FROZEN_RELATIVE_PATH)
52
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [_entry()]})
53
+ findings = collect_tracked_secret_findings(tmp_path, [FROZEN_RELATIVE_PATH])
54
+ assert [finding.relative_path for finding in findings] == [FROZEN_RELATIVE_PATH]
55
+ assert all(finding.message.startswith("[home-path]") for finding in findings)
56
+
57
+
58
+ def test_unlisted_path_keeps_reporting(tmp_path: Path) -> None:
59
+ digest = _write_frozen_file(tmp_path, FROZEN_RELATIVE_PATH)
60
+ _write_frozen_file(tmp_path, OTHER_RELATIVE_PATH)
61
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [_entry(sha256=digest)]})
62
+ findings = collect_tracked_secret_findings(
63
+ tmp_path, [FROZEN_RELATIVE_PATH, OTHER_RELATIVE_PATH]
64
+ )
65
+ assert [finding.relative_path for finding in findings] == [OTHER_RELATIVE_PATH]
66
+
67
+
68
+ def test_exempt_path_stays_out_of_the_email_exemption_identities(
69
+ tmp_path: Path,
70
+ ) -> None:
71
+ digest = _write_frozen_file(tmp_path, FROZEN_RELATIVE_PATH)
72
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [_entry(sha256=digest)]})
73
+ assert load_path_exemptions(tmp_path) == frozenset({(FROZEN_RELATIVE_PATH, digest)})
74
+
75
+
76
+ @pytest.mark.parametrize(
77
+ "path",
78
+ [
79
+ "/manifest.json",
80
+ "C:/manifest.json",
81
+ "../manifest.json",
82
+ "evidence/../manifest.json",
83
+ "*.json",
84
+ "evidence/?.json",
85
+ "evidence/[a].json",
86
+ "evidence\\manifest.json",
87
+ "evidence//manifest.json",
88
+ "./manifest.json",
89
+ "",
90
+ ],
91
+ )
92
+ def test_rejects_nonliteral_or_nonrelative_paths(tmp_path: Path, path: str) -> None:
93
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [_entry(path=path)]})
94
+ with pytest.raises(ValueError):
95
+ load_path_exemptions(tmp_path)
96
+
97
+
98
+ @pytest.mark.parametrize(
99
+ "entry",
100
+ [
101
+ None,
102
+ {},
103
+ _entry(reason=" "),
104
+ _entry(sha256="f" * 63),
105
+ _entry(sha256="z" * 64),
106
+ _entry(category="secret"),
107
+ ],
108
+ )
109
+ def test_rejects_invalid_entries(tmp_path: Path, entry: object) -> None:
110
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [entry]})
111
+ with pytest.raises(ValueError):
112
+ load_path_exemptions(tmp_path)
113
+
114
+
115
+ def test_rejects_duplicate_entries(tmp_path: Path) -> None:
116
+ _write_config(
117
+ tmp_path,
118
+ {
119
+ "version": 1,
120
+ "path_exemptions": [_entry(), _entry(reason="Another explanation")],
121
+ },
122
+ )
123
+ with pytest.raises(ValueError):
124
+ load_path_exemptions(tmp_path)
125
+
126
+
127
+ @pytest.mark.parametrize(
128
+ "content",
129
+ [
130
+ "{",
131
+ "[]",
132
+ '{"version":true,"path_exemptions":[]}',
133
+ '{"version":1,"path_exemptions":{}}',
134
+ '{"version":1,"path_exemptions":[],"extra":0}',
135
+ '{"version":1,"path_exemptions":[],"path_exemptions":[]}',
136
+ ],
137
+ )
138
+ def test_malformed_config_stops_scanning(tmp_path: Path, content: str) -> None:
139
+ (tmp_path / "config").mkdir()
140
+ (tmp_path / "config" / "repository-policy.json").write_text(
141
+ content, encoding="utf-8"
142
+ )
143
+ with pytest.raises(ValueError):
144
+ collect_tracked_secret_findings(tmp_path, [])
145
+
146
+
147
+ @pytest.mark.parametrize(
148
+ "document",
149
+ [
150
+ {"version": 1},
151
+ {"version": 1, "email_exemptions": []},
152
+ {"version": 1, "path_exemptions": []},
153
+ {"version": 1, "email_exemptions": [], "path_exemptions": []},
154
+ ],
155
+ )
156
+ def test_documents_missing_one_list_still_load(
157
+ tmp_path: Path, document: dict[str, object]
158
+ ) -> None:
159
+ _write_config(tmp_path, document)
160
+ assert load_path_exemptions(tmp_path) == frozenset()
161
+
162
+
163
+ def test_missing_config_has_no_exemptions(tmp_path: Path) -> None:
164
+ assert load_path_exemptions(tmp_path) == frozenset()
@@ -0,0 +1,104 @@
1
+ """The shared policy-document reader and entry validator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+ from repository_checks.policy_document import (
11
+ entry_path_and_digest,
12
+ read_policy_entries,
13
+ )
14
+
15
+ RELATIVE_PATH = "evidence/manifest.json"
16
+ DIGEST = hashlib.sha256(b"frozen bytes").hexdigest()
17
+ CONFIG_RELATIVE_PATH = "config/repository-policy.json"
18
+
19
+
20
+ def _write_config(root: Path, document: object) -> None:
21
+ config_path = root / CONFIG_RELATIVE_PATH
22
+ config_path.parent.mkdir(parents=True, exist_ok=True)
23
+ config_path.write_text(json.dumps(document), encoding="utf-8")
24
+
25
+
26
+ def _entry(**changes: object) -> dict[str, object]:
27
+ return {
28
+ "path": RELATIVE_PATH,
29
+ "sha256": DIGEST,
30
+ "reason": "Frozen evidence file pinned by its consumers",
31
+ **changes,
32
+ }
33
+
34
+
35
+ def test_reader_returns_the_recorded_entries_without_validating_them(
36
+ tmp_path: Path,
37
+ ) -> None:
38
+ """The reader validates the document, and the caller validates each entry."""
39
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [{"anything": 1}]})
40
+ assert read_policy_entries(tmp_path, "path_exemptions") == [{"anything": 1}]
41
+
42
+
43
+ def test_reader_returns_no_entries_for_a_field_the_document_omits(
44
+ tmp_path: Path,
45
+ ) -> None:
46
+ _write_config(tmp_path, {"version": 1, "path_exemptions": [_entry()]})
47
+ assert read_policy_entries(tmp_path, "email_exemptions") == []
48
+
49
+
50
+ def test_reader_rejects_a_configuration_symlinked_outside_the_repository(
51
+ tmp_path: Path,
52
+ ) -> None:
53
+ outside_path = tmp_path / "outside-policy.json"
54
+ outside_path.write_text(json.dumps({"version": 1}), encoding="utf-8")
55
+ repository_root = tmp_path / "repo"
56
+ (repository_root / "config").mkdir(parents=True)
57
+ (repository_root / CONFIG_RELATIVE_PATH).symlink_to(outside_path)
58
+ with pytest.raises(ValueError) as rejection:
59
+ read_policy_entries(repository_root, "path_exemptions")
60
+ assert (
61
+ str(rejection.value)
62
+ == "Repository policy configuration must remain inside the repository"
63
+ )
64
+
65
+
66
+ def test_entry_validator_returns_the_path_and_digest_without_the_reason() -> None:
67
+ assert entry_path_and_digest(_entry(), "Path exemption") == (
68
+ RELATIVE_PATH,
69
+ DIGEST,
70
+ )
71
+
72
+
73
+ @pytest.mark.parametrize(
74
+ ("entry", "expected_message"),
75
+ [
76
+ ({}, "Path exemptions require path, sha256, and reason"),
77
+ (_entry(path=1), "Path exemption path must name one repository file"),
78
+ (
79
+ _entry(path="../manifest.json"),
80
+ "Path exemption path must be a literal relative file path",
81
+ ),
82
+ (
83
+ _entry(sha256="F" * 64),
84
+ "Path exemption sha256 must contain 64 lowercase hex digits",
85
+ ),
86
+ (
87
+ _entry(reason=" "),
88
+ "Path exemption reason must explain why the exception is owned",
89
+ ),
90
+ ],
91
+ )
92
+ def test_entry_validator_names_the_subject_in_every_rejection(
93
+ entry: object, expected_message: str
94
+ ) -> None:
95
+ """One validator serves both families, so each message names its subject."""
96
+ with pytest.raises(ValueError) as rejection:
97
+ entry_path_and_digest(entry, "Path exemption")
98
+ assert str(rejection.value) == expected_message
99
+
100
+
101
+ def test_entry_validator_uses_the_subject_it_is_given() -> None:
102
+ with pytest.raises(ValueError) as rejection:
103
+ entry_path_and_digest({}, "Email exemption")
104
+ assert str(rejection.value) == "Email exemptions require path, sha256, and reason"
@@ -0,0 +1,101 @@
1
+ """Function-length diagnostics report only functions the change is responsible for.
2
+
3
+ The code-rule adapter runs the enforcer with ``defer_scope_to_caller`` set, so
4
+ the enforcer hands back every over-long function in the document and the caller
5
+ owns the scope decision. The engine's own changed-line filter keys on a
6
+ diagnostic's start line, and a function-length message carries no ``Line N:``
7
+ prefix, so before this scope pass a pre-existing long function blocked any
8
+ change that touched its file.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path, PurePosixPath
14
+
15
+ from policy_lint import adapters
16
+ from policy_lint.model import ContentOrigin, Diagnostic, Document
17
+ from policy_lint.selection import _changed_lines
18
+
19
+ FUNCTION_LENGTH_MESSAGE_FRAGMENT = "exceeds blocking threshold"
20
+ PRODUCTION_RELATIVE_PATH = "packages/claude-dev-env/scripts/sample_long_module.py"
21
+ OVER_THRESHOLD_BODY_LINE_COUNT = 70
22
+ UNDER_THRESHOLD_BODY_LINE_COUNT = 40
23
+ REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
24
+
25
+
26
+ def _counting_function_source(function_name: str, body_line_count: int) -> str:
27
+ all_body_lines = [
28
+ f" running_total = running_total + {each_index}"
29
+ for each_index in range(body_line_count)
30
+ ]
31
+ signature_lines = [f"def {function_name}(running_total: int) -> int:"]
32
+ return (
33
+ "\n".join(signature_lines + all_body_lines + [" return running_total"])
34
+ + "\n"
35
+ )
36
+
37
+
38
+ def _messages_for_document(document: Document) -> tuple[str, ...]:
39
+ all_diagnostics: tuple[Diagnostic, ...] = adapters.code_rule_diagnostics(
40
+ document, REPOSITORY_ROOT
41
+ )
42
+ return tuple(
43
+ each_diagnostic.message
44
+ for each_diagnostic in all_diagnostics
45
+ if FUNCTION_LENGTH_MESSAGE_FRAGMENT in each_diagnostic.message
46
+ )
47
+
48
+
49
+ def _function_length_messages(
50
+ prior_text: str | None, current_text: str
51
+ ) -> tuple[str, ...]:
52
+ return _messages_for_document(
53
+ Document(
54
+ PurePosixPath(PRODUCTION_RELATIVE_PATH),
55
+ current_text,
56
+ prior_text,
57
+ _changed_lines(prior_text, current_text),
58
+ ContentOrigin.REVISION_DIFF,
59
+ )
60
+ )
61
+
62
+
63
+ def test_should_leave_an_untouched_long_function_alone() -> None:
64
+ prior_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
65
+ current_text = f"import os\nimport sys\n{prior_text}"
66
+ assert _function_length_messages(prior_text, current_text) == ()
67
+
68
+
69
+ def test_should_report_a_function_the_change_grew_past_the_threshold() -> None:
70
+ prior_text = _counting_function_source("main", UNDER_THRESHOLD_BODY_LINE_COUNT)
71
+ current_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
72
+ assert _function_length_messages(prior_text, current_text) != ()
73
+
74
+
75
+ def test_should_report_an_over_long_function_the_change_adds() -> None:
76
+ prior_text = _counting_function_source("existing", UNDER_THRESHOLD_BODY_LINE_COUNT)
77
+ added_text = _counting_function_source("added", OVER_THRESHOLD_BODY_LINE_COUNT)
78
+ assert _function_length_messages(prior_text, f"{prior_text}\n{added_text}") != ()
79
+
80
+
81
+ def test_should_report_every_over_long_function_in_a_wholly_new_document() -> None:
82
+ current_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
83
+ assert _function_length_messages(None, current_text) != ()
84
+
85
+
86
+ def test_should_leave_a_long_function_alone_when_the_change_only_deletes() -> None:
87
+ current_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
88
+ prior_text = f"REMOVED_CONSTANT = 1\n{current_text}"
89
+ assert _function_length_messages(prior_text, current_text) == ()
90
+
91
+
92
+ def test_should_report_a_long_function_when_the_selection_names_no_changed_lines() -> None:
93
+ current_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
94
+ document = Document(
95
+ PurePosixPath(PRODUCTION_RELATIVE_PATH),
96
+ current_text,
97
+ None,
98
+ None,
99
+ ContentOrigin.WORKTREE,
100
+ )
101
+ assert _messages_for_document(document) != ()