claude-dev-env 8.7.0 → 8.9.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/SKILL.md +1 -0
- package/.agents/skills/pull-request/scripts/pull_request.py +1 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +2 -0
- package/.agents/skills/team-advisor/SKILL.md +2 -2
- package/_shared/advisor/reference/identity.md +1 -1
- package/_shared/advisor/reference/spawn-walk-log.md +1 -1
- package/_shared/advisor/reference/warm-up.md +3 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +20 -3
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +50 -6
- package/docs/references/advisor-tool.md +3 -2
- package/docs/second-claude-account.md +57 -0
- package/hooks/blocking/test_pii_scanner.py +8 -0
- package/hooks/hooks.json +25 -0
- package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
- package/hooks/hooks_constants/skill_loaded_reminder_constants.py +58 -0
- package/hooks/session/skill_loaded_reminder.py +199 -0
- package/hooks/session/test_skill_loaded_reminder.py +206 -0
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +26 -1
- package/rules/long-horizon-autonomy.md +11 -0
- package/rules/prefer-existing-tools.md +35 -0
- package/rules/review-closure-is-a-check.md +1 -1
- package/scripts/claude_account_choice.py +291 -0
- package/scripts/claude_account_profile.py +371 -0
- package/scripts/claude_chain_usage.py +49 -5
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +181 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +27 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +60 -0
- package/scripts/dev_env_scripts_constants/test_claude_account_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/test_private_term_constants.py +29 -0
- package/scripts/durable_post_lint.py +47 -1
- package/scripts/durable_post_lint_config/config/constants.py +1 -0
- package/scripts/gh_artifact_upload.py +56 -3
- package/scripts/private_term_scan.py +135 -0
- package/scripts/private_terms.py +87 -0
- package/scripts/repository_checks/config/constants.py +3 -0
- package/scripts/repository_checks/runner.py +7 -0
- package/scripts/repository_checks/tracked_private_terms.py +57 -0
- package/scripts/test_claude_account_choice.py +245 -0
- package/scripts/test_claude_account_profile.py +220 -0
- package/scripts/test_claude_chain_usage.py +40 -0
- package/scripts/test_durable_post_lint.py +90 -0
- package/scripts/tests/test_gh_artifact_upload.py +75 -0
- package/scripts/tests/test_match_exemptions.py +2 -2
- package/scripts/tests/test_private_term_scan.py +176 -0
- package/scripts/tests/test_private_terms.py +59 -0
- package/scripts/tests/test_repository_policy.py +2 -0
- package/scripts/tests/test_runner.py +20 -0
- package/scripts/tests/test_tracked_private_terms.py +71 -0
- package/scripts/tests/test_tracked_secrets.py +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
import types
|
|
@@ -26,6 +27,15 @@ _DEFAULT_READBACK_ASSET = {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
_REAL_WHICH = shutil.which
|
|
31
|
+
_REAL_RUN = subprocess.run
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(autouse=True)
|
|
35
|
+
def oxipng_on_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
36
|
+
monkeypatch.setattr(shutil, "which", lambda _name: "oxipng")
|
|
37
|
+
|
|
38
|
+
|
|
29
39
|
def _make_gh_stub(
|
|
30
40
|
recorded_calls: list[list[str]],
|
|
31
41
|
view_return_code: int,
|
|
@@ -203,3 +213,68 @@ def test_upload_artifact_missing_file_raises(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
203
213
|
monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=0))
|
|
204
214
|
with pytest.raises(mod.ArtifactUploadError):
|
|
205
215
|
mod.upload_artifact("does_not_exist_9f3a.png", "owner/repo")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_upload_artifact_sends_the_oxipng_shrunk_png(
|
|
219
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
220
|
+
) -> None:
|
|
221
|
+
image_module = pytest.importorskip("PIL.Image")
|
|
222
|
+
if _REAL_WHICH("oxipng") is None:
|
|
223
|
+
pytest.skip("oxipng is not on PATH")
|
|
224
|
+
monkeypatch.setattr(shutil, "which", _REAL_WHICH)
|
|
225
|
+
source_file = tmp_path / "contact_sheet.png"
|
|
226
|
+
image_module.new("RGB", (64, 64), (200, 40, 40)).save(
|
|
227
|
+
source_file, format="PNG", compress_level=0
|
|
228
|
+
)
|
|
229
|
+
source_bytes = source_file.read_bytes()
|
|
230
|
+
all_uploaded_sizes: list[int] = []
|
|
231
|
+
gh_stub = _make_gh_stub([], view_return_code=0)
|
|
232
|
+
|
|
233
|
+
def gh_stub_beside_live_oxipng(
|
|
234
|
+
all_command_arguments: list[str], **keyword_arguments: object
|
|
235
|
+
) -> object:
|
|
236
|
+
if all_command_arguments[0] != "gh":
|
|
237
|
+
return _REAL_RUN(all_command_arguments, **keyword_arguments)
|
|
238
|
+
if "upload" in all_command_arguments:
|
|
239
|
+
staged_path = Path(all_command_arguments[4])
|
|
240
|
+
all_uploaded_sizes.append(staged_path.stat().st_size)
|
|
241
|
+
with image_module.open(staged_path) as uploaded:
|
|
242
|
+
assert uploaded.convert("RGB").tobytes() == bytes((200, 40, 40)) * 4096
|
|
243
|
+
return gh_stub(all_command_arguments, **keyword_arguments)
|
|
244
|
+
|
|
245
|
+
monkeypatch.setattr(subprocess, "run", gh_stub_beside_live_oxipng)
|
|
246
|
+
|
|
247
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
248
|
+
|
|
249
|
+
assert all_uploaded_sizes and all_uploaded_sizes[0] < len(source_bytes)
|
|
250
|
+
assert source_file.read_bytes() == source_bytes
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_upload_artifact_refuses_a_png_when_oxipng_is_missing(
|
|
254
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
255
|
+
) -> None:
|
|
256
|
+
source_file = tmp_path / "out.png"
|
|
257
|
+
source_file.write_bytes(b"binary")
|
|
258
|
+
recorded_calls: list[list[str]] = []
|
|
259
|
+
monkeypatch.setattr(shutil, "which", lambda _name: None)
|
|
260
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0))
|
|
261
|
+
|
|
262
|
+
with pytest.raises(mod.ArtifactUploadError, match="oxipng"):
|
|
263
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
264
|
+
|
|
265
|
+
assert not any("upload" in call for call in recorded_calls)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_optimize_png_losslessly_leaves_other_formats_alone(
|
|
269
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
270
|
+
) -> None:
|
|
271
|
+
source_file = tmp_path / "trace.zip"
|
|
272
|
+
source_file.write_bytes(b"PK archive")
|
|
273
|
+
recorded_calls: list[list[str]] = []
|
|
274
|
+
monkeypatch.setattr(shutil, "which", lambda _name: None)
|
|
275
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0))
|
|
276
|
+
|
|
277
|
+
mod.optimize_png_losslessly(source_file)
|
|
278
|
+
|
|
279
|
+
assert source_file.read_bytes() == b"PK archive"
|
|
280
|
+
assert recorded_calls == []
|
|
@@ -21,8 +21,8 @@ from repository_checks.tracked_secrets import collect_tracked_secret_findings
|
|
|
21
21
|
CONTACT = "fixture-contact@company.io"
|
|
22
22
|
CHANGED_CONTACT = "changed-contact@company.io"
|
|
23
23
|
DIGEST = hashlib.sha256(CONTACT.encode()).hexdigest()
|
|
24
|
-
RUNNER_ADDRESS = "10.
|
|
25
|
-
CHANGED_RUNNER_ADDRESS = "10.
|
|
24
|
+
RUNNER_ADDRESS = "10.0.0.14"
|
|
25
|
+
CHANGED_RUNNER_ADDRESS = "10.0.0.15"
|
|
26
26
|
RUNNER_ADDRESS_DIGEST = hashlib.sha256(RUNNER_ADDRESS.encode()).hexdigest()
|
|
27
27
|
|
|
28
28
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Behavior tests for the GitHub event private-term scan."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from dev_env_scripts_constants.private_term_constants import PrivateTermDigest
|
|
13
|
+
|
|
14
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
15
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
17
|
+
|
|
18
|
+
import private_term_scan
|
|
19
|
+
|
|
20
|
+
_FIXTURE_TERM = "acmewidget"
|
|
21
|
+
_CLEAN_IDENTITY = ("Claude", "noreply@example.com")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(autouse=True)
|
|
25
|
+
def private_fixture_terms(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
26
|
+
monkeypatch.setattr(
|
|
27
|
+
private_term_scan,
|
|
28
|
+
"ALL_PRIVATE_TERM_DIGESTS",
|
|
29
|
+
frozenset(
|
|
30
|
+
{
|
|
31
|
+
PrivateTermDigest(
|
|
32
|
+
length=len(_FIXTURE_TERM),
|
|
33
|
+
sha256=hashlib.sha256(_FIXTURE_TERM.encode("utf-8")).hexdigest(),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _write_event(tmp_path: Path, event: dict[str, object]) -> Path:
|
|
41
|
+
event_path = tmp_path / "event.json"
|
|
42
|
+
event_path.write_text(json.dumps(event), encoding="utf-8")
|
|
43
|
+
return event_path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _git(repository_root: Path, *all_arguments: str, name: str, email: str) -> str:
|
|
47
|
+
completed = subprocess.run(
|
|
48
|
+
["git", "-C", str(repository_root), *all_arguments],
|
|
49
|
+
check=True,
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
env={
|
|
53
|
+
"GIT_AUTHOR_NAME": name,
|
|
54
|
+
"GIT_AUTHOR_EMAIL": email,
|
|
55
|
+
"GIT_COMMITTER_NAME": name,
|
|
56
|
+
"GIT_COMMITTER_EMAIL": email,
|
|
57
|
+
"GIT_CONFIG_GLOBAL": "/dev/null",
|
|
58
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
59
|
+
"HOME": str(repository_root),
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
return completed.stdout.strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _repository_with_commits(
|
|
66
|
+
tmp_path: Path, all_commits: list[tuple[str, str, str]]
|
|
67
|
+
) -> tuple[Path, str, str]:
|
|
68
|
+
repository_root = tmp_path / "repo"
|
|
69
|
+
repository_root.mkdir()
|
|
70
|
+
_git(
|
|
71
|
+
repository_root, "init", "-q", name=_CLEAN_IDENTITY[0], email=_CLEAN_IDENTITY[1]
|
|
72
|
+
)
|
|
73
|
+
_git(
|
|
74
|
+
repository_root,
|
|
75
|
+
"commit",
|
|
76
|
+
"-q",
|
|
77
|
+
"--allow-empty",
|
|
78
|
+
"-m",
|
|
79
|
+
"base",
|
|
80
|
+
name=_CLEAN_IDENTITY[0],
|
|
81
|
+
email=_CLEAN_IDENTITY[1],
|
|
82
|
+
)
|
|
83
|
+
base = _git(repository_root, "rev-parse", "HEAD", name="x", email="x@example.com")
|
|
84
|
+
for each_message, each_name, each_email in all_commits:
|
|
85
|
+
_git(
|
|
86
|
+
repository_root,
|
|
87
|
+
"commit",
|
|
88
|
+
"-q",
|
|
89
|
+
"--allow-empty",
|
|
90
|
+
"-m",
|
|
91
|
+
each_message,
|
|
92
|
+
name=each_name,
|
|
93
|
+
email=each_email,
|
|
94
|
+
)
|
|
95
|
+
head = _git(repository_root, "rev-parse", "HEAD", name="x", email="x@example.com")
|
|
96
|
+
return repository_root, base, head
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_flag_a_pull_request_body_line(tmp_path: Path) -> None:
|
|
100
|
+
event_path = _write_event(
|
|
101
|
+
tmp_path,
|
|
102
|
+
{
|
|
103
|
+
"pull_request": {
|
|
104
|
+
"title": "fix: one",
|
|
105
|
+
"body": "Why\nSee Acme-Widgets-Inc/x#1\n",
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
all_findings = private_term_scan.scan_event(event_path)
|
|
110
|
+
assert all_findings == [
|
|
111
|
+
"pull request body: Line 2 names a private organization. "
|
|
112
|
+
"Describe it in general terms and drop any link to it."
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_should_flag_a_pull_request_title(tmp_path: Path) -> None:
|
|
117
|
+
event_path = _write_event(
|
|
118
|
+
tmp_path, {"pull_request": {"title": "fix: acme widget upload", "body": None}}
|
|
119
|
+
)
|
|
120
|
+
assert private_term_scan.scan_event(event_path)[0].startswith("pull request title:")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.parametrize(
|
|
124
|
+
("event", "label"),
|
|
125
|
+
[
|
|
126
|
+
({"comment": {"body": "by acme widgets"}}, "comment body"),
|
|
127
|
+
({"review": {"body": "by acme widgets"}}, "review body"),
|
|
128
|
+
({"issue": {"title": "clean", "body": "by acme widgets"}}, "issue body"),
|
|
129
|
+
({"release": {"name": "v1", "body": "by acme widgets"}}, "release body"),
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
def test_should_flag_each_event_text(
|
|
133
|
+
tmp_path: Path, event: dict[str, object], label: str
|
|
134
|
+
) -> None:
|
|
135
|
+
all_findings = private_term_scan.scan_event(_write_event(tmp_path, event))
|
|
136
|
+
assert len(all_findings) == 1
|
|
137
|
+
assert all_findings[0].startswith(f"{label}: Line 1")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_should_pass_a_clean_event(tmp_path: Path) -> None:
|
|
141
|
+
event_path = _write_event(
|
|
142
|
+
tmp_path,
|
|
143
|
+
{"comment": {"body": "Looks good."}, "issue": {"title": "t", "body": None}},
|
|
144
|
+
)
|
|
145
|
+
assert private_term_scan.scan_event(event_path) == []
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_should_flag_a_commit_message_and_a_commit_identity(tmp_path: Path) -> None:
|
|
149
|
+
repository_root, base, head = _repository_with_commits(
|
|
150
|
+
tmp_path,
|
|
151
|
+
[
|
|
152
|
+
("fix: clean change", "Pat", "pat@acmewidgets.example.com"),
|
|
153
|
+
("docs: note\n\nWritten for Acme Widgets.", *_CLEAN_IDENTITY),
|
|
154
|
+
("chore: clean", *_CLEAN_IDENTITY),
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
all_findings = private_term_scan.scan_commits(repository_root, f"{base}..{head}")
|
|
158
|
+
assert len(all_findings) == 2
|
|
159
|
+
assert all_findings[0].startswith("commit ")
|
|
160
|
+
assert " identity:" in all_findings[0]
|
|
161
|
+
assert " message: Line 3 " in all_findings[1]
|
|
162
|
+
assert all("acme" not in each_finding.lower() for each_finding in all_findings)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_should_print_findings_and_fail_from_the_command_line(
|
|
166
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
167
|
+
) -> None:
|
|
168
|
+
event_path = _write_event(tmp_path, {"comment": {"body": "by acme widgets"}})
|
|
169
|
+
exit_code = private_term_scan.main(["--event-path", str(event_path)])
|
|
170
|
+
assert exit_code == 1
|
|
171
|
+
assert "comment body: Line 1" in capsys.readouterr().out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_should_pass_from_the_command_line_on_a_clean_event(tmp_path: Path) -> None:
|
|
175
|
+
event_path = _write_event(tmp_path, {"comment": {"body": "fine"}})
|
|
176
|
+
assert private_term_scan.main(["--event-path", str(event_path)]) == 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Behavior tests for the hashed private-term matcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
10
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
12
|
+
|
|
13
|
+
from dev_env_scripts_constants.private_term_constants import PrivateTermDigest
|
|
14
|
+
from private_terms import names_private_term, private_term_line_numbers
|
|
15
|
+
|
|
16
|
+
_FIXTURE_TERM = "acmewidget"
|
|
17
|
+
_ALL_FIXTURE_DIGESTS = frozenset(
|
|
18
|
+
{
|
|
19
|
+
PrivateTermDigest(
|
|
20
|
+
length=len(_FIXTURE_TERM),
|
|
21
|
+
sha256=hashlib.sha256(_FIXTURE_TERM.encode("utf-8")).hexdigest(),
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_report_the_line_of_a_spaced_spelling() -> None:
|
|
28
|
+
text = "first line\nBuilt for Acme Widgets, Inc. last year\nthird line\n"
|
|
29
|
+
assert private_term_line_numbers(text, _ALL_FIXTURE_DIGESTS) == (2,)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_should_report_a_hyphenated_owner_in_a_link() -> None:
|
|
33
|
+
text = "See https://github.com/Acme-Widgets-Inc/tools/pull/12 for detail."
|
|
34
|
+
assert private_term_line_numbers(text, _ALL_FIXTURE_DIGESTS) == (1,)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_report_an_email_domain() -> None:
|
|
38
|
+
text = "Co-authored-by: Pat <pat@ACMEWIDGETS.EXAMPLE.COM>"
|
|
39
|
+
assert private_term_line_numbers(text, _ALL_FIXTURE_DIGESTS) == (1,)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_should_report_a_term_split_across_a_line_break_at_its_first_line() -> None:
|
|
43
|
+
text = "one\nsold by Acme\nWidgets today\n"
|
|
44
|
+
assert private_term_line_numbers(text, _ALL_FIXTURE_DIGESTS) == (2,)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_should_report_each_line_once() -> None:
|
|
48
|
+
text = "acme widget and acme-widget\nclean\nAcmeWidget\n"
|
|
49
|
+
assert private_term_line_numbers(text, _ALL_FIXTURE_DIGESTS) == (1, 3)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_should_report_nothing_for_clean_text() -> None:
|
|
53
|
+
text = "An acme of widgets.\nWidget acme.\n"
|
|
54
|
+
assert private_term_line_numbers(text, _ALL_FIXTURE_DIGESTS) == ()
|
|
55
|
+
assert not names_private_term(text, _ALL_FIXTURE_DIGESTS)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_should_answer_whether_text_names_a_term() -> None:
|
|
59
|
+
assert names_private_term("Acme-Widgets-Inc", _ALL_FIXTURE_DIGESTS)
|
|
@@ -20,6 +20,7 @@ from repository_checks.config.constants import (
|
|
|
20
20
|
CHECK_ID_PACKAGE_INVENTORY,
|
|
21
21
|
CHECK_ID_PYTEST_TESTPATHS,
|
|
22
22
|
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
23
|
+
CHECK_ID_TRACKED_PRIVATE_TERMS,
|
|
23
24
|
FOLLOWUP_LEDGER_MODULE_NAME,
|
|
24
25
|
SUCCESS_EXIT_CODE,
|
|
25
26
|
)
|
|
@@ -52,6 +53,7 @@ def test_should_keep_stable_check_identifiers() -> None:
|
|
|
52
53
|
CHECK_ID_PACKAGE_INVENTORY,
|
|
53
54
|
CHECK_ID_PYTEST_TESTPATHS,
|
|
54
55
|
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
56
|
+
CHECK_ID_TRACKED_PRIVATE_TERMS,
|
|
55
57
|
)
|
|
56
58
|
|
|
57
59
|
|
|
@@ -12,6 +12,7 @@ if str(_TESTS_DIRECTORY) not in sys.path:
|
|
|
12
12
|
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
13
13
|
|
|
14
14
|
from repository_checks.config.constants import (
|
|
15
|
+
CHECK_ID_TRACKED_PRIVATE_TERMS,
|
|
15
16
|
FAILED_CHECK_EXIT_CODE,
|
|
16
17
|
FINDINGS_EXIT_CODE,
|
|
17
18
|
)
|
|
@@ -57,3 +58,22 @@ def test_should_emit_stable_sorted_findings(tmp_path: Path) -> None:
|
|
|
57
58
|
assert first_exit_code == FINDINGS_EXIT_CODE
|
|
58
59
|
assert first_stdout == second_stdout
|
|
59
60
|
assert first_stdout.index("alpha/CLAUDE.md") < first_stdout.index("zeta/CLAUDE.md")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_fail_closed_when_the_private_term_check_cannot_read_a_file(
|
|
64
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
65
|
+
) -> None:
|
|
66
|
+
repository_root = seed_clean_repository(tmp_path / "repo")
|
|
67
|
+
|
|
68
|
+
def fail_private_term_scan(
|
|
69
|
+
_repository_root: Path, _all_tracked_paths: object
|
|
70
|
+
) -> list[object]:
|
|
71
|
+
raise OSError("tracked file unreadable")
|
|
72
|
+
|
|
73
|
+
monkeypatch.setattr(
|
|
74
|
+
"repository_checks.runner.collect_tracked_private_term_findings",
|
|
75
|
+
fail_private_term_scan,
|
|
76
|
+
)
|
|
77
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
78
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
79
|
+
assert f"error: rule failed: {CHECK_ID_TRACKED_PRIVATE_TERMS}" in stdout_text
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Behavior tests for the tracked private-term check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from dev_env_scripts_constants.private_term_constants import PrivateTermDigest
|
|
11
|
+
from repository_checks import tracked_private_terms
|
|
12
|
+
from repository_checks.config.constants import (
|
|
13
|
+
CHECK_ID_TRACKED_PRIVATE_TERMS,
|
|
14
|
+
FINDINGS_EXIT_CODE,
|
|
15
|
+
SUCCESS_EXIT_CODE,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
19
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
21
|
+
|
|
22
|
+
from repository_policy_test_support import (
|
|
23
|
+
commit_tracked_files,
|
|
24
|
+
initialize_repository,
|
|
25
|
+
run_policy,
|
|
26
|
+
write_text,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_FIXTURE_TERM = "acmewidget"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture(autouse=True)
|
|
33
|
+
def private_fixture_terms(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
34
|
+
monkeypatch.setattr(
|
|
35
|
+
tracked_private_terms,
|
|
36
|
+
"ALL_PRIVATE_TERM_DIGESTS",
|
|
37
|
+
frozenset(
|
|
38
|
+
{
|
|
39
|
+
PrivateTermDigest(
|
|
40
|
+
length=len(_FIXTURE_TERM),
|
|
41
|
+
sha256=hashlib.sha256(_FIXTURE_TERM.encode("utf-8")).hexdigest(),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_should_flag_a_tracked_file_naming_a_private_organization(
|
|
49
|
+
tmp_path: Path,
|
|
50
|
+
) -> None:
|
|
51
|
+
repository_root = tmp_path / "repo"
|
|
52
|
+
initialize_repository(repository_root)
|
|
53
|
+
write_text(repository_root / "docs" / "notes.md", "intro\nBuilt by Acme Widgets.\n")
|
|
54
|
+
commit_tracked_files(repository_root)
|
|
55
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
56
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
57
|
+
assert f"{CHECK_ID_TRACKED_PRIVATE_TERMS}: docs/notes.md:" in stdout_text
|
|
58
|
+
assert "Line 2" in stdout_text
|
|
59
|
+
assert "Acme" not in stdout_text
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_should_pass_a_tree_that_names_no_private_organization(
|
|
63
|
+
tmp_path: Path,
|
|
64
|
+
) -> None:
|
|
65
|
+
repository_root = tmp_path / "repo"
|
|
66
|
+
initialize_repository(repository_root)
|
|
67
|
+
write_text(repository_root / "docs" / "notes.md", "An acme of widget design.\n")
|
|
68
|
+
commit_tracked_files(repository_root)
|
|
69
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
70
|
+
assert exit_code == SUCCESS_EXIT_CODE
|
|
71
|
+
assert stdout_text == ""
|
|
@@ -46,7 +46,7 @@ _PII_PREVENTION_CONSTANTS = load_hooks_module(
|
|
|
46
46
|
_PII_CATEGORY_EMAIL = _PII_PREVENTION_CONSTANTS.CATEGORY_EMAIL
|
|
47
47
|
_PII_CATEGORY_HOME_PATH = _PII_PREVENTION_CONSTANTS.CATEGORY_HOME_PATH
|
|
48
48
|
_PII_CATEGORY_PRIVATE_IP = _PII_PREVENTION_CONSTANTS.CATEGORY_PRIVATE_IP
|
|
49
|
-
_OWNED_ADDRESS = "10.
|
|
49
|
+
_OWNED_ADDRESS = "10.0.0.14"
|
|
50
50
|
_OWNED_ADDRESS_RELATIVE_PATH = "src/runner.py"
|
|
51
51
|
|
|
52
52
|
|