claude-dev-env 7.4.1 → 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.
- package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
- package/.agents/skills/fresh-branch/SKILL.md +1 -0
- package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
- package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/syncing-submodules/SKILL.md +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
- package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/SKILL.md +1 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
- package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
- package/_shared/pr-loop/scripts/preflight.py +7 -0
- package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
- package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
- package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
- package/bin/install-constants.mjs +4 -2
- package/bin/install.cursor-rules.test.mjs +57 -15
- package/bin/install.mjs +10 -16
- package/bin/install.policy-lint.test.mjs +1 -2
- package/bin/install.test.mjs +0 -1
- package/bin/pstack-model-preferences.claude.json +23 -0
- package/hooks/advisory/msys_path_conversion_advisor.py +200 -0
- package/hooks/advisory/pr_done_reminder.py +14 -13
- package/hooks/advisory/refactor_guard.py +8 -0
- package/hooks/advisory/test_msys_path_conversion_advisor.py +223 -0
- package/hooks/advisory/test_pr_done_reminder.py +22 -0
- package/hooks/advisory/test_refactor_guard.py +40 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +6 -11
- package/hooks/blocking/block_main_commit.py +4 -0
- package/hooks/blocking/code_rules_comments.py +39 -5
- package/hooks/blocking/msys_rev_path_rewriter.py +192 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +3 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +16 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
- package/hooks/blocking/test_msys_rev_path_rewriter.py +141 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
- package/hooks/git-hooks/pull_request_handoff.py +3 -0
- package/hooks/git-hooks/subprocess_window_access.py +41 -0
- package/hooks/git-hooks/test_verification_start.py +4 -0
- package/hooks/git-hooks/verification_notice_context.py +3 -0
- package/hooks/git-hooks/verification_start.py +7 -8
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +9 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +6 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +48 -0
- package/hooks/hooks_constants/msys_rev_path_rewriter_constants.py +37 -0
- package/hooks/hooks_constants/post_tool_use_context.py +34 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +0 -2
- package/hooks/hooks_constants/pre_tool_use_allow_output.py +48 -0
- package/hooks/hooks_constants/subprocess_window.py +141 -0
- package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +1 -2
- package/hooks/hooks_constants/test_post_tool_use_context.py +22 -0
- package/hooks/hooks_constants/test_pre_tool_use_allow_output.py +27 -0
- package/hooks/hooks_constants/test_subprocess_window.py +128 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
- package/hooks/observability/test_failure_recorder.py +3 -1
- package/hooks/validation/mypy_validator.py +4 -0
- package/hooks/validators/README.md +1 -0
- package/hooks/validators/git_checks.py +4 -0
- package/hooks/validators/health_check.py +3 -0
- package/hooks/validators/hook_timing_harness.py +3 -0
- package/hooks/validators/mypy_integration.py +4 -0
- package/hooks/validators/ruff_integration.py +6 -0
- package/hooks/validators/run_all_validators.py +7 -0
- package/hooks/validators/subprocess_window_access.py +41 -0
- package/hooks/workflow/auto_formatter.py +8 -0
- package/hooks/workflow/test_auto_formatter.py +39 -0
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +3 -3
- package/rules/code-standards.md +3 -2
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/destructive-commands.md +19 -19
- package/rules/doc-inventory-integrity.md +4 -4
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +4 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +34 -0
- package/rules/long-horizon-autonomy.md +2 -0
- package/rules/nas-ssh-invocation.md +3 -3
- package/rules/no-cross-skill-duplicate-helpers.md +6 -6
- package/rules/orphan-css-class.md +5 -5
- package/rules/paired-test-coverage.md +6 -6
- package/rules/plain-illustrative-docstrings.md +3 -3
- package/rules/pstack-models.md +4 -3
- package/rules/re-stage-before-commit.md +7 -7
- package/rules/retired-hook-prose.md +16 -0
- package/rules/shell-invocation.md +17 -1
- package/rules/testing.md +3 -5
- package/rules/verify-runtime-state.md +1 -1
- package/rules/windows-filesystem-safe.md +2 -2
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/automatic_advisory/checkout.py +8 -1
- package/scripts/automatic_advisory/cli.py +9 -3
- package/scripts/automatic_advisory/config/constants.py +0 -2
- package/scripts/automatic_advisory/execution.py +7 -1
- package/scripts/automatic_advisory/git.py +9 -1
- package/scripts/automatic_advisory/process_host.py +3 -0
- package/scripts/claude_chain_runner.py +3 -0
- package/scripts/durable_post_lint_config/config/constants.py +0 -3
- package/scripts/durable_post_lint_config/config/test_constants.py +5 -0
- package/scripts/gh_artifact_upload.py +3 -0
- package/scripts/grok_headless_runner.py +3 -0
- package/scripts/grok_patch_artifacts.py +4 -0
- package/scripts/grok_worker_preflight.py +3 -0
- package/scripts/invoke_code_review.py +3 -0
- package/scripts/local_verification/command_runner.py +3 -0
- package/scripts/local_verification/git_state.py +3 -0
- package/scripts/policy_lint/adapter_configuration.py +24 -0
- package/scripts/policy_lint/adapter_detectors.py +48 -2
- package/scripts/policy_lint/adapter_pairing.py +150 -2
- package/scripts/policy_lint/adapter_retired_hook_prose.py +201 -0
- package/scripts/policy_lint/adapters.py +82 -5
- package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/registry.py +6 -1
- package/scripts/policy_lint/selection.py +37 -18
- package/scripts/policy_lint/selection_git.py +17 -2
- package/scripts/repository_checks/config/constants.py +5 -0
- package/scripts/repository_checks/config/policy_document.py +19 -0
- package/scripts/repository_checks/email_exemptions.py +14 -71
- package/scripts/repository_checks/path_exemptions.py +35 -0
- package/scripts/repository_checks/policy_document.py +120 -0
- package/scripts/repository_checks/tracked_secrets.py +27 -4
- package/scripts/setup_project_paths.py +3 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/subprocess_window_access.py +41 -0
- package/scripts/test_dispatcher_profile_import.py +4 -0
- package/scripts/test_durable_post_lint.py +42 -9
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
- package/scripts/tests/test_email_exemptions.py +37 -0
- package/scripts/tests/test_path_exemptions.py +164 -0
- package/scripts/tests/test_policy_document.py +104 -0
- package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +197 -0
- package/scripts/tests/test_policy_lint_selection.py +141 -0
- package/scripts/tests/test_subprocess_window_access.py +118 -0
- package/scripts/tests/test_tracked_secrets.py +79 -0
- package/scripts/validate_instruction_pairs.py +3 -0
- package/scripts/verify_installable_package.py +5 -0
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -8
- package/hooks/hooks_constants/destructive_command_environment_constants.py +0 -18
- package/hooks/hooks_constants/test_text_stripping.py +0 -39
- package/hooks/hooks_constants/text_stripping.py +0 -36
- package/scripts/automatic_advisory/window_flags.py +0 -41
- package/scripts/repository_checks/config/email_exemptions.py +0 -10
|
@@ -22,6 +22,27 @@ from policy_lint.model import (
|
|
|
22
22
|
|
|
23
23
|
_FUNCTION_SOURCE = "def work() -> None:\n pass\n"
|
|
24
24
|
_CHANGED_RULE_SETS = frozenset({"changed"})
|
|
25
|
+
_UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT = (
|
|
26
|
+
"import subprocess\n"
|
|
27
|
+
"PYTHON_FORMAT_TIMEOUT_SECONDS = 12\n"
|
|
28
|
+
"GIT_CHECK_TIMEOUT_SECONDS = 5\n"
|
|
29
|
+
"def worst_case_python_format_seconds() -> int:\n"
|
|
30
|
+
" return PYTHON_FORMAT_TIMEOUT_SECONDS + PYTHON_FORMAT_TIMEOUT_SECONDS\n"
|
|
31
|
+
"def is_untracked_in_git(file_path: str) -> bool:\n"
|
|
32
|
+
" git_check = subprocess.run(['git', 'ls-files', file_path], "
|
|
33
|
+
"timeout=GIT_CHECK_TIMEOUT_SECONDS)\n"
|
|
34
|
+
" return git_check.returncode != 0\n"
|
|
35
|
+
"def run_format(file_path: str) -> None:\n"
|
|
36
|
+
" subprocess.run(['ruff', 'format', file_path], "
|
|
37
|
+
"timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)\n"
|
|
38
|
+
"def main(file_path: str) -> None:\n"
|
|
39
|
+
" if is_untracked_in_git(file_path):\n"
|
|
40
|
+
" return\n"
|
|
41
|
+
" run_format(file_path)\n"
|
|
42
|
+
)
|
|
43
|
+
_GENERATED_CHANGELOG_TEXT = (
|
|
44
|
+
"## 7.5.0\n\n* **rules:** correct hook claims for gates that " + "no" + " longer run\n"
|
|
45
|
+
)
|
|
25
46
|
|
|
26
47
|
|
|
27
48
|
def _rule_named(rule_id: str) -> DocumentRule:
|
|
@@ -266,6 +287,51 @@ def test_hook_format_accepts_claude_settings() -> None:
|
|
|
266
287
|
assert configuration_rule.accepts(settings_document) is False
|
|
267
288
|
|
|
268
289
|
|
|
290
|
+
def test_prose_rules_skip_a_generated_changelog() -> None:
|
|
291
|
+
changelog_document = Document.from_text(
|
|
292
|
+
"packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT
|
|
293
|
+
)
|
|
294
|
+
handwritten_document = Document.from_text("docs/notes.md", _GENERATED_CHANGELOG_TEXT)
|
|
295
|
+
stored_prompt_changelog = Document.from_text("rules/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT)
|
|
296
|
+
stored_prompt_document = Document.from_text("rules/notes.md", _GENERATED_CHANGELOG_TEXT)
|
|
297
|
+
state_rule = _rule_named("state-description")
|
|
298
|
+
plain_language_rule = _rule_named("plain-language")
|
|
299
|
+
assert state_rule.accepts(changelog_document) is False
|
|
300
|
+
assert state_rule.accepts(handwritten_document) is True
|
|
301
|
+
assert plain_language_rule.accepts(stored_prompt_changelog) is False
|
|
302
|
+
assert plain_language_rule.accepts(stored_prompt_document) is True
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_lint_reports_the_changelog_phrase_only_in_the_handwritten_document(
|
|
306
|
+
tmp_path: Path,
|
|
307
|
+
) -> None:
|
|
308
|
+
_ensure_git_repository(tmp_path)
|
|
309
|
+
document_set = DocumentSet(
|
|
310
|
+
(
|
|
311
|
+
Document.from_text("packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT),
|
|
312
|
+
Document.from_text("docs/notes.md", _GENERATED_CHANGELOG_TEXT),
|
|
313
|
+
),
|
|
314
|
+
SelectionKind.TEXT,
|
|
315
|
+
tmp_path,
|
|
316
|
+
)
|
|
317
|
+
lint_report = lint(
|
|
318
|
+
LintRequest(tmp_path, document_set), all_registry=(_rule_named("state-description"),)
|
|
319
|
+
)
|
|
320
|
+
all_reported_paths = {
|
|
321
|
+
each_diagnostic.location.path.as_posix()
|
|
322
|
+
for each_diagnostic in lint_report.diagnostics
|
|
323
|
+
if each_diagnostic.location is not None
|
|
324
|
+
}
|
|
325
|
+
assert all_reported_paths == {"docs/notes.md"}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_non_prose_markdown_rules_still_run_on_a_generated_changelog() -> None:
|
|
329
|
+
changelog_document = Document.from_text(
|
|
330
|
+
"packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT
|
|
331
|
+
)
|
|
332
|
+
assert _rule_named("docstring-gate-count").accepts(changelog_document) is True
|
|
333
|
+
|
|
334
|
+
|
|
269
335
|
def test_workflow_substitution_accepts_workflow_js_suffix() -> None:
|
|
270
336
|
workflow_document = Document.from_text("packages/foo/bar.workflow.js", "loop")
|
|
271
337
|
directory_document = Document.from_text("docs/workflow/readme.md", "loop")
|
|
@@ -581,3 +647,53 @@ def test_lint_keeps_skipped_rules_out_of_executed_rules(tmp_path: Path) -> None:
|
|
|
581
647
|
all_rules_report = lint(request, all_registry=(skipped_rule,))
|
|
582
648
|
assert all_rules_report.executed_rules == ()
|
|
583
649
|
assert all_rules_report.skipped_rules == ("skipped",)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def test_subprocess_budget_skips_a_test_module_and_keeps_production() -> None:
|
|
653
|
+
subprocess_budget_rule = _rule_named("subprocess-budget")
|
|
654
|
+
prefixed_test_document = Document.from_text(
|
|
655
|
+
"packages/claude-dev-env/hooks/workflow/test_auto_formatter.py",
|
|
656
|
+
_UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT,
|
|
657
|
+
)
|
|
658
|
+
repository_root_test_document = Document.from_text(
|
|
659
|
+
"tests/timing.py", _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT
|
|
660
|
+
)
|
|
661
|
+
production_document = Document.from_text(
|
|
662
|
+
"src/timing.py", _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT
|
|
663
|
+
)
|
|
664
|
+
assert subprocess_budget_rule.accepts(prefixed_test_document) is False
|
|
665
|
+
assert subprocess_budget_rule.accepts(repository_root_test_document) is False
|
|
666
|
+
assert subprocess_budget_rule.accepts(production_document) is True
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _subprocess_budget_rule_identifiers(
|
|
670
|
+
repository_root: Path, document_path: str
|
|
671
|
+
) -> tuple[str, ...]:
|
|
672
|
+
document_set = DocumentSet(
|
|
673
|
+
(Document.from_text(document_path, _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT),),
|
|
674
|
+
SelectionKind.TEXT,
|
|
675
|
+
repository_root,
|
|
676
|
+
)
|
|
677
|
+
lint_report = lint(
|
|
678
|
+
LintRequest(repository_root, document_set),
|
|
679
|
+
all_registry=(_rule_named("subprocess-budget"),),
|
|
680
|
+
)
|
|
681
|
+
return tuple(
|
|
682
|
+
each_diagnostic.rule_id for each_diagnostic in lint_report.diagnostics
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def test_lint_reports_an_undercounted_budget_only_in_the_production_module(
|
|
687
|
+
tmp_path: Path,
|
|
688
|
+
) -> None:
|
|
689
|
+
_ensure_git_repository(tmp_path)
|
|
690
|
+
assert _subprocess_budget_rule_identifiers(tmp_path, "src/timing.py") == (
|
|
691
|
+
"subprocess-budget",
|
|
692
|
+
)
|
|
693
|
+
assert (
|
|
694
|
+
_subprocess_budget_rule_identifiers(
|
|
695
|
+
tmp_path, "packages/claude-dev-env/hooks/workflow/test_auto_formatter.py"
|
|
696
|
+
)
|
|
697
|
+
== ()
|
|
698
|
+
)
|
|
699
|
+
assert _subprocess_budget_rule_identifiers(tmp_path, "tests/timing.py") == ()
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Tests for the retired-hook-prose rule over rules Markdown."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
|
+
|
|
7
|
+
from policy_lint import adapter_retired_hook_prose, registry
|
|
8
|
+
from policy_lint.model import ContentOrigin, Document, DocumentRule
|
|
9
|
+
|
|
10
|
+
_RULE_ID = "retired-hook-prose"
|
|
11
|
+
_RULES_DOCUMENT_PATH = "packages/claude-dev-env/rules/example.md"
|
|
12
|
+
_INSTALLER_SOURCE = """
|
|
13
|
+
export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
|
|
14
|
+
'blocking/destructive_command_blocker.py',
|
|
15
|
+
]);
|
|
16
|
+
"""
|
|
17
|
+
_HOOKS_CONFIGURATION = """
|
|
18
|
+
{
|
|
19
|
+
"hooks": {
|
|
20
|
+
"PostToolUse": [
|
|
21
|
+
{"hooks": [{"command": "python hooks/observability/session_file_edit_tracker.py"}]}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
"""
|
|
26
|
+
_DISPATCHER_ROSTER_SOURCE = """
|
|
27
|
+
ALL_HOSTED_HOOK_ENTRIES = (
|
|
28
|
+
HostedHookEntry("advisory/refactor_guard.py", is_blocking=False),
|
|
29
|
+
)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_package(repository_root: Path) -> None:
|
|
34
|
+
package_root = repository_root / "packages" / "claude-dev-env"
|
|
35
|
+
hooks_root = package_root / "hooks"
|
|
36
|
+
(hooks_root / "blocking").mkdir(parents=True)
|
|
37
|
+
(hooks_root / "advisory").mkdir(parents=True)
|
|
38
|
+
(hooks_root / "observability").mkdir(parents=True)
|
|
39
|
+
(hooks_root / "hooks_constants").mkdir(parents=True)
|
|
40
|
+
(package_root / "bin").mkdir(parents=True)
|
|
41
|
+
(package_root / "rules").mkdir(parents=True)
|
|
42
|
+
(hooks_root / "blocking" / "windows_rmtree_blocker.py").write_text("", "utf-8")
|
|
43
|
+
(hooks_root / "advisory" / "refactor_guard.py").write_text("", "utf-8")
|
|
44
|
+
(hooks_root / "observability" / "session_file_edit_tracker.py").write_text(
|
|
45
|
+
"", "utf-8"
|
|
46
|
+
)
|
|
47
|
+
(hooks_root / "blocking" / "content_hash_store.py").write_text("", "utf-8")
|
|
48
|
+
(hooks_root / "hooks.json").write_text(_HOOKS_CONFIGURATION, "utf-8")
|
|
49
|
+
(
|
|
50
|
+
hooks_root / "hooks_constants" / "pre_tool_use_dispatcher_constants.py"
|
|
51
|
+
).write_text(_DISPATCHER_ROSTER_SOURCE, "utf-8")
|
|
52
|
+
(package_root / "bin" / "install.mjs").write_text(_INSTALLER_SOURCE, "utf-8")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _diagnostics_for(repository_root: Path, prose: str) -> tuple[str, ...]:
|
|
56
|
+
document = Document(
|
|
57
|
+
PurePosixPath(_RULES_DOCUMENT_PATH),
|
|
58
|
+
prose,
|
|
59
|
+
None,
|
|
60
|
+
None,
|
|
61
|
+
ContentOrigin.WORKTREE,
|
|
62
|
+
)
|
|
63
|
+
all_diagnostics = adapter_retired_hook_prose.retired_hook_prose_diagnostics(
|
|
64
|
+
document, repository_root
|
|
65
|
+
)
|
|
66
|
+
return tuple(each_diagnostic.message for each_diagnostic in all_diagnostics)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_a_live_claim_for_a_surviving_unregistered_hook_is_reported(
|
|
70
|
+
tmp_path: Path,
|
|
71
|
+
) -> None:
|
|
72
|
+
_build_package(tmp_path)
|
|
73
|
+
all_messages = _diagnostics_for(
|
|
74
|
+
tmp_path,
|
|
75
|
+
"The `windows_rmtree_blocker.py` PreToolUse hook blocks the unsafe pattern.\n",
|
|
76
|
+
)
|
|
77
|
+
assert len(all_messages) == 1
|
|
78
|
+
assert "windows_rmtree_blocker" in all_messages[0]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_a_live_claim_for_a_roster_hook_absent_from_disk_is_reported(
|
|
82
|
+
tmp_path: Path,
|
|
83
|
+
) -> None:
|
|
84
|
+
_build_package(tmp_path)
|
|
85
|
+
all_messages = _diagnostics_for(
|
|
86
|
+
tmp_path,
|
|
87
|
+
"The `destructive_command_blocker` hook watches every Bash-tool command.\n",
|
|
88
|
+
)
|
|
89
|
+
assert len(all_messages) == 1
|
|
90
|
+
assert "destructive_command_blocker" in all_messages[0]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_a_registered_hook_described_in_the_present_tense_passes(
|
|
94
|
+
tmp_path: Path,
|
|
95
|
+
) -> None:
|
|
96
|
+
_build_package(tmp_path)
|
|
97
|
+
all_messages = _diagnostics_for(
|
|
98
|
+
tmp_path,
|
|
99
|
+
"`session_file_edit_tracker` records each Write path, and `refactor_guard` reports a risk.\n",
|
|
100
|
+
)
|
|
101
|
+
assert all_messages == ()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_a_past_tense_retirement_sentence_passes(tmp_path: Path) -> None:
|
|
105
|
+
_build_package(tmp_path)
|
|
106
|
+
all_messages = _diagnostics_for(
|
|
107
|
+
tmp_path,
|
|
108
|
+
"Commit `0f21faf8e` retired the blocking hooks. "
|
|
109
|
+
"`destructive_command_blocker` was one of them.\n",
|
|
110
|
+
)
|
|
111
|
+
assert all_messages == ()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_a_sentence_crediting_the_staged_policy_lint_passes(tmp_path: Path) -> None:
|
|
115
|
+
_build_package(tmp_path)
|
|
116
|
+
all_messages = _diagnostics_for(
|
|
117
|
+
tmp_path,
|
|
118
|
+
"`windows_rmtree_blocker.py` reports the unsafe pattern, "
|
|
119
|
+
"and the staged policy lint runs it over each changed file.\n",
|
|
120
|
+
)
|
|
121
|
+
assert all_messages == ()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_a_hook_directory_support_module_passes(tmp_path: Path) -> None:
|
|
125
|
+
_build_package(tmp_path)
|
|
126
|
+
all_messages = _diagnostics_for(
|
|
127
|
+
tmp_path,
|
|
128
|
+
"`content_hash_store.py` holds the full contract in its module docstring.\n",
|
|
129
|
+
)
|
|
130
|
+
assert all_messages == ()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_a_verb_before_the_hook_name_passes(tmp_path: Path) -> None:
|
|
134
|
+
_build_package(tmp_path)
|
|
135
|
+
all_messages = _diagnostics_for(
|
|
136
|
+
tmp_path,
|
|
137
|
+
"The staged lint runs `windows_rmtree_blocker.py` over each changed file.\n",
|
|
138
|
+
)
|
|
139
|
+
assert all_messages == ()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_a_code_span_between_the_name_and_its_verb_does_not_hide_the_claim(
|
|
143
|
+
tmp_path: Path,
|
|
144
|
+
) -> None:
|
|
145
|
+
_build_package(tmp_path)
|
|
146
|
+
all_messages = _diagnostics_for(
|
|
147
|
+
tmp_path,
|
|
148
|
+
"`destructive_command_blocker` (PreToolUse on Bash `git commit`) "
|
|
149
|
+
"denies a commit that drops session edits.",
|
|
150
|
+
)
|
|
151
|
+
assert len(all_messages) == 1
|
|
152
|
+
assert "destructive_command_blocker" in all_messages[0]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_a_second_hook_name_bounds_the_first_claim(tmp_path: Path) -> None:
|
|
156
|
+
_build_package(tmp_path)
|
|
157
|
+
all_messages = _diagnostics_for(
|
|
158
|
+
tmp_path,
|
|
159
|
+
"`destructive_command_blocker` was retired, and `refactor_guard` reports a risk.",
|
|
160
|
+
)
|
|
161
|
+
assert all_messages == ()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_the_rule_is_registered_for_changed_and_repository_selections() -> None:
|
|
165
|
+
all_matching_rules = tuple(
|
|
166
|
+
each_rule
|
|
167
|
+
for each_rule in registry.default_registry()
|
|
168
|
+
if each_rule.rule_id == _RULE_ID
|
|
169
|
+
)
|
|
170
|
+
assert len(all_matching_rules) == 1
|
|
171
|
+
registered_rule = all_matching_rules[0]
|
|
172
|
+
assert isinstance(registered_rule, DocumentRule)
|
|
173
|
+
assert registered_rule.rule_sets == frozenset({"changed", "repository"})
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_the_committed_rules_directory_names_no_gate_that_stopped_running() -> None:
|
|
177
|
+
repository_root = Path(__file__).resolve().parents[4]
|
|
178
|
+
rules_root = repository_root / "packages" / "claude-dev-env" / "rules"
|
|
179
|
+
all_messages: list[str] = []
|
|
180
|
+
for each_path in sorted(rules_root.glob("*.md")):
|
|
181
|
+
relative_path = each_path.relative_to(repository_root).as_posix()
|
|
182
|
+
document = Document(
|
|
183
|
+
PurePosixPath(relative_path),
|
|
184
|
+
each_path.read_text(encoding="utf-8"),
|
|
185
|
+
None,
|
|
186
|
+
None,
|
|
187
|
+
ContentOrigin.WORKTREE,
|
|
188
|
+
)
|
|
189
|
+
all_messages.extend(
|
|
190
|
+
f"{relative_path}:{each_diagnostic.location.start_line}: {each_diagnostic.message}"
|
|
191
|
+
for each_diagnostic in (
|
|
192
|
+
adapter_retired_hook_prose.retired_hook_prose_diagnostics(
|
|
193
|
+
document, repository_root
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
assert all_messages == []
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import subprocess
|
|
7
|
+
import sys
|
|
7
8
|
from pathlib import Path, PurePosixPath
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
@@ -19,6 +20,13 @@ from policy_lint.model import (
|
|
|
19
20
|
)
|
|
20
21
|
from policy_lint.selection import SelectionRunFatal, select_documents
|
|
21
22
|
|
|
23
|
+
WINDOWS_NO_WINDOW_FLAG = 0x08000000
|
|
24
|
+
EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
|
|
25
|
+
|
|
26
|
+
NON_UTF8_PNG_BYTES = bytes(
|
|
27
|
+
(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0xFF, 0xFE, 0xFF)
|
|
28
|
+
)
|
|
29
|
+
|
|
22
30
|
|
|
23
31
|
def _git_environment() -> dict[str, str]:
|
|
24
32
|
return {
|
|
@@ -66,6 +74,22 @@ def _commit_text(repository_root: Path, relative_path: str, text: str) -> None:
|
|
|
66
74
|
_run_git(repository_root, "commit", "--quiet", "-m", "selection fixture")
|
|
67
75
|
|
|
68
76
|
|
|
77
|
+
def _write_bytes(repository_root: Path, relative_path: str, raw_bytes: bytes) -> None:
|
|
78
|
+
file_path = repository_root / relative_path
|
|
79
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
file_path.write_bytes(raw_bytes)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _commit_bytes(repository_root: Path, relative_path: str, raw_bytes: bytes) -> None:
|
|
84
|
+
_write_bytes(repository_root, relative_path, raw_bytes)
|
|
85
|
+
_run_git(repository_root, "add", "--", relative_path)
|
|
86
|
+
_run_git(repository_root, "commit", "--quiet", "-m", "selection binary fixture")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _all_selected_paths(document_set: DocumentSet) -> set[str]:
|
|
90
|
+
return {each_document.path.as_posix() for each_document in document_set.documents}
|
|
91
|
+
|
|
92
|
+
|
|
69
93
|
def _document_for(document_set: DocumentSet, relative_path: str) -> Document:
|
|
70
94
|
for each_document in document_set.documents:
|
|
71
95
|
if each_document.path.as_posix() == relative_path:
|
|
@@ -394,3 +418,120 @@ def test_tracked_worktree_document_preserves_git_path_spelling(
|
|
|
394
418
|
tmp_path, "MiXeD.py"
|
|
395
419
|
)
|
|
396
420
|
assert selected_document.path == PurePosixPath("MiXeD.py")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_base_added_binary_file_should_be_left_out_of_the_selection(
|
|
424
|
+
tmp_path: Path,
|
|
425
|
+
) -> None:
|
|
426
|
+
repository_root = _initialize_repository(tmp_path)
|
|
427
|
+
_commit_text(repository_root, "file.py", "base-text\n")
|
|
428
|
+
_run_git(repository_root, "checkout", "--quiet", "-b", "feature")
|
|
429
|
+
_commit_text(repository_root, "file.py", "feature-text\n")
|
|
430
|
+
_commit_bytes(repository_root, "approved_design_overlay.png", NON_UTF8_PNG_BYTES)
|
|
431
|
+
|
|
432
|
+
document_set = select_documents(LintRequest.base(repository_root, "main"))
|
|
433
|
+
|
|
434
|
+
assert _all_selected_paths(document_set) == {"file.py"}
|
|
435
|
+
assert _document_for(document_set, "file.py").text == "feature-text\n"
|
|
436
|
+
assert document_set.deleted_paths == ()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_base_modified_binary_file_should_be_left_out_of_the_selection(
|
|
440
|
+
tmp_path: Path,
|
|
441
|
+
) -> None:
|
|
442
|
+
repository_root = _initialize_repository(tmp_path)
|
|
443
|
+
_commit_bytes(repository_root, "approved_design_overlay.png", NON_UTF8_PNG_BYTES)
|
|
444
|
+
_run_git(repository_root, "checkout", "--quiet", "-b", "feature")
|
|
445
|
+
_commit_bytes(
|
|
446
|
+
repository_root,
|
|
447
|
+
"approved_design_overlay.png",
|
|
448
|
+
NON_UTF8_PNG_BYTES + bytes((0xFF, 0x00, 0xFE)),
|
|
449
|
+
)
|
|
450
|
+
_commit_text(repository_root, "file.py", "feature-text\n")
|
|
451
|
+
|
|
452
|
+
document_set = select_documents(LintRequest.base(repository_root, "main"))
|
|
453
|
+
|
|
454
|
+
assert _all_selected_paths(document_set) == {"file.py"}
|
|
455
|
+
assert _document_for(document_set, "file.py").text == "feature-text\n"
|
|
456
|
+
assert document_set.deleted_paths == ()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_staged_binary_file_should_be_left_out_of_the_selection(tmp_path: Path) -> None:
|
|
460
|
+
repository_root = _initialize_repository(tmp_path)
|
|
461
|
+
_commit_text(repository_root, "file.py", "committed\n")
|
|
462
|
+
_write_bytes(repository_root, "approved_design_overlay.png", NON_UTF8_PNG_BYTES)
|
|
463
|
+
_write_text(repository_root, "file.py", "staged\n")
|
|
464
|
+
_run_git(repository_root, "add", "--all", "--", ".")
|
|
465
|
+
|
|
466
|
+
document_set = select_documents(LintRequest.staged(repository_root))
|
|
467
|
+
|
|
468
|
+
assert _all_selected_paths(document_set) == {"file.py"}
|
|
469
|
+
assert _document_for(document_set, "file.py").text == "staged\n"
|
|
470
|
+
assert document_set.deleted_paths == ()
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_repository_source_should_leave_binary_tracked_files_out(
|
|
474
|
+
tmp_path: Path,
|
|
475
|
+
) -> None:
|
|
476
|
+
repository_root = _initialize_repository(tmp_path)
|
|
477
|
+
_commit_text(repository_root, "tracked.py", "tracked\n")
|
|
478
|
+
_commit_bytes(repository_root, "approved_design_overlay.png", NON_UTF8_PNG_BYTES)
|
|
479
|
+
|
|
480
|
+
document_set = select_documents(LintRequest.repository(repository_root))
|
|
481
|
+
|
|
482
|
+
assert _all_selected_paths(document_set) == {"tracked.py"}
|
|
483
|
+
assert _document_for(document_set, "tracked.py").text == "tracked\n"
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_files_source_should_leave_a_named_binary_path_out(tmp_path: Path) -> None:
|
|
487
|
+
repository_root = _initialize_repository(tmp_path)
|
|
488
|
+
_commit_text(repository_root, "file.py", "committed\n")
|
|
489
|
+
_write_bytes(repository_root, "approved_design_overlay.png", NON_UTF8_PNG_BYTES)
|
|
490
|
+
|
|
491
|
+
document_set = select_documents(
|
|
492
|
+
LintRequest.files(
|
|
493
|
+
repository_root,
|
|
494
|
+
[Path("file.py"), Path("approved_design_overlay.png")],
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
assert _all_selected_paths(document_set) == {"file.py"}
|
|
499
|
+
assert _document_for(document_set, "file.py").text == "committed\n"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def test_files_missing_path_should_still_raise(tmp_path: Path) -> None:
|
|
503
|
+
repository_root = _initialize_repository(tmp_path)
|
|
504
|
+
_commit_text(repository_root, "file.py", "committed\n")
|
|
505
|
+
|
|
506
|
+
with pytest.raises(SelectionRunFatal, match="does not exist"):
|
|
507
|
+
select_documents(LintRequest.files(repository_root, [Path("missing.py")]))
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _record_git_creation_flags(monkeypatch: pytest.MonkeyPatch) -> dict[str, object]:
|
|
511
|
+
all_recorded_keyword_arguments: dict[str, object] = {}
|
|
512
|
+
|
|
513
|
+
def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
|
514
|
+
del args
|
|
515
|
+
all_recorded_keyword_arguments.update(kwargs)
|
|
516
|
+
return subprocess.CompletedProcess([], 0, stdout=b"abc123\n", stderr=b"")
|
|
517
|
+
|
|
518
|
+
monkeypatch.setattr(selection_git.subprocess, "run", _record)
|
|
519
|
+
return all_recorded_keyword_arguments
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_git_bytes_for_should_start_without_a_console_window(
|
|
523
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
524
|
+
) -> None:
|
|
525
|
+
all_recorded_keyword_arguments = _record_git_creation_flags(monkeypatch)
|
|
526
|
+
|
|
527
|
+
assert selection_git.git_bytes_for(tmp_path, ("status",)) == b"abc123\n"
|
|
528
|
+
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_head_revision_should_start_without_a_console_window(
|
|
532
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
533
|
+
) -> None:
|
|
534
|
+
all_recorded_keyword_arguments = _record_git_creation_flags(monkeypatch)
|
|
535
|
+
|
|
536
|
+
assert selection_git.head_revision(tmp_path) == "abc123"
|
|
537
|
+
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Prove every deployed subprocess-window shim reaches the packaged helpers.
|
|
2
|
+
|
|
3
|
+
Each shim sits beside the scripts of one deployed directory. A deployed
|
|
4
|
+
profile carries no installed Python distribution, and the repository runs its
|
|
5
|
+
scripts under ``python -S -E``, which hides installed packages from the child.
|
|
6
|
+
So each check below drives a real ``-S -E`` child, imports the shim by the name
|
|
7
|
+
its callers use, and reads back where the helper came from.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
_PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
20
|
+
_SHIM_MODULE_NAME = "subprocess_window_access"
|
|
21
|
+
_SHIM_FILE_NAME = f"{_SHIM_MODULE_NAME}.py"
|
|
22
|
+
_PACKAGED_HELPER_PATH = (
|
|
23
|
+
_PACKAGE_ROOT / "hooks" / "hooks_constants" / "subprocess_window.py"
|
|
24
|
+
)
|
|
25
|
+
_ALL_EXPORTED_HELPER_NAMES = (
|
|
26
|
+
"HiddenWindowStartupInfo",
|
|
27
|
+
"detached_hidden_window_creation_flags",
|
|
28
|
+
"hidden_window_creation_flags",
|
|
29
|
+
"inherited_stream_startup_info",
|
|
30
|
+
)
|
|
31
|
+
_ALL_SHIM_DIRECTORY_PATHS = (
|
|
32
|
+
"scripts",
|
|
33
|
+
"_shared/pr-loop/scripts",
|
|
34
|
+
"_shared/process-tree/scripts",
|
|
35
|
+
"hooks/git-hooks",
|
|
36
|
+
"hooks/validators",
|
|
37
|
+
".agents/skills/fresh-branch/scripts",
|
|
38
|
+
".agents/skills/syncing-submodules/scripts",
|
|
39
|
+
".agents/skills/test-runner/scripts",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_IMPORT_PROBE_SOURCE = "\n".join(
|
|
43
|
+
(
|
|
44
|
+
"import json",
|
|
45
|
+
"import sys",
|
|
46
|
+
"shim_directory = sys.argv[1]",
|
|
47
|
+
"sys.path.insert(0, shim_directory)",
|
|
48
|
+
"head_before_import = sys.path[0]",
|
|
49
|
+
f"import {_SHIM_MODULE_NAME} as shim",
|
|
50
|
+
"print(json.dumps({",
|
|
51
|
+
" 'helper_path': sys.modules[",
|
|
52
|
+
" shim.hidden_window_creation_flags.__module__",
|
|
53
|
+
" ].__file__,",
|
|
54
|
+
" 'head_after_import': sys.path[0],",
|
|
55
|
+
" 'head_before_import': head_before_import,",
|
|
56
|
+
" 'exported_names': sorted(shim.__all__),",
|
|
57
|
+
"}))",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _import_probe_results(shim_directory: Path) -> dict[str, object]:
|
|
63
|
+
"""Import one shim in a ``-S -E`` child and read back what it resolved."""
|
|
64
|
+
completed_process = subprocess.run(
|
|
65
|
+
[
|
|
66
|
+
sys.executable,
|
|
67
|
+
"-S",
|
|
68
|
+
"-E",
|
|
69
|
+
"-c",
|
|
70
|
+
_IMPORT_PROBE_SOURCE,
|
|
71
|
+
str(shim_directory),
|
|
72
|
+
],
|
|
73
|
+
cwd=str(_PACKAGE_ROOT),
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
assert completed_process.returncode == 0, completed_process.stderr
|
|
79
|
+
return json.loads(completed_process.stdout)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
83
|
+
def test_every_deployed_directory_carries_a_shim(shim_directory_path: str) -> None:
|
|
84
|
+
"""Each directory whose scripts import the shim also ships one."""
|
|
85
|
+
assert (_PACKAGE_ROOT / shim_directory_path / _SHIM_FILE_NAME).is_file()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
89
|
+
def test_shim_resolves_the_packaged_helper_without_an_installed_package(
|
|
90
|
+
shim_directory_path: str,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""A ``-S -E`` child reaches the packaged helper through the shim alone."""
|
|
93
|
+
all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
|
|
94
|
+
|
|
95
|
+
assert Path(str(all_probe_results["helper_path"])) == _PACKAGED_HELPER_PATH
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
99
|
+
def test_shim_leaves_the_head_of_the_search_path_alone(
|
|
100
|
+
shim_directory_path: str,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""A caller that prepended its own roots keeps them ahead of the hooks tree."""
|
|
103
|
+
all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
|
|
104
|
+
|
|
105
|
+
assert (
|
|
106
|
+
all_probe_results["head_after_import"]
|
|
107
|
+
== (all_probe_results["head_before_import"])
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
112
|
+
def test_shim_re_exports_every_hidden_window_helper(
|
|
113
|
+
shim_directory_path: str,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Every caller finds the name it needs on the shim it imports."""
|
|
116
|
+
all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
|
|
117
|
+
|
|
118
|
+
assert all_probe_results["exported_names"] == sorted(_ALL_EXPORTED_HELPER_NAMES)
|