claude-dev-env 1.38.1 → 1.40.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/CLAUDE.md +10 -36
- package/_shared/pr-loop/audit-reply-template.md +147 -0
- package/_shared/pr-loop/fix-protocol.md +25 -4
- package/_shared/pr-loop/gh-payloads.md +37 -50
- package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
- package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +199 -0
- package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
- package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
- package/_shared/pr-loop/scripts/preflight.py +129 -2
- package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1116 -0
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
- package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
- package/_shared/pr-loop/state-schema.md +1 -1
- package/agents/clean-coder.md +2 -2
- package/agents/pr-description-writer.md +150 -52
- package/bin/install.mjs +6 -7
- package/bin/install.test.mjs +8 -0
- package/commands/doc-gist.md +16 -0
- package/commands/plan.md +0 -2
- package/commands/review-plan.md +1 -1
- package/docs/CODE_RULES.md +122 -2
- package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
- package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
- package/hooks/blocking/code_rules_enforcer.py +1143 -129
- package/hooks/blocking/convergence_gate_blocker.py +130 -0
- package/hooks/blocking/destructive_command_blocker.py +74 -0
- package/hooks/blocking/gh_body_arg_blocker.py +30 -0
- package/hooks/blocking/md_to_html_blocker.py +119 -0
- package/hooks/blocking/pr_description_enforcer.py +57 -22
- package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
- package/hooks/blocking/test_code_rules_enforcer.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
- package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
- package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
- package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
- package/hooks/blocking/test_destructive_command_blocker.py +146 -0
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
- package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
- package/hooks/blocking/test_md_to_html_blocker.py +317 -0
- package/hooks/blocking/test_pr_description_enforcer.py +69 -8
- package/hooks/config/any_type_config.py +7 -0
- package/hooks/config/banned_identifiers_constants.py +11 -0
- package/hooks/config/blocking_check_limits.py +38 -0
- package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
- package/hooks/config/code_rules_enforcer_constants.py +53 -0
- package/hooks/config/convergence_branch_constants.py +9 -0
- package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
- package/hooks/config/html_companion_constants.py +20 -0
- package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
- package/hooks/config/pr_description_enforcer_constants.py +14 -0
- package/hooks/config/test_banned_identifiers_constants.py +17 -0
- package/hooks/hooks.json +28 -20
- package/hooks/pyproject.toml +69 -0
- package/hooks/validators/mypy_integration.py +47 -1
- package/hooks/validators/run_all_validators.py +3 -3
- package/hooks/validators/test_mypy_integration.py +50 -1
- package/hooks/workflow/doc_gist_auto_publish.py +144 -0
- package/hooks/workflow/md_to_html_companion.py +365 -0
- package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
- package/hooks/workflow/test_md_to_html_companion.py +452 -0
- package/package.json +1 -1
- package/rules/gh-body-file.md +2 -0
- package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
- package/scripts/check.ps1 +106 -0
- package/scripts/config/timing.py +11 -0
- package/scripts/sweep_empty_dirs.py +138 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_sweep_empty_dirs.py +183 -0
- package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
- package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
- package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
- package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
- package/skills/bugteam/CONSTRAINTS.md +21 -22
- package/skills/bugteam/EXAMPLES.md +3 -3
- package/skills/bugteam/PROMPTS.md +227 -67
- package/skills/bugteam/SKILL.md +132 -455
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +112 -39
- package/skills/bugteam/reference/audit-contract.md +4 -22
- package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
- package/skills/bugteam/reference/design-rationale.md +2 -2
- package/skills/bugteam/reference/github-pr-reviews.md +50 -57
- package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
- package/skills/bugteam/reference/team-setup.md +111 -9
- package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
- package/skills/bugteam/scripts/README.md +60 -0
- package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +328 -0
- package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
- package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
- package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
- package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
- package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
- package/skills/bugteam/scripts/test_bugteam_preflight.py +309 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
- package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
- package/skills/bugteam/test_skill_additions.py +1 -11
- package/skills/code/SKILL.md +176 -0
- package/skills/copilot-review/SKILL.md +16 -0
- package/skills/doc-gist/SKILL.md +99 -0
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
- package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
- package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
- package/skills/doc-gist/references/examples/05-design-system.html +629 -0
- package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
- package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
- package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
- package/skills/doc-gist/references/examples/11-status-report.html +528 -0
- package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
- package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
- package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
- package/skills/doc-gist/references/examples/README.md +5 -0
- package/skills/doc-gist/scripts/config/__init__.py +0 -0
- package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
- package/skills/doc-gist/scripts/gist_upload.py +177 -0
- package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
- package/skills/findbugs/SKILL.md +96 -2
- package/skills/monitor-open-prs/SKILL.md +14 -32
- package/skills/monitor-open-prs/test_skill_contract.py +0 -11
- package/skills/pr-consistency-audit/SKILL.md +112 -0
- package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
- package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
- package/skills/pr-converge/SKILL.md +229 -23
- package/skills/pr-converge/config/__init__.py +0 -0
- package/skills/pr-converge/config/constants.py +63 -0
- package/skills/pr-converge/reference/convergence-gates.md +138 -44
- package/skills/pr-converge/reference/examples.md +43 -11
- package/skills/pr-converge/reference/fix-protocol.md +6 -5
- package/skills/pr-converge/reference/ground-rules.md +5 -3
- package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
- package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
- package/skills/pr-converge/reference/per-tick.md +107 -31
- package/skills/pr-converge/reference/state-schema.md +22 -1
- package/skills/pr-converge/reference/stop-conditions.md +9 -7
- package/skills/pr-converge/scripts/README.md +34 -46
- package/skills/pr-converge/scripts/check_bugbot_ci.py +279 -0
- package/skills/pr-converge/scripts/check_convergence.py +497 -0
- package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
- package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
- package/skills/qbug/SKILL.md +157 -27
- package/skills/session-log/SKILL.md +216 -114
- package/skills/session-tidy/SKILL.md +1 -1
- package/skills/skill-builder/SKILL.md +138 -56
- package/skills/skill-builder/references/delegation-map.md +72 -113
- package/skills/skill-builder/references/progressive-disclosure.md +122 -0
- package/skills/skill-builder/references/self-audit-checklist.md +92 -0
- package/skills/skill-builder/references/skill-types.md +228 -0
- package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
- package/skills/skill-builder/templates/gap-analysis.md +15 -8
- package/skills/skill-builder/workflows/improve-skill.md +86 -57
- package/skills/skill-builder/workflows/new-skill.md +80 -168
- package/skills/skill-builder/workflows/polish-skill.md +78 -54
- package/skills/structure-prompt/SKILL.md +50 -0
- package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
- package/skills/structure-prompt/reference/block-classification.md +27 -0
- package/skills/structure-prompt/reference/canonical-case.md +48 -0
- package/skills/structure-prompt/reference/citation-depth.md +70 -0
- package/skills/structure-prompt/reference/cleanup.md +33 -0
- package/skills/structure-prompt/reference/constraints.md +33 -0
- package/skills/structure-prompt/reference/directives.md +37 -0
- package/skills/structure-prompt/reference/examples.md +72 -0
- package/skills/structure-prompt/reference/instantiation.md +51 -0
- package/skills/structure-prompt/reference/output-contract.md +72 -0
- package/skills/structure-prompt/reference/per-category.md +23 -0
- package/skills/structure-prompt/reference/persona.md +38 -0
- package/skills/structure-prompt/reference/research.md +33 -0
- package/skills/structure-prompt/reference/structure.md +28 -0
- package/agents/code-standards-agent.md +0 -93
- package/agents/groq-coder.md +0 -113
- package/agents/plan-executor.md +0 -226
- package/agents/project-docs-analyzer.md +0 -53
- package/agents/project-structure-organizer-agent.md +0 -72
- package/agents/skill-to-agent-converter.md +0 -370
- package/agents/skill-writer-agent.md +0 -470
- package/agents/user-docs-writer.md +0 -67
- package/agents/workflow-visual-documenter.md +0 -82
- package/commands/readability-review.md +0 -20
- package/hooks/mypy.ini +0 -2
- package/hooks/notification/attention_needed_notify.py +0 -71
- package/hooks/notification/claude_notification_handler.py +0 -67
- package/hooks/notification/notification_utils.py +0 -267
- package/hooks/notification/subagent_complete_notify.py +0 -381
- package/hooks/notification/test_attention_needed_notify.py +0 -47
- package/hooks/notification/test_claude_notification_handler.py +0 -54
- package/hooks/notification/test_notification_utils.py +0 -91
- package/hooks/notification/test_subagent_complete_notify.py +0 -79
- package/scripts/config/groq_bugteam_config.py +0 -230
- package/scripts/config/test_groq_bugteam_config.py +0 -83
- package/scripts/config/test_spec_implementer_prompt.py +0 -32
- package/scripts/groq_bugteam.README.md +0 -131
- package/scripts/groq_bugteam.py +0 -647
- package/scripts/groq_bugteam_dotenv.py +0 -40
- package/scripts/groq_bugteam_spec.py +0 -226
- package/scripts/test_groq_bugteam.py +0 -529
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
- package/scripts/test_groq_bugteam_dotenv.py +0 -66
- package/scripts/test_groq_bugteam_spec.py +0 -338
- package/skills/bugteam/SKILL_EVALS.md +0 -309
- package/skills/dream/SKILL.md +0 -118
- package/skills/ingest/SKILL.md +0 -40
- package/skills/npm-creator/SKILL.md +0 -187
- package/skills/readability-review/SKILL.md +0 -127
- package/skills/resume-review/SKILL.md +0 -261
- package/skills/rule-audit/SKILL.md +0 -307
- package/skills/rule-creator/SKILL.md +0 -150
- package/skills/searching-obsidian-vault/SKILL.md +0 -131
- package/skills/skill-writer/REFERENCE.md +0 -284
- package/skills/skill-writer/SKILL.md +0 -222
- package/skills/tdd-team/SKILL.md +0 -128
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""TDD-pair tests for the underscore-prefixed _claude_permissions_common module.
|
|
2
|
+
|
|
3
|
+
The TDD enforcer matches a production filename ``X.py`` to ``test_X.py``;
|
|
4
|
+
``_claude_permissions_common.py`` carries a leading underscore that the
|
|
5
|
+
enforcer treats as part of the name. This file's tests are the canonical
|
|
6
|
+
match. The broader behavioral suite continues to live alongside, in
|
|
7
|
+
``test_claude_permissions_common.py``.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
_script_directory = str(Path(__file__).resolve().parent)
|
|
18
|
+
if _script_directory not in sys.path:
|
|
19
|
+
sys.path.insert(0, _script_directory)
|
|
20
|
+
|
|
21
|
+
import _claude_permissions_common as common_module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_write_atomically_with_mode_releases_fd_when_fdopen_raises(
|
|
25
|
+
tmp_path: Path,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""A failure inside os.fdopen must close the raw file descriptor."""
|
|
28
|
+
target_path = tmp_path / "settings.json.tmp"
|
|
29
|
+
with patch.object(
|
|
30
|
+
common_module.os, "fdopen", side_effect=MemoryError("fdopen failure")
|
|
31
|
+
):
|
|
32
|
+
with pytest.raises(MemoryError):
|
|
33
|
+
common_module.write_atomically_with_mode(
|
|
34
|
+
target_path, "payload", file_mode=0o600
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_get_mode_to_preserve_returns_existing_file_mode(
|
|
39
|
+
tmp_path: Path,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""When the file exists, the actual filesystem mode must be returned (not the default)."""
|
|
42
|
+
target_path = tmp_path / "settings.json"
|
|
43
|
+
target_path.write_text("{}", encoding="utf-8")
|
|
44
|
+
actual_filesystem_mode = target_path.stat().st_mode & 0o777
|
|
45
|
+
returned_mode = common_module.get_mode_to_preserve(target_path)
|
|
46
|
+
assert returned_mode == actual_filesystem_mode
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_write_atomically_with_mode_raises_oserror_when_open_fails(
|
|
50
|
+
tmp_path: Path,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""OSError from os.open must propagate (no fd leak path to test here)."""
|
|
53
|
+
target_path = tmp_path / "subdirectory" / "missing" / "settings.json.tmp"
|
|
54
|
+
with pytest.raises(OSError):
|
|
55
|
+
common_module.write_atomically_with_mode(
|
|
56
|
+
target_path, "payload", file_mode=0o600
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_write_atomically_unlinks_temp_when_fdopen_raises(
|
|
61
|
+
tmp_path: Path,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""A failure inside os.fdopen must remove the on-disk temp file.
|
|
64
|
+
|
|
65
|
+
Regression for loop1-9: the file existed the moment os.open returned,
|
|
66
|
+
but the OSError/MemoryError handler only closed the raw FD — leaving an
|
|
67
|
+
empty .tmp sibling on disk after the exception propagated up to
|
|
68
|
+
save_settings, where the unlink in the finally block was skipped because
|
|
69
|
+
`is_temp_owned_by_this_invocation` had not yet been set.
|
|
70
|
+
"""
|
|
71
|
+
target_path = tmp_path / "settings.json.tmp"
|
|
72
|
+
with patch.object(
|
|
73
|
+
common_module.os, "fdopen", side_effect=MemoryError("fdopen failure")
|
|
74
|
+
):
|
|
75
|
+
with pytest.raises(MemoryError):
|
|
76
|
+
common_module.write_atomically_with_mode(
|
|
77
|
+
target_path, "payload", file_mode=0o600
|
|
78
|
+
)
|
|
79
|
+
assert not target_path.exists(), (
|
|
80
|
+
"the temp file created by os.open must be unlinked before re-raising"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_save_settings_uses_pid_keyed_temp_suffix(tmp_path: Path) -> None:
|
|
85
|
+
"""Concurrent save_settings invocations must not race on a deterministic temp name.
|
|
86
|
+
|
|
87
|
+
Regression for loop1-10: building the temp path as `settings.json.tmp` is
|
|
88
|
+
deterministic and unkeyed by PID, so two parallel /bugteam invocations
|
|
89
|
+
racing on Step 0 (grant) or Step 5 (revoke) collide on O_CREAT|O_EXCL —
|
|
90
|
+
the second caller hits FileExistsError and the OSError handler hard-exits,
|
|
91
|
+
silently dropping that PR's permission grant.
|
|
92
|
+
"""
|
|
93
|
+
settings_path = tmp_path / "settings.json"
|
|
94
|
+
settings_path.write_text("{}", encoding="utf-8")
|
|
95
|
+
captured_temp_paths: list[str] = []
|
|
96
|
+
real_open = common_module.os.open
|
|
97
|
+
|
|
98
|
+
def capturing_open(target: str, *args: object, **kwargs: object) -> int:
|
|
99
|
+
captured_temp_paths.append(target)
|
|
100
|
+
return real_open(target, *args, **kwargs)
|
|
101
|
+
|
|
102
|
+
with patch.object(common_module.os, "open", side_effect=capturing_open):
|
|
103
|
+
common_module.save_settings(settings_path, {"first": True})
|
|
104
|
+
common_module.save_settings(settings_path, {"second": True})
|
|
105
|
+
assert len(captured_temp_paths) == 2
|
|
106
|
+
assert all(
|
|
107
|
+
str(common_module.os.getpid()) in each_temp_path
|
|
108
|
+
for each_temp_path in captured_temp_paths
|
|
109
|
+
), (
|
|
110
|
+
"temp filename must include os.getpid() so concurrent runs do not "
|
|
111
|
+
f"collide on a deterministic name; saw: {captured_temp_paths!r}"
|
|
112
|
+
)
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import unittest.mock
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
10
|
+
if str(SCRIPT_DIRECTORY) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(SCRIPT_DIRECTORY))
|
|
12
|
+
|
|
13
|
+
import bugteam_code_rules_gate as gate_module
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_git_in_repository(repository_root: Path, *arguments: str) -> str:
|
|
17
|
+
completion = subprocess.run(
|
|
18
|
+
["git", *arguments],
|
|
19
|
+
cwd=str(repository_root),
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
encoding="utf-8",
|
|
23
|
+
errors="replace",
|
|
24
|
+
check=True,
|
|
25
|
+
)
|
|
26
|
+
return completion.stdout
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def initialize_git_repository(repository_root: Path) -> None:
|
|
30
|
+
run_git_in_repository(repository_root, "init")
|
|
31
|
+
run_git_in_repository(repository_root, "symbolic-ref", "HEAD", "refs/heads/main")
|
|
32
|
+
run_git_in_repository(repository_root, "config", "user.email", "test@example.com")
|
|
33
|
+
run_git_in_repository(repository_root, "config", "user.name", "Test")
|
|
34
|
+
run_git_in_repository(repository_root, "config", "commit.gpgsign", "false")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def commit_all_files(repository_root: Path, commit_message: str) -> None:
|
|
38
|
+
run_git_in_repository(repository_root, "add", "-A")
|
|
39
|
+
run_git_in_repository(repository_root, "commit", "-m", commit_message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_file(file_path: Path, content: str) -> None:
|
|
43
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
file_path.write_text(content, encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def stage_file(repository_root: Path, relative_path: str) -> None:
|
|
48
|
+
run_git_in_repository(repository_root, "add", "--", relative_path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture()
|
|
52
|
+
def temporary_git_repository(tmp_path: Path) -> Path:
|
|
53
|
+
repository_root = tmp_path / "repository_under_test"
|
|
54
|
+
repository_root.mkdir()
|
|
55
|
+
initialize_git_repository(repository_root)
|
|
56
|
+
return repository_root
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_paths_from_git_staged_returns_staged_files(
|
|
60
|
+
temporary_git_repository: Path,
|
|
61
|
+
) -> None:
|
|
62
|
+
write_file(temporary_git_repository / "committed_file.py", "one = 1\n")
|
|
63
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
64
|
+
write_file(temporary_git_repository / "newly_staged_file.py", "two = 2\n")
|
|
65
|
+
write_file(temporary_git_repository / "unstaged_file.py", "three = 3\n")
|
|
66
|
+
stage_file(temporary_git_repository, "newly_staged_file.py")
|
|
67
|
+
|
|
68
|
+
staged_paths = gate_module.paths_from_git_staged(temporary_git_repository)
|
|
69
|
+
|
|
70
|
+
staged_names = {path.name for path in staged_paths}
|
|
71
|
+
assert "newly_staged_file.py" in staged_names
|
|
72
|
+
assert "unstaged_file.py" not in staged_names
|
|
73
|
+
assert "committed_file.py" not in staged_names
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_added_lines_for_staged_file_reports_new_lines(
|
|
77
|
+
temporary_git_repository: Path,
|
|
78
|
+
) -> None:
|
|
79
|
+
write_file(temporary_git_repository / "target.py", "first = 1\nsecond = 2\n")
|
|
80
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
81
|
+
write_file(
|
|
82
|
+
temporary_git_repository / "target.py",
|
|
83
|
+
"first = 1\nsecond = 2\nthird = 3\nfourth = 4\n",
|
|
84
|
+
)
|
|
85
|
+
stage_file(temporary_git_repository, "target.py")
|
|
86
|
+
|
|
87
|
+
added_line_numbers = gate_module.added_lines_for_staged_file(
|
|
88
|
+
temporary_git_repository,
|
|
89
|
+
"target.py",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert 3 in added_line_numbers
|
|
93
|
+
assert 4 in added_line_numbers
|
|
94
|
+
assert 1 not in added_line_numbers
|
|
95
|
+
assert 2 not in added_line_numbers
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_added_lines_for_staged_file_treats_new_file_as_fully_added(
|
|
99
|
+
temporary_git_repository: Path,
|
|
100
|
+
) -> None:
|
|
101
|
+
write_file(temporary_git_repository / "existing.py", "ignored = 0\n")
|
|
102
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
103
|
+
write_file(
|
|
104
|
+
temporary_git_repository / "brand_new.py",
|
|
105
|
+
"alpha = 1\nbeta = 2\ngamma = 3\n",
|
|
106
|
+
)
|
|
107
|
+
stage_file(temporary_git_repository, "brand_new.py")
|
|
108
|
+
|
|
109
|
+
added_line_numbers = gate_module.added_lines_for_staged_file(
|
|
110
|
+
temporary_git_repository,
|
|
111
|
+
"brand_new.py",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
assert added_line_numbers == {1, 2, 3}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_paths_from_git_staged_uses_null_delimiter(
|
|
118
|
+
temporary_git_repository: Path,
|
|
119
|
+
) -> None:
|
|
120
|
+
write_file(temporary_git_repository / "first.py", "a = 1\n")
|
|
121
|
+
write_file(temporary_git_repository / "second.py", "b = 2\n")
|
|
122
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
123
|
+
write_file(temporary_git_repository / "first.py", "a = 10\n")
|
|
124
|
+
write_file(temporary_git_repository / "second.py", "b = 20\n")
|
|
125
|
+
stage_file(temporary_git_repository, "first.py")
|
|
126
|
+
stage_file(temporary_git_repository, "second.py")
|
|
127
|
+
|
|
128
|
+
staged_paths = gate_module.paths_from_git_staged(temporary_git_repository)
|
|
129
|
+
|
|
130
|
+
staged_names = {path.name for path in staged_paths}
|
|
131
|
+
assert staged_names == {"first.py", "second.py"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_paths_from_git_staged_warns_and_skips_non_utf8_filename(
|
|
135
|
+
tmp_path: Path,
|
|
136
|
+
capsys: pytest.CaptureFixture[str],
|
|
137
|
+
) -> None:
|
|
138
|
+
non_utf8_raw = b"valid.py\x00\xff\xfe_bad.py\x00"
|
|
139
|
+
mock_completed = unittest.mock.MagicMock()
|
|
140
|
+
mock_completed.returncode = 0
|
|
141
|
+
mock_completed.stdout = non_utf8_raw
|
|
142
|
+
|
|
143
|
+
with unittest.mock.patch("subprocess.run", return_value=mock_completed):
|
|
144
|
+
result_paths = gate_module.paths_from_git_staged(tmp_path)
|
|
145
|
+
|
|
146
|
+
captured = capsys.readouterr()
|
|
147
|
+
assert "non-UTF-8" in captured.err
|
|
148
|
+
assert len(result_paths) == 1
|
|
149
|
+
assert result_paths[0].name == "valid.py"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_staged_added_lines_by_file_maps_every_staged_code_file(
|
|
153
|
+
temporary_git_repository: Path,
|
|
154
|
+
) -> None:
|
|
155
|
+
write_file(temporary_git_repository / "already_committed.py", "zero = 0\n")
|
|
156
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
157
|
+
write_file(
|
|
158
|
+
temporary_git_repository / "already_committed.py",
|
|
159
|
+
"zero = 0\nappended = 1\n",
|
|
160
|
+
)
|
|
161
|
+
write_file(temporary_git_repository / "added_file.py", "only = 1\n")
|
|
162
|
+
stage_file(temporary_git_repository, "already_committed.py")
|
|
163
|
+
stage_file(temporary_git_repository, "added_file.py")
|
|
164
|
+
|
|
165
|
+
staged_paths = gate_module.paths_from_git_staged(temporary_git_repository)
|
|
166
|
+
added_lines_map = gate_module.added_lines_by_file_staged(
|
|
167
|
+
temporary_git_repository,
|
|
168
|
+
staged_paths,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
resolved_repository_root = temporary_git_repository.resolve()
|
|
172
|
+
assert added_lines_map[resolved_repository_root / "already_committed.py"] == {2}
|
|
173
|
+
assert added_lines_map[resolved_repository_root / "added_file.py"] == {1}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_main_staged_mode_blocks_when_staged_lines_introduce_violations(
|
|
177
|
+
temporary_git_repository: Path,
|
|
178
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
179
|
+
) -> None:
|
|
180
|
+
write_file(temporary_git_repository / "module.py", "first_value = 1\n")
|
|
181
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
182
|
+
staged_content_with_banned_identifier = (
|
|
183
|
+
"first_value = 1\n"
|
|
184
|
+
"def compute_total(operand):\n"
|
|
185
|
+
" result = operand + 1\n"
|
|
186
|
+
" return result\n"
|
|
187
|
+
)
|
|
188
|
+
write_file(
|
|
189
|
+
temporary_git_repository / "module.py",
|
|
190
|
+
staged_content_with_banned_identifier,
|
|
191
|
+
)
|
|
192
|
+
stage_file(temporary_git_repository, "module.py")
|
|
193
|
+
|
|
194
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
195
|
+
exit_code = gate_module.main(["--staged"])
|
|
196
|
+
|
|
197
|
+
assert exit_code == 1
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_main_staged_mode_passes_when_no_staged_violations(
|
|
201
|
+
temporary_git_repository: Path,
|
|
202
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
203
|
+
) -> None:
|
|
204
|
+
write_file(temporary_git_repository / "module.py", "first_value = 1\n")
|
|
205
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
206
|
+
write_file(
|
|
207
|
+
temporary_git_repository / "module.py", "first_value = 1\nsecond_value = 2\n"
|
|
208
|
+
)
|
|
209
|
+
stage_file(temporary_git_repository, "module.py")
|
|
210
|
+
|
|
211
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
212
|
+
exit_code = gate_module.main(["--staged"])
|
|
213
|
+
|
|
214
|
+
assert exit_code == 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_main_staged_mode_exits_zero_when_nothing_staged(
|
|
218
|
+
temporary_git_repository: Path,
|
|
219
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
220
|
+
) -> None:
|
|
221
|
+
write_file(temporary_git_repository / "module.py", "first_value = 1\n")
|
|
222
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
223
|
+
|
|
224
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
225
|
+
exit_code = gate_module.main(["--staged"])
|
|
226
|
+
|
|
227
|
+
assert exit_code == 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_added_lines_for_staged_file_returns_empty_for_modified_file_with_no_additions(
|
|
231
|
+
temporary_git_repository: Path,
|
|
232
|
+
) -> None:
|
|
233
|
+
write_file(
|
|
234
|
+
temporary_git_repository / "existing.py",
|
|
235
|
+
"alpha = 1\nbeta = 2\ngamma = 3\n",
|
|
236
|
+
)
|
|
237
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
238
|
+
write_file(temporary_git_repository / "existing.py", "alpha = 1\nbeta = 2\n")
|
|
239
|
+
stage_file(temporary_git_repository, "existing.py")
|
|
240
|
+
|
|
241
|
+
added_line_numbers = gate_module.added_lines_for_staged_file(
|
|
242
|
+
temporary_git_repository,
|
|
243
|
+
"existing.py",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert added_line_numbers == set()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_is_file_absent_in_index_head_does_not_exist_in_module() -> None:
|
|
250
|
+
assert not hasattr(gate_module, "is_file_absent_in_index_head")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_staged_file_line_count_raises_on_git_show_failure(
|
|
254
|
+
tmp_path: Path,
|
|
255
|
+
capsys: pytest.CaptureFixture[str],
|
|
256
|
+
) -> None:
|
|
257
|
+
"""git show failure must surface as SystemExit + stderr, never silent 0."""
|
|
258
|
+
failing_completed = unittest.mock.MagicMock()
|
|
259
|
+
failing_completed.returncode = 128
|
|
260
|
+
failing_completed.stdout = ""
|
|
261
|
+
failing_completed.stderr = "fatal: bad object :missing\n"
|
|
262
|
+
with unittest.mock.patch("subprocess.run", return_value=failing_completed):
|
|
263
|
+
with pytest.raises(SystemExit):
|
|
264
|
+
gate_module.staged_file_line_count(tmp_path, "missing.py")
|
|
265
|
+
captured = capsys.readouterr()
|
|
266
|
+
assert "git show" in captured.err
|
|
267
|
+
assert "fatal: bad object" in captured.err
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_is_staged_file_newly_added_raises_on_git_failure(
|
|
271
|
+
tmp_path: Path,
|
|
272
|
+
capsys: pytest.CaptureFixture[str],
|
|
273
|
+
) -> None:
|
|
274
|
+
"""git diff --name-status failure must surface as SystemExit + stderr."""
|
|
275
|
+
failing_completed = unittest.mock.MagicMock()
|
|
276
|
+
failing_completed.returncode = 128
|
|
277
|
+
failing_completed.stdout = ""
|
|
278
|
+
failing_completed.stderr = "fatal: not a git repository\n"
|
|
279
|
+
with unittest.mock.patch("subprocess.run", return_value=failing_completed):
|
|
280
|
+
with pytest.raises(SystemExit):
|
|
281
|
+
gate_module.is_staged_file_newly_added(tmp_path, "anything.py")
|
|
282
|
+
captured = capsys.readouterr()
|
|
283
|
+
assert "git diff --cached --name-status" in captured.err
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_whole_file_line_set_raises_system_exit_on_oserror(
|
|
287
|
+
tmp_path: Path,
|
|
288
|
+
capsys: pytest.CaptureFixture[str],
|
|
289
|
+
) -> None:
|
|
290
|
+
"""OSError reading a file must propagate as SystemExit, not silently return ``set()``.
|
|
291
|
+
|
|
292
|
+
Regression for loop1-7: returning an empty set on OSError caused the gate
|
|
293
|
+
to route every violation to the advisory bucket and exit 0 — silently
|
|
294
|
+
downgrading blocking violations to non-blocking on a read failure.
|
|
295
|
+
"""
|
|
296
|
+
unreadable_path = tmp_path / "broken.py"
|
|
297
|
+
with unittest.mock.patch.object(
|
|
298
|
+
Path, "read_text", side_effect=PermissionError("denied")
|
|
299
|
+
):
|
|
300
|
+
with pytest.raises(SystemExit):
|
|
301
|
+
gate_module.whole_file_line_set(unreadable_path)
|
|
302
|
+
captured = capsys.readouterr()
|
|
303
|
+
assert str(unreadable_path) in captured.err
|
|
304
|
+
assert "denied" in captured.err or "PermissionError" in captured.err
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_check_database_column_string_magic_signals_cap_exit(
|
|
308
|
+
capsys: pytest.CaptureFixture[str],
|
|
309
|
+
) -> None:
|
|
310
|
+
"""When the issue cap is hit, a 'cap reached' note must reach stderr."""
|
|
311
|
+
source_with_many_column_tuples = "\n".join(
|
|
312
|
+
[
|
|
313
|
+
"def write_rows():",
|
|
314
|
+
" rows = [",
|
|
315
|
+
*[
|
|
316
|
+
f" ('column_name_{each_index}', {each_index}),"
|
|
317
|
+
for each_index in range(10)
|
|
318
|
+
],
|
|
319
|
+
" ]",
|
|
320
|
+
" return rows",
|
|
321
|
+
]
|
|
322
|
+
)
|
|
323
|
+
issues = gate_module.check_database_column_string_magic(
|
|
324
|
+
source_with_many_column_tuples,
|
|
325
|
+
"production/file.py",
|
|
326
|
+
)
|
|
327
|
+
assert len(issues) == 3
|
|
328
|
+
captured = capsys.readouterr()
|
|
329
|
+
assert "cap reached" in captured.err.lower()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_check_wrapper_plumb_through_signals_cap_exit(
|
|
333
|
+
capsys: pytest.CaptureFixture[str],
|
|
334
|
+
) -> None:
|
|
335
|
+
"""check_wrapper_plumb_through must signal when MAXIMUM_ISSUES_TO_REPORT trims."""
|
|
336
|
+
delegate_definition = (
|
|
337
|
+
"def delegate(*, optional_one=1, optional_two=2, optional_three=3,"
|
|
338
|
+
" optional_four=4): return 0\n"
|
|
339
|
+
)
|
|
340
|
+
wrappers_block = "\n".join(
|
|
341
|
+
f"def wrapper_{each_index}():\n return self.delegate()"
|
|
342
|
+
for each_index in range(5)
|
|
343
|
+
)
|
|
344
|
+
source_with_many_wrappers = delegate_definition + wrappers_block + "\n"
|
|
345
|
+
issues = gate_module.check_wrapper_plumb_through(
|
|
346
|
+
source_with_many_wrappers,
|
|
347
|
+
"production/wrappers.py",
|
|
348
|
+
)
|
|
349
|
+
assert len(issues) == 3
|
|
350
|
+
captured = capsys.readouterr()
|
|
351
|
+
assert "cap reached" in captured.err.lower()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_run_gate_exits_nonzero_when_a_file_is_unreadable(
|
|
355
|
+
tmp_path: Path,
|
|
356
|
+
capsys: pytest.CaptureFixture[str],
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Skipping an unreadable file during run_gate must cause a non-zero exit."""
|
|
359
|
+
target_file = tmp_path / "sample.py"
|
|
360
|
+
target_file.write_text("clean = 1\n", encoding="utf-8")
|
|
361
|
+
|
|
362
|
+
def fake_validate(_content: str, _path: str, **_kwargs: object) -> list[str]:
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
with unittest.mock.patch.object(
|
|
366
|
+
Path, "read_text", side_effect=PermissionError("denied")
|
|
367
|
+
):
|
|
368
|
+
exit_code = gate_module.run_gate(
|
|
369
|
+
fake_validate,
|
|
370
|
+
[target_file],
|
|
371
|
+
tmp_path,
|
|
372
|
+
all_added_lines_map=None,
|
|
373
|
+
)
|
|
374
|
+
captured = capsys.readouterr()
|
|
375
|
+
assert exit_code != 0, (
|
|
376
|
+
"Files skipped due to read errors must produce a non-zero gate exit"
|
|
377
|
+
)
|
|
378
|
+
assert "skip unreadable" in captured.err
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_added_lines_for_staged_file_returns_parsed_result_when_diff_is_non_empty_even_if_parse_returns_empty(
|
|
382
|
+
temporary_git_repository: Path,
|
|
383
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
384
|
+
) -> None:
|
|
385
|
+
write_file(
|
|
386
|
+
temporary_git_repository / "sample.py",
|
|
387
|
+
"alpha = 1\nbeta = 2\n",
|
|
388
|
+
)
|
|
389
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
390
|
+
write_file(temporary_git_repository / "sample.py", "alpha = 1\nbeta = 2\ngamma = 3\n")
|
|
391
|
+
stage_file(temporary_git_repository, "sample.py")
|
|
392
|
+
|
|
393
|
+
monkeypatch.setattr(gate_module, "parse_added_line_numbers", lambda _text: set())
|
|
394
|
+
|
|
395
|
+
added_line_numbers = gate_module.added_lines_for_staged_file(
|
|
396
|
+
temporary_git_repository,
|
|
397
|
+
"sample.py",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
assert added_line_numbers == set()
|