claude-dev-env 2.18.0 → 2.20.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/skills/AGENTS.md +4 -4
- package/.agents/skills/_shared/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +119 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
- package/.agents/skills/e-code-review/SKILL.md +12 -1
- package/.agents/skills/e-code-review/reference/fix.md +5 -1
- package/.agents/skills/e-code-review/reference/loop.md +4 -0
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
- package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
- package/.agents/skills/pr-cleanup/SKILL.md +105 -75
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
- package/.agents/skills/pr-refinement/SKILL.md +31 -0
- package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
- package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
- package/.agents/skills/skill-builder/SKILL.md +1 -0
- package/.agents/skills-archived/bugteam/SKILL.md +1 -1
- package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
- package/.agents/skills-archived/comments/SKILL.md +1 -1
- package/.agents/skills-archived/descriptions/SKILL.md +1 -1
- package/.agents/skills-archived/reviews/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/pr-loop/AGENTS.md +2 -1
- package/_shared/pr-loop/preflight-proposal.md +56 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
- package/bin/ever-shipped-skills.mjs +14 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +23 -0
- package/bin/install.codex-rules.test.mjs +61 -6
- package/bin/install.mjs +57 -9
- package/bin/install.prune.test.mjs +65 -3
- package/bin/install.test.mjs +202 -2
- package/docs/CODE_RULES.md +6 -0
- package/hooks/advisory/conftest.py +10 -0
- package/hooks/advisory/migration_safety_advisor.py +14 -14
- package/hooks/advisory/refactor_guard.py +250 -144
- package/hooks/advisory/refactor_guard_test_support.py +46 -0
- package/hooks/advisory/test_migration_safety_advisor.py +120 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
- package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
- package/hooks/atomic_file_writer.py +81 -0
- package/hooks/blocking/AGENTS.md +2 -1
- package/hooks/blocking/block_main_commit.py +66 -33
- package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
- package/hooks/blocking/code_rules_blast_radius.py +194 -0
- package/hooks/blocking/code_rules_enforcer.py +12 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
- package/hooks/blocking/destructive_command_blocker.py +8 -7
- package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/precommit_code_rules_gate.py +14 -139
- package/hooks/blocking/sensitive_file_protector.py +8 -6
- package/hooks/blocking/session_edit_stage_gate.py +31 -1
- package/hooks/blocking/test_block_main_commit.py +145 -0
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
- package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +309 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
- package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
- package/hooks/blocking/test_destructive_command_blocker.py +154 -138
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +85 -25
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
- package/hooks/blocking/test_hook_subprocess_support.py +296 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
- package/hooks/blocking/test_pii_scanner.py +81 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
- package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +234 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
- package/hooks/blocking/test_sensitive_file_protector.py +145 -5
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
- package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
- package/hooks/blocking/test_test_preflight_check.py +80 -0
- package/hooks/git-hooks/AGENTS.md +1 -1
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
- package/hooks/git-hooks/post_commit.py +160 -51
- package/hooks/git-hooks/pre_commit.py +9 -5
- package/hooks/git-hooks/test_post_commit.py +203 -0
- package/hooks/git-hooks/test_pre_commit.py +2 -2
- package/hooks/git-hooks/test_pre_push.py +57 -0
- package/hooks/hooks_constants/AGENTS.md +3 -1
- package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
- package/hooks/hooks_constants/blast_radius_constants.py +14 -0
- package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
- package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
- package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
- package/hooks/json_file_reader.py +22 -0
- package/hooks/observability/test_instructions_loaded_logger.py +54 -0
- package/hooks/pending_sidecars.py +33 -0
- package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
- package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
- package/hooks/test_atomic_file_writer.py +73 -0
- package/hooks/test_json_file_reader.py +24 -0
- package/hooks/test_pending_sidecars.py +25 -0
- package/hooks/validation/mypy_validator.py +213 -80
- package/hooks/validation/test_mypy_validator.py +288 -13
- package/hooks/workflow/auto_formatter.py +225 -93
- package/hooks/workflow/investigation_tracker_reset.py +2 -0
- package/hooks/workflow/test_auto_formatter.py +261 -12
- package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
- package/package.json +1 -1
- package/rules/failure-blast-radius.md +126 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import io
|
|
6
5
|
import json
|
|
7
6
|
import subprocess
|
|
8
7
|
import sys
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from types import SimpleNamespace
|
|
11
9
|
|
|
12
10
|
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
13
11
|
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
@@ -20,11 +18,10 @@ from code_rules_enforcer import ( # noqa: E402
|
|
|
20
18
|
main,
|
|
21
19
|
validate_content,
|
|
22
20
|
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
validate_content=validate_content,
|
|
21
|
+
from code_rules_enforcer_test_support import ( # noqa: E402
|
|
22
|
+
run_enforcer_cli,
|
|
23
|
+
run_precheck,
|
|
24
|
+
run_serialized_payload_entrypoint,
|
|
28
25
|
)
|
|
29
26
|
|
|
30
27
|
_ENFORCER_SCRIPT_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
@@ -57,13 +54,7 @@ def _run_enforcer_cli(
|
|
|
57
54
|
Returns:
|
|
58
55
|
The completed process carrying stdout, stderr, and the exit code.
|
|
59
56
|
"""
|
|
60
|
-
return
|
|
61
|
-
[sys.executable, str(_ENFORCER_SCRIPT_PATH), *all_cli_arguments],
|
|
62
|
-
input="",
|
|
63
|
-
capture_output=True,
|
|
64
|
-
text=True,
|
|
65
|
-
check=False,
|
|
66
|
-
)
|
|
57
|
+
return run_enforcer_cli(_ENFORCER_SCRIPT_PATH, all_cli_arguments, None)
|
|
67
58
|
|
|
68
59
|
|
|
69
60
|
def _run_precheck(
|
|
@@ -80,28 +71,15 @@ def _run_precheck(
|
|
|
80
71
|
Returns:
|
|
81
72
|
The completed process carrying the pre-check stdout, stderr, and exit code.
|
|
82
73
|
"""
|
|
83
|
-
return
|
|
74
|
+
return run_precheck(Path(candidate_path), target_path, None)
|
|
84
75
|
|
|
85
76
|
|
|
86
77
|
def _run_main_with_edit_payload(
|
|
87
78
|
file_path: str,
|
|
88
79
|
old_string: str,
|
|
89
80
|
new_string: str,
|
|
90
|
-
monkeypatch: object,
|
|
91
|
-
capsys: object,
|
|
92
81
|
) -> str:
|
|
93
|
-
"""Drive ``main()`` through its stdin entry point for an Edit
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
file_path: The on-disk path the Edit targets.
|
|
97
|
-
old_string: The Edit's ``old_string`` fragment.
|
|
98
|
-
new_string: The Edit's ``new_string`` fragment.
|
|
99
|
-
monkeypatch: The pytest fixture used to redirect ``sys.stdin``.
|
|
100
|
-
capsys: The pytest fixture used to capture the deny payload on stdout.
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
The captured stdout, which holds the deny payload when violations fire.
|
|
104
|
-
"""
|
|
82
|
+
"""Drive ``main()`` through its stdin entry point for an Edit."""
|
|
105
83
|
edit_payload = json.dumps(
|
|
106
84
|
{
|
|
107
85
|
"tool_name": "Edit",
|
|
@@ -112,13 +90,8 @@ def _run_main_with_edit_payload(
|
|
|
112
90
|
},
|
|
113
91
|
}
|
|
114
92
|
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
code_rules_enforcer.main([])
|
|
118
|
-
except SystemExit:
|
|
119
|
-
pass
|
|
120
|
-
captured = getattr(capsys, "readouterr")()
|
|
121
|
-
return captured.out
|
|
93
|
+
captured_stdout, _exit_code = run_serialized_payload_entrypoint(main, edit_payload)
|
|
94
|
+
return captured_stdout
|
|
122
95
|
|
|
123
96
|
|
|
124
97
|
def test_precheck_stream_parameter_carries_no_banned_noun() -> None:
|
|
@@ -130,9 +103,7 @@ def test_precheck_stream_parameter_carries_no_banned_noun() -> None:
|
|
|
130
103
|
banned-noun check fires. Scanning the enforcer source at a production path
|
|
131
104
|
proves the introduced stream parameter is free of that violation."""
|
|
132
105
|
enforcer_source = _ENFORCER_SCRIPT_PATH.read_text(encoding="utf-8")
|
|
133
|
-
issues = code_rules_enforcer.
|
|
134
|
-
enforcer_source, "/project/src/code_rules_enforcer.py"
|
|
135
|
-
)
|
|
106
|
+
issues = validate_content(enforcer_source, "/project/src/code_rules_enforcer.py")
|
|
136
107
|
banned_noun_issues = [
|
|
137
108
|
each_issue
|
|
138
109
|
for each_issue in issues
|
|
@@ -234,8 +205,6 @@ def test_precheck_missing_candidate_errors_on_stderr_without_traceback(
|
|
|
234
205
|
|
|
235
206
|
def test_edit_deny_reason_includes_forecast_and_precheck_hint(
|
|
236
207
|
tmp_path_factory: object,
|
|
237
|
-
monkeypatch: object,
|
|
238
|
-
capsys: object,
|
|
239
208
|
) -> None:
|
|
240
209
|
"""An Edit whose new_string introduces a violation on a file that already
|
|
241
210
|
contains a separate violation elsewhere blocks on the fragment violation and
|
|
@@ -244,9 +213,7 @@ def test_edit_deny_reason_includes_forecast_and_precheck_hint(
|
|
|
244
213
|
production_directory = getattr(tmp_path_factory, "mktemp")("production_pkg")
|
|
245
214
|
untouched_print_violation = _UNTOUCHED_PRINT_VIOLATION_SOURCE
|
|
246
215
|
clean_before = _CLEAN_FRAGMENT_BEFORE
|
|
247
|
-
introduces_banned_noun_after = (
|
|
248
|
-
"def short_helper() -> int:\n output = 0\n return output\n"
|
|
249
|
-
)
|
|
216
|
+
introduces_banned_noun_after = "def short_helper() -> int:\n output = 0\n return output\n"
|
|
250
217
|
on_disk_before = untouched_print_violation + "\n" + clean_before
|
|
251
218
|
source_file = production_directory / "production_module.py"
|
|
252
219
|
source_file.write_text(on_disk_before, encoding="utf-8")
|
|
@@ -254,8 +221,6 @@ def test_edit_deny_reason_includes_forecast_and_precheck_hint(
|
|
|
254
221
|
str(source_file),
|
|
255
222
|
clean_before,
|
|
256
223
|
introduces_banned_noun_after,
|
|
257
|
-
monkeypatch,
|
|
258
|
-
capsys,
|
|
259
224
|
)
|
|
260
225
|
deny_payload = json.loads(stdout)
|
|
261
226
|
reason = deny_payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
@@ -263,9 +228,7 @@ def test_edit_deny_reason_includes_forecast_and_precheck_hint(
|
|
|
263
228
|
assert "output" in blocking_section, (
|
|
264
229
|
f"fragment-introduced banned-noun must block, got reason: {reason!r}"
|
|
265
230
|
)
|
|
266
|
-
assert forecast_section != "",
|
|
267
|
-
f"forecast section must be present, got reason: {reason!r}"
|
|
268
|
-
)
|
|
231
|
+
assert forecast_section != "", f"forecast section must be present, got reason: {reason!r}"
|
|
269
232
|
assert "Library print()" in forecast_section, (
|
|
270
233
|
f"forecast must name the untouched print violation, got reason: {reason!r}"
|
|
271
234
|
)
|
|
@@ -274,8 +237,6 @@ def test_edit_deny_reason_includes_forecast_and_precheck_hint(
|
|
|
274
237
|
|
|
275
238
|
def test_edit_clean_fragment_on_dirty_file_produces_no_deny_payload(
|
|
276
239
|
tmp_path_factory: object,
|
|
277
|
-
monkeypatch: object,
|
|
278
|
-
capsys: object,
|
|
279
240
|
) -> None:
|
|
280
241
|
"""A clean Edit fragment on a file that is dirty elsewhere must not block —
|
|
281
242
|
the forecast never converts a clean-fragment edit into a deny."""
|
|
@@ -290,8 +251,6 @@ def test_edit_clean_fragment_on_dirty_file_produces_no_deny_payload(
|
|
|
290
251
|
str(source_file),
|
|
291
252
|
clean_before,
|
|
292
253
|
clean_after,
|
|
293
|
-
monkeypatch,
|
|
294
|
-
capsys,
|
|
295
254
|
)
|
|
296
255
|
assert stdout == "", (
|
|
297
256
|
f"a clean fragment on a dirty file must not produce a deny payload, got stdout: {stdout!r}"
|
|
@@ -300,8 +259,6 @@ def test_edit_clean_fragment_on_dirty_file_produces_no_deny_payload(
|
|
|
300
259
|
|
|
301
260
|
def test_every_deny_reason_carries_the_precheck_hint(
|
|
302
261
|
tmp_path_factory: object,
|
|
303
|
-
monkeypatch: object,
|
|
304
|
-
capsys: object,
|
|
305
262
|
) -> None:
|
|
306
263
|
"""A deny with no forecast still appends the pre-check hint to the reason."""
|
|
307
264
|
production_directory = getattr(tmp_path_factory, "mktemp")("production_pkg")
|
|
@@ -313,8 +270,6 @@ def test_every_deny_reason_carries_the_precheck_hint(
|
|
|
313
270
|
str(source_file),
|
|
314
271
|
clean_before,
|
|
315
272
|
introduces_violation_after,
|
|
316
|
-
monkeypatch,
|
|
317
|
-
capsys,
|
|
318
273
|
)
|
|
319
274
|
deny_payload = json.loads(stdout)
|
|
320
275
|
reason = deny_payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
@@ -333,8 +288,6 @@ def test_every_deny_reason_carries_the_precheck_hint(
|
|
|
333
288
|
|
|
334
289
|
def test_forecast_skipped_when_edit_prior_is_unreconstructable(
|
|
335
290
|
tmp_path_factory: object,
|
|
336
|
-
monkeypatch: object,
|
|
337
|
-
capsys: object,
|
|
338
291
|
) -> None:
|
|
339
292
|
"""An Edit whose old_string is absent from the file has no reliable prior to
|
|
340
293
|
diff against, so the full-file forecast must not run: a pre-existing inline
|
|
@@ -350,8 +303,6 @@ def test_forecast_skipped_when_edit_prior_is_unreconstructable(
|
|
|
350
303
|
str(source_file),
|
|
351
304
|
absent_old,
|
|
352
305
|
introduces_print_new,
|
|
353
|
-
monkeypatch,
|
|
354
|
-
capsys,
|
|
355
306
|
)
|
|
356
307
|
deny_payload = json.loads(stdout)
|
|
357
308
|
reason = deny_payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
@@ -363,8 +314,6 @@ def test_forecast_skipped_when_edit_prior_is_unreconstructable(
|
|
|
363
314
|
|
|
364
315
|
def test_forecast_omits_the_fragment_introduced_violation(
|
|
365
316
|
tmp_path_factory: object,
|
|
366
|
-
monkeypatch: object,
|
|
367
|
-
capsys: object,
|
|
368
317
|
) -> None:
|
|
369
318
|
"""An Edit that introduces the file's only print() blocks on it without the
|
|
370
319
|
forecast re-listing that same violation under its full-file line number."""
|
|
@@ -378,8 +327,6 @@ def test_forecast_omits_the_fragment_introduced_violation(
|
|
|
378
327
|
str(source_file),
|
|
379
328
|
_CLEAN_FRAGMENT_BEFORE,
|
|
380
329
|
introduces_print_after,
|
|
381
|
-
monkeypatch,
|
|
382
|
-
capsys,
|
|
383
330
|
)
|
|
384
331
|
deny_payload = json.loads(stdout)
|
|
385
332
|
reason = deny_payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
@@ -454,9 +401,7 @@ def test_precheck_strips_candidate_byte_order_mark_before_validation(
|
|
|
454
401
|
staging_directory = getattr(tmp_path_factory, "mktemp")("staging")
|
|
455
402
|
production_directory = getattr(tmp_path_factory, "mktemp")("production_pkg")
|
|
456
403
|
candidate_file = staging_directory / "candidate.py"
|
|
457
|
-
candidate_file.write_text(
|
|
458
|
-
"\ufeff" + _VIOLATING_PRODUCTION_SOURCE, encoding="utf-8"
|
|
459
|
-
)
|
|
404
|
+
candidate_file.write_text("\ufeff" + _VIOLATING_PRODUCTION_SOURCE, encoding="utf-8")
|
|
460
405
|
target_path = str(production_directory / "production_module.py")
|
|
461
406
|
completed = _run_precheck(str(candidate_file), target_path)
|
|
462
407
|
assert completed.returncode == 1, (
|
|
@@ -478,9 +423,7 @@ def test_precheck_strips_every_leading_byte_order_mark_before_validation(
|
|
|
478
423
|
staging_directory = getattr(tmp_path_factory, "mktemp")("staging")
|
|
479
424
|
production_directory = getattr(tmp_path_factory, "mktemp")("production_pkg")
|
|
480
425
|
candidate_file = staging_directory / "candidate.py"
|
|
481
|
-
candidate_file.write_text(
|
|
482
|
-
"\ufeff\ufeff" + _VIOLATING_PRODUCTION_SOURCE, encoding="utf-8"
|
|
483
|
-
)
|
|
426
|
+
candidate_file.write_text("\ufeff\ufeff" + _VIOLATING_PRODUCTION_SOURCE, encoding="utf-8")
|
|
484
427
|
target_path = str(production_directory / "production_module.py")
|
|
485
428
|
completed = _run_precheck(str(candidate_file), target_path)
|
|
486
429
|
assert completed.returncode == 1, (
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Tests for the session-scratchpad exemption wired into code_rules_enforcer.main."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
-
import io
|
|
5
4
|
import json
|
|
6
5
|
import os
|
|
7
6
|
import sys
|
|
@@ -11,6 +10,13 @@ from types import ModuleType
|
|
|
11
10
|
|
|
12
11
|
import pytest
|
|
13
12
|
|
|
13
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
14
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
15
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
16
|
+
from blocking import _path_setup # noqa: F401
|
|
17
|
+
|
|
18
|
+
from code_rules_enforcer_test_support import run_serialized_payload_entrypoint
|
|
19
|
+
|
|
14
20
|
ENFORCER_PATH = Path(__file__).parent / "code_rules_enforcer.py"
|
|
15
21
|
FIXED_USER_ID = 6070
|
|
16
22
|
WORKING_DIRECTORY = "/home/user/project"
|
|
@@ -55,19 +61,11 @@ def _write_payload(target: Path) -> dict[str, object]:
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
|
|
58
|
-
def _run_main(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
monkeypatch.setattr(sys, "stdout", captured_stdout)
|
|
64
|
-
exit_code: int | None = None
|
|
65
|
-
try:
|
|
66
|
-
_ENFORCER.main([])
|
|
67
|
-
except SystemExit as raised_exit:
|
|
68
|
-
raw_code = raised_exit.code
|
|
69
|
-
exit_code = raw_code if isinstance(raw_code, int) else None
|
|
70
|
-
return exit_code, captured_stdout.getvalue()
|
|
64
|
+
def _run_main(payload_by_key: dict[str, object]) -> tuple[int | None, str]:
|
|
65
|
+
captured_stdout, exit_code = run_serialized_payload_entrypoint(
|
|
66
|
+
_ENFORCER.main, json.dumps(payload_by_key)
|
|
67
|
+
)
|
|
68
|
+
return exit_code, captured_stdout
|
|
71
69
|
|
|
72
70
|
|
|
73
71
|
def _decision_from(stdout_text: str) -> str | None:
|
|
@@ -84,7 +82,7 @@ def test_scratchpad_write_is_exempt_from_code_rules(
|
|
|
84
82
|
monkeypatch.setenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", "1")
|
|
85
83
|
throwaway_script = scratchpad_directory / "one_off_probe.py"
|
|
86
84
|
|
|
87
|
-
exit_code, stdout_text = _run_main(
|
|
85
|
+
exit_code, stdout_text = _run_main(_write_payload(throwaway_script))
|
|
88
86
|
|
|
89
87
|
assert exit_code == 0
|
|
90
88
|
assert _decision_from(stdout_text) != "deny"
|
|
@@ -99,7 +97,7 @@ def test_identical_non_scratchpad_write_is_still_blocked(
|
|
|
99
97
|
outside_directory.mkdir(parents=True)
|
|
100
98
|
production_module = outside_directory / "one_off_probe.py"
|
|
101
99
|
|
|
102
|
-
exit_code, stdout_text = _run_main(
|
|
100
|
+
exit_code, stdout_text = _run_main(_write_payload(production_module))
|
|
103
101
|
|
|
104
102
|
assert exit_code == 0
|
|
105
103
|
assert _decision_from(stdout_text) == "deny"
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import io
|
|
6
5
|
import json
|
|
7
6
|
import sys
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
from types import SimpleNamespace
|
|
10
9
|
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
11
12
|
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
12
13
|
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
13
14
|
if _BLOCKING_DIRECTORY not in sys.path:
|
|
@@ -23,6 +24,9 @@ from code_rules_enforcer import ( # noqa: E402
|
|
|
23
24
|
prior_and_post_edit_content,
|
|
24
25
|
validate_content,
|
|
25
26
|
)
|
|
27
|
+
from code_rules_enforcer_test_support import ( # noqa: E402
|
|
28
|
+
run_serialized_payload_entrypoint,
|
|
29
|
+
)
|
|
26
30
|
|
|
27
31
|
code_rules_enforcer = SimpleNamespace(
|
|
28
32
|
FUNCTION_LENGTH_BLOCKING_THRESHOLD=FUNCTION_LENGTH_BLOCKING_THRESHOLD,
|
|
@@ -35,9 +39,7 @@ code_rules_enforcer = SimpleNamespace(
|
|
|
35
39
|
|
|
36
40
|
def _oversized_function_source(name: str) -> str:
|
|
37
41
|
body_line_count = code_rules_enforcer.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
38
|
-
body_lines = [
|
|
39
|
-
f" bound_{each_index} = {each_index}" for each_index in range(body_line_count)
|
|
40
|
-
]
|
|
42
|
+
body_lines = [f" bound_{each_index} = {each_index}" for each_index in range(body_line_count)]
|
|
41
43
|
return f"def {name}() -> None:\n" + "\n".join(body_lines) + "\n"
|
|
42
44
|
|
|
43
45
|
|
|
@@ -45,21 +47,8 @@ def _run_main_with_edit_payload(
|
|
|
45
47
|
file_path: str,
|
|
46
48
|
old_string: str,
|
|
47
49
|
new_string: str,
|
|
48
|
-
monkeypatch: object,
|
|
49
|
-
capsys: object,
|
|
50
50
|
) -> str:
|
|
51
|
-
"""Drive ``main()`` through its stdin entry point for an Edit
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
file_path: The on-disk path the Edit targets.
|
|
55
|
-
old_string: The Edit's ``old_string`` fragment.
|
|
56
|
-
new_string: The Edit's ``new_string`` fragment.
|
|
57
|
-
monkeypatch: The pytest fixture used to redirect ``sys.stdin``.
|
|
58
|
-
capsys: The pytest fixture used to capture the deny payload on stdout.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
The captured stdout, which holds the deny payload when violations fire.
|
|
62
|
-
"""
|
|
51
|
+
"""Drive ``main()`` through its stdin entry point for an Edit."""
|
|
63
52
|
edit_payload = json.dumps(
|
|
64
53
|
{
|
|
65
54
|
"tool_name": "Edit",
|
|
@@ -70,13 +59,10 @@ def _run_main_with_edit_payload(
|
|
|
70
59
|
},
|
|
71
60
|
}
|
|
72
61
|
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
pass
|
|
78
|
-
captured = getattr(capsys, "readouterr")()
|
|
79
|
-
return captured.out
|
|
62
|
+
captured_stdout, _exit_code = run_serialized_payload_entrypoint(
|
|
63
|
+
code_rules_enforcer.main, edit_payload
|
|
64
|
+
)
|
|
65
|
+
return captured_stdout
|
|
80
66
|
|
|
81
67
|
|
|
82
68
|
def test_banned_noun_word_keeps_in_scope_binding_among_untouched_ones() -> None:
|
|
@@ -85,8 +71,7 @@ def test_banned_noun_word_keeps_in_scope_binding_among_untouched_ones() -> None:
|
|
|
85
71
|
binding while leaving the untouched bindings out of scope."""
|
|
86
72
|
leading_count = 5
|
|
87
73
|
leading_bindings = "".join(
|
|
88
|
-
f"LEADING_{each_index}_RESULT_PATH = {each_index}\n"
|
|
89
|
-
for each_index in range(leading_count)
|
|
74
|
+
f"LEADING_{each_index}_RESULT_PATH = {each_index}\n" for each_index in range(leading_count)
|
|
90
75
|
)
|
|
91
76
|
target_before = "PLACEHOLDER_NAME = 0\n"
|
|
92
77
|
target_after = "INTRODUCED_RESULT_PATH = 0\n"
|
|
@@ -99,9 +84,9 @@ def test_banned_noun_word_keeps_in_scope_binding_among_untouched_ones() -> None:
|
|
|
99
84
|
full_file_content=post_edit_full_file,
|
|
100
85
|
prior_full_file_content=prior_full_file,
|
|
101
86
|
)
|
|
102
|
-
assert any(
|
|
103
|
-
"
|
|
104
|
-
)
|
|
87
|
+
assert any("INTRODUCED_RESULT_PATH" in each_issue for each_issue in issues), (
|
|
88
|
+
f"in-scope banned-noun past the cap window must still block, got: {issues!r}"
|
|
89
|
+
)
|
|
105
90
|
|
|
106
91
|
|
|
107
92
|
def test_banned_noun_edit_drops_untouched_out_of_scope_binding() -> None:
|
|
@@ -122,9 +107,9 @@ def test_banned_noun_edit_drops_untouched_out_of_scope_binding() -> None:
|
|
|
122
107
|
full_file_content=post_edit_full_file,
|
|
123
108
|
prior_full_file_content=prior_full_file,
|
|
124
109
|
)
|
|
125
|
-
assert not any(
|
|
126
|
-
"
|
|
127
|
-
)
|
|
110
|
+
assert not any("RESULT_PATH" in each_issue for each_issue in issues), (
|
|
111
|
+
f"untouched banned-noun bindings must stay out of scope, got: {issues!r}"
|
|
112
|
+
)
|
|
128
113
|
|
|
129
114
|
|
|
130
115
|
def test_banned_noun_edit_keeps_touched_binding_in_scope() -> None:
|
|
@@ -144,9 +129,9 @@ def test_banned_noun_edit_keeps_touched_binding_in_scope() -> None:
|
|
|
144
129
|
full_file_content=post_edit_full_file,
|
|
145
130
|
prior_full_file_content=prior_full_file,
|
|
146
131
|
)
|
|
147
|
-
assert any(
|
|
148
|
-
"
|
|
149
|
-
)
|
|
132
|
+
assert any("INTRODUCED_RESULT_PATH" in each_issue for each_issue in issues), (
|
|
133
|
+
f"introduced banned-noun binding must block, got: {issues!r}"
|
|
134
|
+
)
|
|
150
135
|
|
|
151
136
|
|
|
152
137
|
def test_banned_noun_edit_does_not_reflag_param_when_unrelated_body_line_changes() -> None:
|
|
@@ -171,9 +156,9 @@ def test_banned_noun_edit_does_not_reflag_param_when_unrelated_body_line_changes
|
|
|
171
156
|
full_file_content=post_edit_full_file,
|
|
172
157
|
prior_full_file_content=prior_full_file,
|
|
173
158
|
)
|
|
174
|
-
assert not any(
|
|
175
|
-
"
|
|
176
|
-
)
|
|
159
|
+
assert not any("canned_results" in each_issue for each_issue in issues), (
|
|
160
|
+
f"pre-existing param must not re-flag on unrelated body edit, got: {issues!r}"
|
|
161
|
+
)
|
|
177
162
|
|
|
178
163
|
|
|
179
164
|
def test_unreadable_prior_yields_no_prior_and_no_reconstruction() -> None:
|
|
@@ -191,13 +176,13 @@ def test_unreadable_prior_yields_no_prior_and_no_reconstruction() -> None:
|
|
|
191
176
|
|
|
192
177
|
|
|
193
178
|
def test_edit_with_missing_old_string_runs_whole_file_against_on_disk_content(
|
|
194
|
-
tmp_path_factory:
|
|
179
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
195
180
|
) -> None:
|
|
196
181
|
"""When an Edit's old_string is absent from the file, ``prior_and_post_edit_content``
|
|
197
182
|
yields ``(None, None)``; ``main()`` must analyze the real on-disk file whole-file
|
|
198
183
|
rather than the new_string fragment, so an oversized function elsewhere in the
|
|
199
184
|
file is still reported with its true line numbers."""
|
|
200
|
-
production_directory =
|
|
185
|
+
production_directory = tmp_path_factory.mktemp("production_pkg")
|
|
201
186
|
untouched_long_function = _oversized_function_source("untouched_long")
|
|
202
187
|
short_helper = "def short_helper() -> int:\n return 1\n"
|
|
203
188
|
on_disk_content = untouched_long_function + "\n" + short_helper
|
|
@@ -206,7 +191,9 @@ def test_edit_with_missing_old_string_runs_whole_file_against_on_disk_content(
|
|
|
206
191
|
absent_fragment_old = "def absent_function() -> int:\n return 0\n"
|
|
207
192
|
short_fragment_new = "def absent_function() -> int:\n return 2\n"
|
|
208
193
|
stdout = _run_main_with_edit_payload(
|
|
209
|
-
str(source_file),
|
|
194
|
+
str(source_file),
|
|
195
|
+
absent_fragment_old,
|
|
196
|
+
short_fragment_new,
|
|
210
197
|
)
|
|
211
198
|
assert "untouched_long" in stdout, (
|
|
212
199
|
"an unreconstructable Edit must fall back to whole-file on-disk analysis, "
|
|
@@ -215,13 +202,13 @@ def test_edit_with_missing_old_string_runs_whole_file_against_on_disk_content(
|
|
|
215
202
|
|
|
216
203
|
|
|
217
204
|
def test_edit_with_unreadable_file_does_not_analyze_fragment_as_whole_file(
|
|
218
|
-
tmp_path_factory:
|
|
205
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
219
206
|
) -> None:
|
|
220
207
|
"""When the on-disk file cannot be read, no well-defined post-edit content
|
|
221
208
|
exists; ``main()`` must exit cleanly rather than analyze the new_string
|
|
222
209
|
fragment as if it were the whole file, so the fragment's own function-length
|
|
223
210
|
violation does not surface as a deny payload."""
|
|
224
|
-
production_directory =
|
|
211
|
+
production_directory = tmp_path_factory.mktemp("production_pkg")
|
|
225
212
|
missing_path = str(production_directory / "never_created.py")
|
|
226
213
|
oversized_fragment_old = "def grows() -> int:\n return 0\n"
|
|
227
214
|
oversized_fragment_new = _oversized_function_source("grows")
|
|
@@ -229,8 +216,6 @@ def test_edit_with_unreadable_file_does_not_analyze_fragment_as_whole_file(
|
|
|
229
216
|
missing_path,
|
|
230
217
|
oversized_fragment_old,
|
|
231
218
|
oversized_fragment_new,
|
|
232
|
-
monkeypatch,
|
|
233
|
-
capsys,
|
|
234
219
|
)
|
|
235
220
|
assert stdout == "", (
|
|
236
221
|
"an unreadable Edit target has no well-defined whole-file content, so the "
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Test stage ownership for the code-rules enforcer entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
14
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
15
|
+
_PR_LOOP_SCRIPTS_DIRECTORY = str(
|
|
16
|
+
Path(__file__).resolve().parents[2] / "_shared" / "pr-loop" / "scripts"
|
|
17
|
+
)
|
|
18
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
19
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
20
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
21
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
22
|
+
if _PR_LOOP_SCRIPTS_DIRECTORY not in sys.path:
|
|
23
|
+
sys.path.insert(0, _PR_LOOP_SCRIPTS_DIRECTORY)
|
|
24
|
+
|
|
25
|
+
from code_rules_enforcer import ( # noqa: E402
|
|
26
|
+
_fragment_or_deferred_check,
|
|
27
|
+
main,
|
|
28
|
+
validate_content,
|
|
29
|
+
)
|
|
30
|
+
from code_rules_gate_parts import gate_running # noqa: E402
|
|
31
|
+
from code_rules_gate_parts.tests._repo_test_helpers import ( # noqa: E402
|
|
32
|
+
init_repository,
|
|
33
|
+
write_commit_and_stage_change,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PRODUCTION_FILE_PATH = "packages/app/services.py"
|
|
38
|
+
ENFORCER_SCRIPT_PATH = Path(__file__).with_name("code_rules_enforcer.py")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _write_payload(file_path: str, source: str) -> str:
|
|
42
|
+
return json.dumps(
|
|
43
|
+
{
|
|
44
|
+
"tool_name": "Write",
|
|
45
|
+
"tool_input": {"file_path": file_path, "content": source},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_write_stage(
|
|
51
|
+
file_path: str,
|
|
52
|
+
source: str,
|
|
53
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
54
|
+
capsys: pytest.CaptureFixture[str],
|
|
55
|
+
) -> str:
|
|
56
|
+
payload = _write_payload(file_path, source)
|
|
57
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(payload))
|
|
58
|
+
try:
|
|
59
|
+
main([])
|
|
60
|
+
except SystemExit:
|
|
61
|
+
pass
|
|
62
|
+
return capsys.readouterr().out
|
|
63
|
+
def _run_precheck_stage(candidate_path: Path, target_path: str) -> subprocess.CompletedProcess[str]:
|
|
64
|
+
return subprocess.run(
|
|
65
|
+
[
|
|
66
|
+
sys.executable,
|
|
67
|
+
str(ENFORCER_SCRIPT_PATH),
|
|
68
|
+
"--check",
|
|
69
|
+
str(candidate_path),
|
|
70
|
+
"--as",
|
|
71
|
+
target_path,
|
|
72
|
+
],
|
|
73
|
+
check=False,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
)
|
|
77
|
+
def test_edit_stage_baselines_the_new_fragment_against_the_old_fragment() -> None:
|
|
78
|
+
all_seen_contents: list[str] = []
|
|
79
|
+
|
|
80
|
+
def record_check(content: str, file_path: str) -> list[str]:
|
|
81
|
+
all_seen_contents.append(content)
|
|
82
|
+
if "introduced" in content:
|
|
83
|
+
return [f"finding in {file_path}"]
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
all_issues = _fragment_or_deferred_check(
|
|
87
|
+
record_check,
|
|
88
|
+
"existing fragment",
|
|
89
|
+
"introduced fragment",
|
|
90
|
+
PRODUCTION_FILE_PATH,
|
|
91
|
+
defer_scope_to_caller=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert all_seen_contents == ["introduced fragment", "existing fragment"]
|
|
95
|
+
assert all_issues == [f"finding in {PRODUCTION_FILE_PATH}"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_commit_stage_owns_scope_and_scans_the_complete_candidate_once() -> None:
|
|
99
|
+
all_seen_contents: list[str] = []
|
|
100
|
+
|
|
101
|
+
def record_check(content: str, file_path: str) -> list[str]:
|
|
102
|
+
all_seen_contents.append(content)
|
|
103
|
+
return [f"finding in {file_path}"]
|
|
104
|
+
|
|
105
|
+
all_issues = _fragment_or_deferred_check(
|
|
106
|
+
record_check,
|
|
107
|
+
"HEAD content",
|
|
108
|
+
"staged candidate",
|
|
109
|
+
PRODUCTION_FILE_PATH,
|
|
110
|
+
defer_scope_to_caller=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert all_seen_contents == ["staged candidate"]
|
|
114
|
+
assert all_issues == [f"finding in {PRODUCTION_FILE_PATH}"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_write_stage_runs_the_real_entrypoint_and_reports_a_deny_payload(
|
|
118
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
119
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
120
|
+
capsys: pytest.CaptureFixture[str],
|
|
121
|
+
) -> None:
|
|
122
|
+
staging_directory = tmp_path_factory.mktemp("stage")
|
|
123
|
+
target_path = str(staging_directory / "service.py")
|
|
124
|
+
source = "def process_data() -> None:\n print('payload')\n"
|
|
125
|
+
|
|
126
|
+
captured_stdout = _run_write_stage(target_path, source, monkeypatch, capsys)
|
|
127
|
+
|
|
128
|
+
deny_payload = json.loads(captured_stdout)
|
|
129
|
+
deny_reason = deny_payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
130
|
+
assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
131
|
+
assert "process_data" in deny_reason
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_write_stage_allows_a_clean_candidate_through_the_real_entrypoint(
|
|
135
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
136
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
137
|
+
capsys: pytest.CaptureFixture[str],
|
|
138
|
+
) -> None:
|
|
139
|
+
staging_directory = tmp_path_factory.mktemp("stage")
|
|
140
|
+
target_path = str(staging_directory / "service.py")
|
|
141
|
+
source = "def calculate_total() -> int:\n return 0\n"
|
|
142
|
+
|
|
143
|
+
captured_stdout = _run_write_stage(target_path, source, monkeypatch, capsys)
|
|
144
|
+
|
|
145
|
+
assert captured_stdout == ""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_precheck_stage_runs_the_cli_and_uses_the_declared_target_path(
|
|
149
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
150
|
+
) -> None:
|
|
151
|
+
staging_directory = tmp_path_factory.mktemp("stage")
|
|
152
|
+
candidate_path = staging_directory / "candidate.py"
|
|
153
|
+
candidate_path.write_text(
|
|
154
|
+
"def process_data() -> None:\n print('payload')\n",
|
|
155
|
+
encoding="utf-8",
|
|
156
|
+
)
|
|
157
|
+
target_path = str(staging_directory / "service.py")
|
|
158
|
+
|
|
159
|
+
completed_process = _run_precheck_stage(candidate_path, target_path)
|
|
160
|
+
|
|
161
|
+
assert completed_process.returncode == 1
|
|
162
|
+
assert "process_data" in completed_process.stdout
|
|
163
|
+
def test_gate_caller_scans_staged_content_with_commit_stage_scope(
|
|
164
|
+
tmp_path: Path,
|
|
165
|
+
) -> None:
|
|
166
|
+
repository_root = tmp_path / "repository"
|
|
167
|
+
repository_root.mkdir()
|
|
168
|
+
init_repository(repository_root)
|
|
169
|
+
committed_source = "def calculate_total() -> int:\n return 0\n"
|
|
170
|
+
staged_source = (
|
|
171
|
+
"def calculate_total() -> int:\n"
|
|
172
|
+
" total = 9999\n"
|
|
173
|
+
" return total\n"
|
|
174
|
+
)
|
|
175
|
+
file_path = write_commit_and_stage_change(
|
|
176
|
+
repository_root,
|
|
177
|
+
"service.py",
|
|
178
|
+
committed_source,
|
|
179
|
+
staged_source,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
all_partitioned_violations = gate_running._scoped_violations_for_file(
|
|
183
|
+
validate_content=validate_content,
|
|
184
|
+
resolved_path=file_path,
|
|
185
|
+
repository_root=repository_root,
|
|
186
|
+
all_added_lines_for_file={2, 3},
|
|
187
|
+
should_read_staged_content=True,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert all_partitioned_violations is not None
|
|
191
|
+
all_blocking_violations, _all_advisory_violations = all_partitioned_violations
|
|
192
|
+
assert any("9999" in each_issue for each_issue in all_blocking_violations)
|