claude-dev-env 8.6.1 → 8.7.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/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +10 -8
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_state_description_blocker.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapters.py +30 -0
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Behavior of the contrast-framing detector and its rule document."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from contrast_framing import (
|
|
9
|
+
describe_contrast_framing,
|
|
10
|
+
find_contrast_framing,
|
|
11
|
+
form_named,
|
|
12
|
+
)
|
|
13
|
+
from dev_env_scripts_constants.contrast_framing_constants import (
|
|
14
|
+
ALL_CONTRAST_FRAMING_FORMS,
|
|
15
|
+
CONTRAST_FRAMING_RULE_DOCUMENT,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
PACKAGE_ROOT: Path = Path(__file__).resolve().parents[2]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _form_names(document_text: str) -> list[str]:
|
|
22
|
+
return [each_hit[2] for each_hit in find_contrast_framing(document_text)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _message_for(document_text: str) -> str:
|
|
26
|
+
line_number, _column, form_name = find_contrast_framing(document_text)[0]
|
|
27
|
+
source_line = document_text.splitlines()[line_number - 1]
|
|
28
|
+
return describe_contrast_framing(source_line, form_name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_trailing_comma_not_reports() -> None:
|
|
32
|
+
assert _form_names("A diff regression, not shared infrastructure.\n") == [
|
|
33
|
+
"trailing-comma-not"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_corrective_it_is_not_reports() -> None:
|
|
38
|
+
assert "corrective-it-is-not" in _form_names("It's not a flake, it's a bug.\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_comparative_ranking_reports() -> None:
|
|
42
|
+
document_text = "Being precise about them matters more than trying to cover them.\n"
|
|
43
|
+
assert "comparative-ranking" in _form_names(document_text)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_substitution_and_additive_forms_report() -> None:
|
|
47
|
+
document_text = "Park it rather than fix it, and not just for today.\n"
|
|
48
|
+
assert set(_form_names(document_text)) >= {
|
|
49
|
+
"substitution-rather-than",
|
|
50
|
+
"additive-not-just",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_plain_quantity_comparison_stays_quiet() -> None:
|
|
55
|
+
assert _form_names("The function runs more than 30 lines.\n") == []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_statement_of_what_is_stays_quiet() -> None:
|
|
59
|
+
document_text = (
|
|
60
|
+
"The check reports the failing line and names the fix.\n"
|
|
61
|
+
"Push the branch and read the verdict.\n"
|
|
62
|
+
)
|
|
63
|
+
assert _form_names(document_text) == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_code_span_and_fence_contents_stay_quiet() -> None:
|
|
67
|
+
document_text = (
|
|
68
|
+
"Call `parse(value, not_found)` for the missing case.\n"
|
|
69
|
+
"```\n"
|
|
70
|
+
"returns a value, not a default\n"
|
|
71
|
+
"```\n"
|
|
72
|
+
)
|
|
73
|
+
assert _form_names(document_text) == []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_reported_line_and_column_locate_the_match() -> None:
|
|
77
|
+
document_text = "First line.\nOne answer, not two.\n"
|
|
78
|
+
assert find_contrast_framing(document_text) == [(2, 11, "trailing-comma-not")]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_message_names_the_form_the_text_and_the_fix() -> None:
|
|
82
|
+
message = _message_for("One answer, not two.\n")
|
|
83
|
+
assert "trailing-comma-not" in message
|
|
84
|
+
assert "state what is true" in message.lower()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_form_named_rejects_an_unknown_name() -> None:
|
|
88
|
+
with pytest.raises(KeyError):
|
|
89
|
+
form_named("no-such-form")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_every_form_carries_a_row_in_the_rule_document() -> None:
|
|
93
|
+
rule_text = (PACKAGE_ROOT / CONTRAST_FRAMING_RULE_DOCUMENT).read_text(
|
|
94
|
+
encoding="utf-8"
|
|
95
|
+
)
|
|
96
|
+
for each_form in ALL_CONTRAST_FRAMING_FORMS:
|
|
97
|
+
assert f"`{each_form.name}`" in rule_text
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_a_list_of_not_items_stays_quiet() -> None:
|
|
101
|
+
document_text = "Not in chat, not in a commit message, not in a body.\n"
|
|
102
|
+
assert _form_names(document_text) == []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_first_of_several_trailing_not_clauses_reports() -> None:
|
|
106
|
+
document_text = "A diff regression, not shared code, not a workaround.\n"
|
|
107
|
+
assert _form_names(document_text) == ["trailing-comma-not"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_a_quoted_line_stays_quiet() -> None:
|
|
111
|
+
assert _form_names("> a bug, not a flake\n") == []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_message_truncates_a_quoted_line_past_the_limit() -> None:
|
|
115
|
+
message = _message_for(("x" * 200) + ", not the shorter reading.\n")
|
|
116
|
+
assert '"' + ("x" * 120) + '"' in message
|
|
117
|
+
assert "x" * 121 not in message
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Behavior tests for recording advisory findings in the follow-up ledger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
9
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
10
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
12
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
14
|
+
|
|
15
|
+
from repository_checks.config.constants import (
|
|
16
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
17
|
+
CHECK_ID_PACKAGE_INVENTORY,
|
|
18
|
+
FOLLOWUP_LEDGER_MODULE_NAME,
|
|
19
|
+
SEVERITY_SMELL,
|
|
20
|
+
)
|
|
21
|
+
from repository_checks.followups import record_smell_findings
|
|
22
|
+
from repository_checks.hook_modules import load_hooks_module
|
|
23
|
+
from repository_checks.models import RepositoryCheckReport, RepositoryFinding
|
|
24
|
+
from repository_policy_test_support import initialize_repository
|
|
25
|
+
|
|
26
|
+
_INVENTORY_PATH = "pipeline/check_dialer_seam_cli.py"
|
|
27
|
+
_INVENTORY_MESSAGE = "production file is absent from package inventory"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_should_record_an_advisory_finding_with_its_message(tmp_path: Path) -> None:
|
|
31
|
+
repository_root = tmp_path / "repo"
|
|
32
|
+
initialize_repository(repository_root)
|
|
33
|
+
record_smell_findings(repository_root, _inventory_report())
|
|
34
|
+
assert _recorded_rows(repository_root) == [
|
|
35
|
+
(CHECK_ID_PACKAGE_INVENTORY, _INVENTORY_PATH, _INVENTORY_MESSAGE, SEVERITY_SMELL)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_record_one_row_for_a_finding_seen_twice(tmp_path: Path) -> None:
|
|
40
|
+
repository_root = tmp_path / "repo"
|
|
41
|
+
initialize_repository(repository_root)
|
|
42
|
+
report = _inventory_report()
|
|
43
|
+
record_smell_findings(repository_root, report)
|
|
44
|
+
record_smell_findings(repository_root, report)
|
|
45
|
+
assert len(_recorded_rows(repository_root)) == 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_should_leave_a_breaking_finding_out_of_the_ledger(tmp_path: Path) -> None:
|
|
49
|
+
repository_root = tmp_path / "repo"
|
|
50
|
+
initialize_repository(repository_root)
|
|
51
|
+
record_smell_findings(
|
|
52
|
+
repository_root,
|
|
53
|
+
RepositoryCheckReport(
|
|
54
|
+
(
|
|
55
|
+
RepositoryFinding(
|
|
56
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
57
|
+
"notes/CLAUDE.md",
|
|
58
|
+
"references missing file absent_helper.py",
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
(),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
assert _recorded_rows(repository_root) == []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _inventory_report() -> RepositoryCheckReport:
|
|
68
|
+
return RepositoryCheckReport(
|
|
69
|
+
(
|
|
70
|
+
RepositoryFinding(
|
|
71
|
+
CHECK_ID_PACKAGE_INVENTORY, _INVENTORY_PATH, _INVENTORY_MESSAGE
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
(),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _recorded_rows(repository_root: Path) -> list[tuple[str, str, str, str]]:
|
|
79
|
+
ledger = load_hooks_module(FOLLOWUP_LEDGER_MODULE_NAME)
|
|
80
|
+
return [
|
|
81
|
+
(
|
|
82
|
+
each_finding.check_id,
|
|
83
|
+
each_finding.file_path,
|
|
84
|
+
each_finding.message,
|
|
85
|
+
each_finding.severity,
|
|
86
|
+
)
|
|
87
|
+
for each_finding in ledger.all_recorded_findings(repository_root)
|
|
88
|
+
]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""An owned match clears while every other scanner finding stays reported."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from repository_checks.config.policy_document import (
|
|
11
|
+
EMAIL_EXEMPTION_FAMILY,
|
|
12
|
+
PRIVATE_IP_EXEMPTION_FAMILY,
|
|
13
|
+
MatchExemptionFamily,
|
|
14
|
+
)
|
|
15
|
+
from repository_checks.match_exemptions import (
|
|
16
|
+
load_all_match_exemptions,
|
|
17
|
+
load_match_exemptions,
|
|
18
|
+
)
|
|
19
|
+
from repository_checks.tracked_secrets import collect_tracked_secret_findings
|
|
20
|
+
|
|
21
|
+
CONTACT = "fixture-contact@company.io"
|
|
22
|
+
CHANGED_CONTACT = "changed-contact@company.io"
|
|
23
|
+
DIGEST = hashlib.sha256(CONTACT.encode()).hexdigest()
|
|
24
|
+
RUNNER_ADDRESS = "10.83.21.14"
|
|
25
|
+
CHANGED_RUNNER_ADDRESS = "10.83.21.15"
|
|
26
|
+
RUNNER_ADDRESS_DIGEST = hashlib.sha256(RUNNER_ADDRESS.encode()).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write_config(root: Path, entries: list[object]) -> None:
|
|
30
|
+
_write_document(root, {"version": 1, "email_exemptions": entries})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_private_ip_config(root: Path, entries: list[object]) -> None:
|
|
34
|
+
_write_document(root, {"version": 1, "private_ip_exemptions": entries})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _write_document(root: Path, document: dict[str, object]) -> None:
|
|
38
|
+
directory = root / "config"
|
|
39
|
+
directory.mkdir(exist_ok=True)
|
|
40
|
+
(directory / "repository-policy.json").write_text(
|
|
41
|
+
json.dumps(document), encoding="utf-8"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _entry(**changes: object) -> dict[str, object]:
|
|
46
|
+
return {
|
|
47
|
+
"path": "contacts.py",
|
|
48
|
+
"sha256": DIGEST,
|
|
49
|
+
"reason": "Published support contact",
|
|
50
|
+
**changes,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _runner_entry(**changes: object) -> dict[str, object]:
|
|
55
|
+
return {
|
|
56
|
+
"path": "runner.py",
|
|
57
|
+
"sha256": RUNNER_ADDRESS_DIGEST,
|
|
58
|
+
"reason": "Documented address of the shared build runner",
|
|
59
|
+
**changes,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_email_exemptions(root: Path) -> frozenset[tuple[str, str, str]]:
|
|
64
|
+
return load_match_exemptions(root, EMAIL_EXEMPTION_FAMILY)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_only_matching_email_and_path_are_exempt(tmp_path: Path) -> None:
|
|
68
|
+
_write_config(tmp_path, [_entry()])
|
|
69
|
+
(tmp_path / "contacts.py").write_text(
|
|
70
|
+
f"contact = '{CONTACT}'\nchanged = '{CHANGED_CONTACT}'\n", encoding="utf-8"
|
|
71
|
+
)
|
|
72
|
+
(tmp_path / "other.py").write_text(f"contact = '{CONTACT}'\n", encoding="utf-8")
|
|
73
|
+
findings = collect_tracked_secret_findings(tmp_path, ["contacts.py", "other.py"])
|
|
74
|
+
assert [finding.relative_path for finding in findings] == [
|
|
75
|
+
"contacts.py",
|
|
76
|
+
"other.py",
|
|
77
|
+
]
|
|
78
|
+
assert all(finding.message.startswith("[email]") for finding in findings)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_matching_contact_has_no_findings(tmp_path: Path) -> None:
|
|
82
|
+
_write_config(tmp_path, [_entry()])
|
|
83
|
+
(tmp_path / "contacts.py").write_text(f"contact = '{CONTACT}'\n", encoding="utf-8")
|
|
84
|
+
assert collect_tracked_secret_findings(tmp_path, ["contacts.py"]) == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_matching_private_ip_has_no_findings(tmp_path: Path) -> None:
|
|
88
|
+
_write_private_ip_config(tmp_path, [_runner_entry()])
|
|
89
|
+
(tmp_path / "runner.py").write_text(
|
|
90
|
+
f"host = '{RUNNER_ADDRESS}'\n", encoding="utf-8"
|
|
91
|
+
)
|
|
92
|
+
assert collect_tracked_secret_findings(tmp_path, ["runner.py"]) == []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_only_matching_private_ip_and_path_are_exempt(tmp_path: Path) -> None:
|
|
96
|
+
_write_private_ip_config(tmp_path, [_runner_entry()])
|
|
97
|
+
(tmp_path / "runner.py").write_text(
|
|
98
|
+
f"host = '{RUNNER_ADDRESS}'\nspare = '{CHANGED_RUNNER_ADDRESS}'\n",
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
)
|
|
101
|
+
(tmp_path / "other.py").write_text(f"host = '{RUNNER_ADDRESS}'\n", encoding="utf-8")
|
|
102
|
+
findings = collect_tracked_secret_findings(tmp_path, ["runner.py", "other.py"])
|
|
103
|
+
assert [finding.relative_path for finding in findings] == ["runner.py", "other.py"]
|
|
104
|
+
assert all(finding.message.startswith("[private-ip]") for finding in findings)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_private_ip_exemption_leaves_email_findings(tmp_path: Path) -> None:
|
|
108
|
+
_write_document(
|
|
109
|
+
tmp_path,
|
|
110
|
+
{"version": 1, "private_ip_exemptions": [_runner_entry(path="hosts.py")]},
|
|
111
|
+
)
|
|
112
|
+
(tmp_path / "hosts.py").write_text(
|
|
113
|
+
f"host = '{RUNNER_ADDRESS}'\ncontact = '{CONTACT}'\n", encoding="utf-8"
|
|
114
|
+
)
|
|
115
|
+
findings = collect_tracked_secret_findings(tmp_path, ["hosts.py"])
|
|
116
|
+
assert [finding.message.split("]")[0] + "]" for finding in findings] == ["[email]"]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_both_families_apply_together(tmp_path: Path) -> None:
|
|
120
|
+
_write_document(
|
|
121
|
+
tmp_path,
|
|
122
|
+
{
|
|
123
|
+
"version": 1,
|
|
124
|
+
"email_exemptions": [_entry(path="hosts.py")],
|
|
125
|
+
"private_ip_exemptions": [_runner_entry(path="hosts.py")],
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
(tmp_path / "hosts.py").write_text(
|
|
129
|
+
f"host = '{RUNNER_ADDRESS}'\ncontact = '{CONTACT}'\n", encoding="utf-8"
|
|
130
|
+
)
|
|
131
|
+
assert collect_tracked_secret_findings(tmp_path, ["hosts.py"]) == []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_email_entry_does_not_exempt_a_private_ip(tmp_path: Path) -> None:
|
|
135
|
+
_write_config(tmp_path, [_entry(path="runner.py", sha256=RUNNER_ADDRESS_DIGEST)])
|
|
136
|
+
(tmp_path / "runner.py").write_text(
|
|
137
|
+
f"host = '{RUNNER_ADDRESS}'\n", encoding="utf-8"
|
|
138
|
+
)
|
|
139
|
+
findings = collect_tracked_secret_findings(tmp_path, ["runner.py"])
|
|
140
|
+
assert [finding.message.startswith("[private-ip]") for finding in findings] == [
|
|
141
|
+
True
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.parametrize(
|
|
146
|
+
"path",
|
|
147
|
+
[
|
|
148
|
+
"/contacts.py",
|
|
149
|
+
"C:/contacts.py",
|
|
150
|
+
"../contacts.py",
|
|
151
|
+
"src/../contacts.py",
|
|
152
|
+
"*.py",
|
|
153
|
+
"src/?.py",
|
|
154
|
+
"src/[a].py",
|
|
155
|
+
"src\\contacts.py",
|
|
156
|
+
"src//contacts.py",
|
|
157
|
+
"./contacts.py",
|
|
158
|
+
"",
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
def test_rejects_nonliteral_or_nonrelative_paths(tmp_path: Path, path: str) -> None:
|
|
162
|
+
_write_config(tmp_path, [_entry(path=path)])
|
|
163
|
+
with pytest.raises(ValueError):
|
|
164
|
+
_load_email_exemptions(tmp_path)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.parametrize(
|
|
168
|
+
"entry",
|
|
169
|
+
[
|
|
170
|
+
None,
|
|
171
|
+
{},
|
|
172
|
+
_entry(reason=" "),
|
|
173
|
+
_entry(sha256="f" * 63),
|
|
174
|
+
_entry(sha256="z" * 64),
|
|
175
|
+
_entry(category="secret"),
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
def test_rejects_invalid_entries(tmp_path: Path, entry: object) -> None:
|
|
179
|
+
_write_config(tmp_path, [entry])
|
|
180
|
+
with pytest.raises(ValueError):
|
|
181
|
+
_load_email_exemptions(tmp_path)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_rejects_duplicate_entries(tmp_path: Path) -> None:
|
|
185
|
+
_write_config(tmp_path, [_entry(), _entry(reason="Another explanation")])
|
|
186
|
+
with pytest.raises(ValueError):
|
|
187
|
+
_load_email_exemptions(tmp_path)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_rejects_duplicate_private_ip_entries(tmp_path: Path) -> None:
|
|
191
|
+
_write_private_ip_config(
|
|
192
|
+
tmp_path, [_runner_entry(), _runner_entry(reason="Another explanation")]
|
|
193
|
+
)
|
|
194
|
+
with pytest.raises(ValueError):
|
|
195
|
+
load_match_exemptions(tmp_path, PRIVATE_IP_EXEMPTION_FAMILY)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@pytest.mark.parametrize(
|
|
199
|
+
"content",
|
|
200
|
+
[
|
|
201
|
+
"{",
|
|
202
|
+
"[]",
|
|
203
|
+
'{"version":true,"email_exemptions":[]}',
|
|
204
|
+
'{"version":1,"email_exemptions":{}}',
|
|
205
|
+
'{"version":1,"email_exemptions":[],"extra":0}',
|
|
206
|
+
'{"version":1,"version":1,"email_exemptions":[]}',
|
|
207
|
+
'{"version":1,"private_ip_exemptions":{}}',
|
|
208
|
+
],
|
|
209
|
+
)
|
|
210
|
+
def test_malformed_config_stops_scanning(tmp_path: Path, content: str) -> None:
|
|
211
|
+
(tmp_path / "config").mkdir()
|
|
212
|
+
(tmp_path / "config" / "repository-policy.json").write_text(
|
|
213
|
+
content, encoding="utf-8"
|
|
214
|
+
)
|
|
215
|
+
with pytest.raises(ValueError):
|
|
216
|
+
collect_tracked_secret_findings(tmp_path, [])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_missing_config_has_no_exemptions(tmp_path: Path) -> None:
|
|
220
|
+
assert _load_email_exemptions(tmp_path) == frozenset()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.mark.parametrize(
|
|
224
|
+
"family", [EMAIL_EXEMPTION_FAMILY, PRIVATE_IP_EXEMPTION_FAMILY]
|
|
225
|
+
)
|
|
226
|
+
@pytest.mark.parametrize(
|
|
227
|
+
"document",
|
|
228
|
+
[
|
|
229
|
+
{"version": 1},
|
|
230
|
+
{
|
|
231
|
+
"version": 1,
|
|
232
|
+
"path_exemptions": [
|
|
233
|
+
{
|
|
234
|
+
"path": "evidence/manifest.json",
|
|
235
|
+
"sha256": DIGEST,
|
|
236
|
+
"reason": "Frozen evidence file pinned by its consumers",
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
)
|
|
242
|
+
def test_document_without_a_family_list_loads_no_exemptions(
|
|
243
|
+
tmp_path: Path, document: dict[str, object], family: MatchExemptionFamily
|
|
244
|
+
) -> None:
|
|
245
|
+
"""A document may carry another family, or no exception list at all."""
|
|
246
|
+
_write_document(tmp_path, document)
|
|
247
|
+
assert load_match_exemptions(tmp_path, family) == frozenset()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_reason_rejection_names_the_ownership_requirement(tmp_path: Path) -> None:
|
|
251
|
+
_write_config(tmp_path, [_entry(reason=" ")])
|
|
252
|
+
with pytest.raises(ValueError) as rejection:
|
|
253
|
+
_load_email_exemptions(tmp_path)
|
|
254
|
+
assert (
|
|
255
|
+
str(rejection.value)
|
|
256
|
+
== "Email exemption reason must explain why the exception is owned"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_private_ip_reason_rejection_names_its_own_family(tmp_path: Path) -> None:
|
|
261
|
+
_write_private_ip_config(tmp_path, [_runner_entry(reason=" ")])
|
|
262
|
+
with pytest.raises(ValueError) as rejection:
|
|
263
|
+
load_match_exemptions(tmp_path, PRIVATE_IP_EXEMPTION_FAMILY)
|
|
264
|
+
assert (
|
|
265
|
+
str(rejection.value)
|
|
266
|
+
== "Private IP exemption reason must explain why the exception is owned"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_loading_every_family_returns_both_owned_matches(tmp_path: Path) -> None:
|
|
271
|
+
_write_document(
|
|
272
|
+
tmp_path,
|
|
273
|
+
{
|
|
274
|
+
"version": 1,
|
|
275
|
+
"email_exemptions": [_entry()],
|
|
276
|
+
"private_ip_exemptions": [_runner_entry()],
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
assert load_all_match_exemptions(tmp_path) == frozenset(
|
|
281
|
+
{
|
|
282
|
+
("contacts.py", "email", DIGEST),
|
|
283
|
+
("runner.py", "private-ip", RUNNER_ADDRESS_DIGEST),
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_loading_every_family_returns_nothing_without_a_policy_document(
|
|
289
|
+
tmp_path: Path,
|
|
290
|
+
) -> None:
|
|
291
|
+
assert load_all_match_exemptions(tmp_path) == frozenset()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Behavior tests for
|
|
1
|
+
"""Behavior tests for the severity that decides the committed-tree gate."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,15 +6,31 @@ import sys
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
9
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
9
10
|
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
10
11
|
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
12
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
11
14
|
|
|
12
15
|
from repository_checks.config.constants import (
|
|
16
|
+
ALL_CHECK_IDS,
|
|
17
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
18
|
+
CHECK_ID_PACKAGE_INVENTORY,
|
|
13
19
|
FAILED_CHECK_EXIT_CODE,
|
|
14
20
|
FINDINGS_EXIT_CODE,
|
|
21
|
+
SEVERITY_BREAKING,
|
|
22
|
+
SEVERITY_BY_CHECK_ID,
|
|
23
|
+
SEVERITY_SMELL,
|
|
15
24
|
SUCCESS_EXIT_CODE,
|
|
16
25
|
)
|
|
17
|
-
from repository_checks.
|
|
26
|
+
from repository_checks.hook_modules import load_hooks_module
|
|
27
|
+
from repository_checks.models import (
|
|
28
|
+
RepositoryCheckReport,
|
|
29
|
+
RepositoryFinding,
|
|
30
|
+
severity_for_check_id,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_LEDGER_CONSTANTS_MODULE_NAME = "hooks_constants.followup_ledger_constants"
|
|
18
34
|
|
|
19
35
|
|
|
20
36
|
def test_should_choose_exit_code_by_report_severity() -> None:
|
|
@@ -24,3 +40,52 @@ def test_should_choose_exit_code_by_report_severity() -> None:
|
|
|
24
40
|
assert RepositoryCheckReport((finding,), ("check",)).exit_code == (
|
|
25
41
|
FAILED_CHECK_EXIT_CODE
|
|
26
42
|
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_should_declare_a_severity_for_every_check() -> None:
|
|
46
|
+
assert tuple(SEVERITY_BY_CHECK_ID) == ALL_CHECK_IDS
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_should_read_a_check_with_no_row_as_breaking() -> None:
|
|
50
|
+
assert severity_for_check_id("check-with-no-row") == SEVERITY_BREAKING
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_carry_the_ledger_severity_names() -> None:
|
|
54
|
+
ledger_constants = load_hooks_module(_LEDGER_CONSTANTS_MODULE_NAME)
|
|
55
|
+
assert SEVERITY_BREAKING == ledger_constants.SEVERITY_BREAKING
|
|
56
|
+
assert SEVERITY_SMELL == ledger_constants.SEVERITY_SMELL
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_should_classify_the_package_inventory_check_as_advisory() -> None:
|
|
60
|
+
assert severity_for_check_id(CHECK_ID_PACKAGE_INVENTORY) == SEVERITY_SMELL
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_pass_a_report_that_carries_advisory_findings_alone() -> None:
|
|
64
|
+
report = RepositoryCheckReport((_inventory_finding(),), ())
|
|
65
|
+
assert report.all_smell_findings == (_inventory_finding(),)
|
|
66
|
+
assert report.all_breaking_findings == ()
|
|
67
|
+
assert report.exit_code == SUCCESS_EXIT_CODE
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_should_fail_a_report_that_carries_a_breaking_finding() -> None:
|
|
71
|
+
breaking_finding = RepositoryFinding(
|
|
72
|
+
CHECK_ID_CLAUDE_MD_ORPHANS, "notes/CLAUDE.md", "references missing file x.py"
|
|
73
|
+
)
|
|
74
|
+
report = RepositoryCheckReport((_inventory_finding(), breaking_finding), ())
|
|
75
|
+
assert report.all_breaking_findings == (breaking_finding,)
|
|
76
|
+
assert report.exit_code == FINDINGS_EXIT_CODE
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_should_fail_closed_over_an_advisory_finding() -> None:
|
|
80
|
+
report = RepositoryCheckReport(
|
|
81
|
+
(_inventory_finding(),), (CHECK_ID_PACKAGE_INVENTORY,)
|
|
82
|
+
)
|
|
83
|
+
assert report.exit_code == FAILED_CHECK_EXIT_CODE
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _inventory_finding() -> RepositoryFinding:
|
|
87
|
+
return RepositoryFinding(
|
|
88
|
+
CHECK_ID_PACKAGE_INVENTORY,
|
|
89
|
+
"pipeline/check_dialer_seam_cli.py",
|
|
90
|
+
"production file is absent from package inventory",
|
|
91
|
+
)
|
|
@@ -14,7 +14,7 @@ if str(_TESTS_DIRECTORY) not in sys.path:
|
|
|
14
14
|
from repository_checks.config.constants import (
|
|
15
15
|
CHECK_ID_PACKAGE_INVENTORY,
|
|
16
16
|
FAILED_CHECK_EXIT_CODE,
|
|
17
|
-
|
|
17
|
+
SUCCESS_EXIT_CODE,
|
|
18
18
|
)
|
|
19
19
|
from repository_policy_test_support import (
|
|
20
20
|
commit_tracked_files,
|
|
@@ -33,14 +33,16 @@ _SKILL_INVENTORY_MARKDOWN = (
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def test_should_report_a_stale_package_inventory_without_failing(
|
|
37
|
+
tmp_path: Path,
|
|
38
|
+
) -> None:
|
|
37
39
|
repository_root = tmp_path / "repo"
|
|
38
40
|
initialize_repository(repository_root)
|
|
39
41
|
package_directory = repository_root / "pipeline"
|
|
40
42
|
_write_package_inventory(package_directory)
|
|
41
43
|
commit_tracked_files(repository_root)
|
|
42
44
|
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
43
|
-
assert exit_code ==
|
|
45
|
+
assert exit_code == SUCCESS_EXIT_CODE
|
|
44
46
|
assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
|
|
45
47
|
assert "pipeline/check_dialer_seam_cli.py" in stdout_text
|
|
46
48
|
|
|
@@ -52,7 +54,7 @@ def test_should_check_active_skills_and_exclude_archived_skills(tmp_path: Path)
|
|
|
52
54
|
_seed_skill_package(repository_root / ".agents" / "skills-archived" / "old-skill")
|
|
53
55
|
commit_tracked_files(repository_root)
|
|
54
56
|
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
55
|
-
assert exit_code ==
|
|
57
|
+
assert exit_code == SUCCESS_EXIT_CODE
|
|
56
58
|
assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
|
|
57
59
|
assert ".agents/skills/live-skill/scripts/unlisted_gamma.py" in stdout_text
|
|
58
60
|
assert (
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Specifications for the quality gate reaching every PowerShell suite.
|
|
2
|
+
|
|
3
|
+
A suite nobody calls stops guarding without failing, so the tree reads as
|
|
4
|
+
covered while nothing holds it. These tests pin the wiring that calls the
|
|
5
|
+
suites and the directory the call points at.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
13
|
+
_CHECK_SCRIPT_PATH = _SCRIPTS_DIRECTORY / "check.ps1"
|
|
14
|
+
_RUNNER_SCRIPT_PATH = _SCRIPTS_DIRECTORY / "Invoke-PesterSuites.ps1"
|
|
15
|
+
_TESTS_DIRECTORY = _SCRIPTS_DIRECTORY / "tests"
|
|
16
|
+
_SUITE_GLOB = "*.Tests.ps1"
|
|
17
|
+
_UTF8_ENCODING = "utf-8"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_should_run_the_suites_as_a_gate_tool() -> None:
|
|
21
|
+
script_text = _CHECK_SCRIPT_PATH.read_text(encoding=_UTF8_ENCODING)
|
|
22
|
+
|
|
23
|
+
assert "Invoke-Tool -Label 'pester'" in script_text
|
|
24
|
+
assert "Invoke-PesterSuites.ps1" in script_text
|
|
25
|
+
assert "SkipPester" in script_text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_should_point_the_run_at_the_directory_holding_the_suites() -> None:
|
|
29
|
+
script_text = _CHECK_SCRIPT_PATH.read_text(encoding=_UTF8_ENCODING)
|
|
30
|
+
|
|
31
|
+
assert "$pesterTestsRoot = Resolve-Path (Join-Path $PSScriptRoot 'tests')" in (
|
|
32
|
+
script_text
|
|
33
|
+
)
|
|
34
|
+
assert "-TestsRoot $pesterTestsRoot" in script_text
|
|
35
|
+
assert list(_TESTS_DIRECTORY.glob(_SUITE_GLOB))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_should_run_the_suites_in_a_child_process() -> None:
|
|
39
|
+
"""Invoke-Tool reads an exit code, which an in-process cmdlet never sets."""
|
|
40
|
+
script_text = _CHECK_SCRIPT_PATH.read_text(encoding=_UTF8_ENCODING)
|
|
41
|
+
|
|
42
|
+
assert "pwsh -NoProfile -File" in script_text
|
|
43
|
+
assert "Invoke-Pester -Configuration" not in script_text
|
|
44
|
+
assert "Invoke-Pester -Configuration" in (
|
|
45
|
+
_RUNNER_SCRIPT_PATH.read_text(encoding=_UTF8_ENCODING)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_should_fail_the_run_when_the_module_is_missing() -> None:
|
|
50
|
+
runner_text = _RUNNER_SCRIPT_PATH.read_text(encoding=_UTF8_ENCODING)
|
|
51
|
+
|
|
52
|
+
assert "Get-Module -ListAvailable -Name Pester" in runner_text
|
|
53
|
+
assert "exit 1" in runner_text
|