claude-dev-env 8.72.2 → 8.73.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/build-eval/SKILL.md +4 -8
- package/.agents/skills/build-eval/reference/context-budget.md +49 -0
- package/.agents/skills/orchestrator/reference/oversee-delegated-work.md +1 -0
- package/.agents/skills/pr-lifecycle/SKILL.md +3 -10
- package/bin/codex-trimmed-sol.mjs +109 -0
- package/bin/codex-trimmed-sol.test.mjs +226 -0
- package/bin/install.mjs +18 -0
- package/bin/install.question-presentation.test.mjs +1 -1
- package/docs/codex-accounts.md +26 -0
- package/hooks/blocking/context_budget_blocker.py +177 -0
- package/hooks/blocking/test_context_budget_blocker.py +189 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +3 -1
- package/hooks/context_budget/__init__.py +1 -0
- package/hooks/context_budget/baseline.py +78 -0
- package/hooks/context_budget/findings.py +251 -0
- package/hooks/context_budget/hook_measure.py +178 -0
- package/hooks/context_budget/model.py +134 -0
- package/hooks/context_budget/policy_changes.py +214 -0
- package/hooks/context_budget/policy_file.py +222 -0
- package/hooks/context_budget/sections.py +213 -0
- package/hooks/context_budget/tests/budget_support.py +71 -0
- package/hooks/context_budget/tests/test_baseline.py +43 -0
- package/hooks/context_budget/tests/test_findings.py +120 -0
- package/hooks/context_budget/tests/test_hook_measure.py +57 -0
- package/hooks/context_budget/tests/test_policy_changes.py +175 -0
- package/hooks/context_budget/tests/test_policy_file.py +62 -0
- package/hooks/context_budget/tests/test_sections.py +112 -0
- package/hooks/features/write-edit-blocking.md +5 -1
- package/hooks/hooks_constants/context_budget_constants.py +154 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -1
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -2
- package/hooks/test_context_budget_hooks.py +79 -0
- package/hooks/test_files/context_budget/advisor_config/settings.json +3 -0
- package/hooks/test_files/context_budget/empty_transcript.jsonl +0 -0
- package/hooks/test_files/context_budget/orchestrator_skill_3f2c3ef.txt +170 -0
- package/hooks/test_files/context_budget/orchestrator_skill_98f45b1.txt +158 -0
- package/package.json +1 -1
- package/scripts/_code_review_test_support.py +7 -12
- package/scripts/context_budget_check.py +243 -0
- package/scripts/dev_env_scripts_constants/context_budget_check_constants.py +26 -0
- package/scripts/policy_lint/adapter_context_budget.py +158 -0
- package/scripts/policy_lint/adapters.py +73 -0
- package/scripts/policy_lint/config/constants.py +17 -1
- package/scripts/policy_lint/registry.py +17 -0
- package/scripts/tests/test_context_budget_check.py +270 -0
- package/scripts/tests/test_policy_lint_rules_context_budget.py +218 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +26 -0
- package/system-prompts/codex-sol.md +170 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""The context budget command checks explicit files against HEAD and keeps the list generated."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import context_budget_check
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
|
|
15
|
+
SKILL_RELATIVE_PATH = "skills/example/SKILL.md"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _git(repository_root: Path, *all_arguments: str) -> None:
|
|
19
|
+
subprocess.run(
|
|
20
|
+
[
|
|
21
|
+
"git",
|
|
22
|
+
"-c",
|
|
23
|
+
"user.name=Test",
|
|
24
|
+
"-c",
|
|
25
|
+
"user.email=test@example.com",
|
|
26
|
+
*all_arguments,
|
|
27
|
+
],
|
|
28
|
+
cwd=repository_root,
|
|
29
|
+
check=True,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _section(heading: str, detail_count: int) -> str:
|
|
35
|
+
return f"## {heading}\n" + "".join(
|
|
36
|
+
f"Detail line {each}.\n" for each in range(detail_count)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def repository_root(tmp_path: Path) -> Path:
|
|
42
|
+
policy = {
|
|
43
|
+
"section_detail_line_limit": 6,
|
|
44
|
+
"kinds": [
|
|
45
|
+
{
|
|
46
|
+
"name": "skill entry",
|
|
47
|
+
"patterns": ["**/SKILL.md"],
|
|
48
|
+
"line_limit": 10,
|
|
49
|
+
"section_rule": True,
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"hooks": [],
|
|
53
|
+
"baseline": {
|
|
54
|
+
"files": {SKILL_RELATIVE_PATH: {"lines": 20, "sections": ["Plan"]}},
|
|
55
|
+
"hooks": {},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
(tmp_path / ".claude").mkdir()
|
|
59
|
+
(tmp_path / ".claude" / "context-budget.json").write_text(
|
|
60
|
+
json.dumps(policy), encoding="utf-8"
|
|
61
|
+
)
|
|
62
|
+
skill_path = tmp_path / SKILL_RELATIVE_PATH
|
|
63
|
+
skill_path.parent.mkdir(parents=True)
|
|
64
|
+
skill_path.write_text(_section("Plan", 7) + "\n" * 12, encoding="utf-8")
|
|
65
|
+
_git(tmp_path, "init", "-q")
|
|
66
|
+
_git(tmp_path, "add", ".")
|
|
67
|
+
_git(tmp_path, "commit", "-q", "-m", "seed")
|
|
68
|
+
return tmp_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _run(repository_root: Path, *all_arguments: str) -> tuple[int, str]:
|
|
72
|
+
stdout = io.StringIO()
|
|
73
|
+
exit_code = context_budget_check.main(list(all_arguments), repository_root, stdout)
|
|
74
|
+
return exit_code, stdout.getvalue()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_an_unchanged_listed_file_is_ok(repository_root: Path) -> None:
|
|
78
|
+
exit_code, report_text = _run(
|
|
79
|
+
repository_root, str(repository_root / SKILL_RELATIVE_PATH)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert exit_code == 0
|
|
83
|
+
assert "context-budget: OK (1 checked)" in report_text
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_head_is_the_prior_state_so_a_shrunk_listed_file_must_lower_its_entry(
|
|
87
|
+
repository_root: Path,
|
|
88
|
+
) -> None:
|
|
89
|
+
(repository_root / SKILL_RELATIVE_PATH).write_text(
|
|
90
|
+
_section("Plan", 7) + "\n" * 4, encoding="utf-8"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
exit_code, report_text = _run(
|
|
94
|
+
repository_root, str(repository_root / SKILL_RELATIVE_PATH)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert exit_code == 1
|
|
98
|
+
assert '{"lines": 12, "sections": ["Plan"]}' in report_text
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_a_new_unpointed_section_is_reported(repository_root: Path) -> None:
|
|
102
|
+
skill_path = repository_root / SKILL_RELATIVE_PATH
|
|
103
|
+
skill_path.write_text(_section("Plan", 7) + _section("Steps", 7), encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
exit_code, report_text = _run(repository_root, str(skill_path))
|
|
106
|
+
|
|
107
|
+
assert exit_code == 1
|
|
108
|
+
assert 'section "Steps" has 7 detail lines' in report_text
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_write_baseline_records_the_tree(repository_root: Path) -> None:
|
|
112
|
+
(repository_root / SKILL_RELATIVE_PATH).write_text(
|
|
113
|
+
_section("Plan", 7) + "\n" * 4, encoding="utf-8"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
exit_code, report_text = _run(repository_root, "--write-baseline")
|
|
117
|
+
|
|
118
|
+
written = json.loads(
|
|
119
|
+
(repository_root / ".claude" / "context-budget.json").read_text(
|
|
120
|
+
encoding="utf-8"
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
assert exit_code == 0 and "1 files" in report_text
|
|
124
|
+
assert written["baseline"]["files"] == {
|
|
125
|
+
SKILL_RELATIVE_PATH: {"lines": 12, "sections": ["Plan"]}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_the_committed_baseline_matches_what_the_generator_builds_today() -> None:
|
|
130
|
+
policy = context_budget_check.read_policy_file(REPOSITORY_ROOT)
|
|
131
|
+
assert policy is not None
|
|
132
|
+
committed = json.loads(
|
|
133
|
+
(REPOSITORY_ROOT / ".claude" / "context-budget.json").read_text(
|
|
134
|
+
encoding="utf-8"
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
regenerated = context_budget_check.current_baseline(policy, REPOSITORY_ROOT)
|
|
139
|
+
|
|
140
|
+
assert regenerated == committed["baseline"]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _hook_policy(repository_root: Path, all_hook_specs: list[tuple[str, str]]) -> Path:
|
|
144
|
+
all_hooks = []
|
|
145
|
+
for index, (name, source_text) in enumerate(all_hook_specs):
|
|
146
|
+
hook_path = repository_root / f"fixture_hook_{index}.py"
|
|
147
|
+
hook_path.write_text(source_text, encoding="utf-8")
|
|
148
|
+
all_hooks.append(
|
|
149
|
+
{
|
|
150
|
+
"name": name,
|
|
151
|
+
"command": [sys.executable, str(hook_path)],
|
|
152
|
+
"stdin": {},
|
|
153
|
+
"char_limit": 1500,
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
policy_path = repository_root / ".claude" / "context-budget.json"
|
|
157
|
+
policy_path.write_text(
|
|
158
|
+
json.dumps(
|
|
159
|
+
{
|
|
160
|
+
"kinds": [],
|
|
161
|
+
"hooks": all_hooks,
|
|
162
|
+
"baseline": {"files": {}, "hooks": {"sample": 2200}},
|
|
163
|
+
}
|
|
164
|
+
),
|
|
165
|
+
encoding="utf-8",
|
|
166
|
+
)
|
|
167
|
+
return policy_path
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _hook_cli(repository_root: Path, action: str) -> subprocess.CompletedProcess[str]:
|
|
171
|
+
return subprocess.run(
|
|
172
|
+
[sys.executable, context_budget_check.__file__, action],
|
|
173
|
+
cwd=repository_root,
|
|
174
|
+
capture_output=True,
|
|
175
|
+
text=True,
|
|
176
|
+
check=False,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@pytest.mark.parametrize("action", ["--hooks", "--write-baseline"])
|
|
181
|
+
def test_duplicate_hook_names_fail_without_replacing_the_policy(
|
|
182
|
+
repository_root: Path,
|
|
183
|
+
action: str,
|
|
184
|
+
) -> None:
|
|
185
|
+
policy_path = _hook_policy(
|
|
186
|
+
repository_root,
|
|
187
|
+
[
|
|
188
|
+
("sample", "print('x' * 2100)\n"),
|
|
189
|
+
("sample", "print('x')\n"),
|
|
190
|
+
],
|
|
191
|
+
)
|
|
192
|
+
prior_text = policy_path.read_bytes()
|
|
193
|
+
|
|
194
|
+
completed = _hook_cli(repository_root, action)
|
|
195
|
+
|
|
196
|
+
assert completed.returncode == 1
|
|
197
|
+
assert "hook names must be unique" in completed.stdout
|
|
198
|
+
assert policy_path.read_bytes() == prior_text
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.parametrize("action", ["--hooks", "--write-baseline"])
|
|
202
|
+
@pytest.mark.parametrize(
|
|
203
|
+
"source_text",
|
|
204
|
+
[
|
|
205
|
+
"raise RuntimeError('fixture failure')\n",
|
|
206
|
+
"print('partial output')\nraise RuntimeError('fixture failure')\n",
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
def test_failed_hook_measurement_fails_without_replacing_the_policy(
|
|
210
|
+
repository_root: Path,
|
|
211
|
+
action: str,
|
|
212
|
+
source_text: str,
|
|
213
|
+
) -> None:
|
|
214
|
+
policy_path = _hook_policy(repository_root, [("sample", source_text)])
|
|
215
|
+
prior_text = policy_path.read_bytes()
|
|
216
|
+
|
|
217
|
+
completed = _hook_cli(repository_root, action)
|
|
218
|
+
|
|
219
|
+
assert completed.returncode == 1
|
|
220
|
+
assert 'hook "sample" exited with status 1' in completed.stdout
|
|
221
|
+
assert "fixture failure" in completed.stdout
|
|
222
|
+
assert policy_path.read_bytes() == prior_text
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_unique_hook_measurements_keep_each_count(repository_root: Path) -> None:
|
|
226
|
+
_hook_policy(
|
|
227
|
+
repository_root, [("sample", "print('x' * 2100)\n"), ("short", "print('x')\n")]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
completed = _hook_cli(repository_root, "--hooks")
|
|
231
|
+
|
|
232
|
+
assert completed.returncode == 0
|
|
233
|
+
assert "sample: 2101 characters" in completed.stdout
|
|
234
|
+
assert "short: 2 characters" in completed.stdout
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_successful_empty_hook_can_remove_its_baseline(repository_root: Path) -> None:
|
|
238
|
+
policy_path = _hook_policy(repository_root, [("sample", "")])
|
|
239
|
+
|
|
240
|
+
completed = _hook_cli(repository_root, "--write-baseline")
|
|
241
|
+
|
|
242
|
+
assert completed.returncode == 0
|
|
243
|
+
assert (
|
|
244
|
+
json.loads(policy_path.read_text(encoding="utf-8"))["baseline"]["hooks"] == {}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_successful_hook_baseline_preserves_its_count(repository_root: Path) -> None:
|
|
249
|
+
policy_path = _hook_policy(repository_root, [("sample", "print('x' * 2100)\n")])
|
|
250
|
+
|
|
251
|
+
completed = _hook_cli(repository_root, "--write-baseline")
|
|
252
|
+
|
|
253
|
+
assert completed.returncode == 0
|
|
254
|
+
assert json.loads(policy_path.read_text(encoding="utf-8"))["baseline"]["hooks"] == {
|
|
255
|
+
"sample": 2101
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_a_unique_unlisted_long_hook_still_exceeds_its_limit(
|
|
260
|
+
repository_root: Path,
|
|
261
|
+
) -> None:
|
|
262
|
+
policy_path = _hook_policy(repository_root, [("sample", "print('x' * 2100)\n")])
|
|
263
|
+
raw_policy = json.loads(policy_path.read_text(encoding="utf-8"))
|
|
264
|
+
raw_policy["baseline"]["hooks"] = {}
|
|
265
|
+
policy_path.write_text(json.dumps(raw_policy), encoding="utf-8")
|
|
266
|
+
|
|
267
|
+
completed = _hook_cli(repository_root, "--hooks")
|
|
268
|
+
|
|
269
|
+
assert completed.returncode == 1
|
|
270
|
+
assert "injects 2101 characters (limit 1500)" in completed.stdout
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""The context-budget rules reach context Markdown and the policy file through the registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from policy_lint import adapters, registry
|
|
13
|
+
from policy_lint.engine import lint
|
|
14
|
+
from policy_lint.model import ContentOrigin, Document, DocumentRule, LintRequest
|
|
15
|
+
|
|
16
|
+
_SKILL_PATH = "tools/example/SKILL.md"
|
|
17
|
+
_POLICY_PATH = ".claude/context-budget.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _policy(baseline_files: dict | None = None, line_limit: int = 200) -> dict:
|
|
21
|
+
return {
|
|
22
|
+
"section_detail_line_limit": 6,
|
|
23
|
+
"kinds": [
|
|
24
|
+
{
|
|
25
|
+
"name": "skill entry",
|
|
26
|
+
"patterns": ["**/SKILL.md"],
|
|
27
|
+
"line_limit": line_limit,
|
|
28
|
+
"section_rule": True,
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"hooks": [],
|
|
32
|
+
"baseline": {"files": baseline_files or {}, "hooks": {}},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _write_policy(repository_root: Path, raw_policy: dict) -> None:
|
|
37
|
+
(repository_root / ".claude").mkdir(exist_ok=True)
|
|
38
|
+
(repository_root / _POLICY_PATH).write_text(
|
|
39
|
+
json.dumps(raw_policy), encoding="utf-8"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _document(path: str, text: str, prior_text: str | None = None) -> Document:
|
|
44
|
+
return Document(PurePosixPath(path), text, prior_text, None, ContentOrigin.WORKTREE)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _section(heading: str, detail_count: int) -> str:
|
|
48
|
+
all_lines = [
|
|
49
|
+
f"## {heading}",
|
|
50
|
+
*(f"Detail line {each}." for each in range(detail_count)),
|
|
51
|
+
]
|
|
52
|
+
return "\n".join(all_lines) + "\n"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _registered_rule(rule_id: str) -> DocumentRule:
|
|
56
|
+
for each_rule in registry.default_registry():
|
|
57
|
+
if each_rule.rule_id == rule_id and isinstance(each_rule, DocumentRule):
|
|
58
|
+
return each_rule
|
|
59
|
+
raise AssertionError(rule_id)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_both_rules_are_registered_for_changed_and_repository_selections() -> None:
|
|
63
|
+
for each_rule_id in ("context-budget", "context-budget-policy"):
|
|
64
|
+
assert _registered_rule(each_rule_id).rule_sets == frozenset(
|
|
65
|
+
{"changed", "repository"}
|
|
66
|
+
)
|
|
67
|
+
assert _registered_rule("context-budget").accepts(_document(_SKILL_PATH, ""))
|
|
68
|
+
assert _registered_rule("context-budget-policy").accepts(
|
|
69
|
+
_document(_POLICY_PATH, "{}")
|
|
70
|
+
)
|
|
71
|
+
assert not _registered_rule("context-budget-policy").accepts(
|
|
72
|
+
_document("a/b.json", "{}")
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_an_unpointed_long_section_reports_with_its_fix(tmp_path: Path) -> None:
|
|
77
|
+
_write_policy(tmp_path, _policy())
|
|
78
|
+
|
|
79
|
+
all_diagnostics = adapters.context_budget_diagnostics(
|
|
80
|
+
_document(_SKILL_PATH, _section("Plan", 7)), tmp_path
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
assert [each.rule_id for each in all_diagnostics] == ["context-budget"]
|
|
84
|
+
assert (
|
|
85
|
+
'section "Plan" has 7 detail lines and no pointer (limit 6)'
|
|
86
|
+
in all_diagnostics[0].message
|
|
87
|
+
)
|
|
88
|
+
assert "reference/plan.md" in all_diagnostics[0].message
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_a_repository_without_a_policy_file_is_not_checked(tmp_path: Path) -> None:
|
|
92
|
+
assert (
|
|
93
|
+
adapters.context_budget_diagnostics(
|
|
94
|
+
_document(_SKILL_PATH, _section("Plan", 9)), tmp_path
|
|
95
|
+
)
|
|
96
|
+
== ()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_a_changed_listed_file_below_its_record_must_lower_its_entry(
|
|
101
|
+
tmp_path: Path,
|
|
102
|
+
) -> None:
|
|
103
|
+
prior_text = _section("Plan", 7) + "\n" * 10
|
|
104
|
+
_write_policy(
|
|
105
|
+
tmp_path, _policy({_SKILL_PATH: {"lines": 18, "sections": ["Plan"]}}, 10)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
all_diagnostics = adapters.context_budget_diagnostics(
|
|
109
|
+
_document(_SKILL_PATH, _section("Plan", 7) + "\n" * 5, prior_text), tmp_path
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert len(all_diagnostics) == 1
|
|
113
|
+
assert '{"lines": 13, "sections": ["Plan"]}' in all_diagnostics[0].message
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_raising_a_baseline_entry_is_a_shrink_only_finding(tmp_path: Path) -> None:
|
|
117
|
+
prior_text = json.dumps(_policy({_SKILL_PATH: {"lines": 300, "sections": []}}))
|
|
118
|
+
current_text = json.dumps(_policy({_SKILL_PATH: {"lines": 310, "sections": []}}))
|
|
119
|
+
|
|
120
|
+
all_diagnostics = adapters.context_budget_policy_diagnostics(
|
|
121
|
+
_document(_POLICY_PATH, current_text, prior_text), tmp_path
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
assert [each.rule_id for each in all_diagnostics] == ["context-budget-policy"]
|
|
125
|
+
assert "the over-budget list may only shrink" in all_diagnostics[0].message
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_a_new_or_shrunk_policy_file_passes_and_a_malformed_one_reports(
|
|
129
|
+
tmp_path: Path,
|
|
130
|
+
) -> None:
|
|
131
|
+
prior_text = json.dumps(_policy({_SKILL_PATH: {"lines": 300, "sections": []}}))
|
|
132
|
+
shrunk_text = json.dumps(_policy())
|
|
133
|
+
|
|
134
|
+
assert (
|
|
135
|
+
adapters.context_budget_policy_diagnostics(
|
|
136
|
+
_document(_POLICY_PATH, shrunk_text, prior_text), tmp_path
|
|
137
|
+
)
|
|
138
|
+
== ()
|
|
139
|
+
)
|
|
140
|
+
assert (
|
|
141
|
+
adapters.context_budget_policy_diagnostics(
|
|
142
|
+
_document(_POLICY_PATH, shrunk_text), tmp_path
|
|
143
|
+
)
|
|
144
|
+
== ()
|
|
145
|
+
)
|
|
146
|
+
malformed = adapters.context_budget_policy_diagnostics(
|
|
147
|
+
_document(_POLICY_PATH, "{broken"), tmp_path
|
|
148
|
+
)
|
|
149
|
+
assert "does not parse" in malformed[0].message
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _git(repository_root: Path, *arguments: str) -> None:
|
|
153
|
+
environment = {
|
|
154
|
+
variable_name: variable_text
|
|
155
|
+
for variable_name, variable_text in os.environ.items()
|
|
156
|
+
if not variable_name.upper().startswith("GIT_")
|
|
157
|
+
}
|
|
158
|
+
subprocess.run(
|
|
159
|
+
["git", "-c", "core.hooksPath=.git/fixture-hooks", *arguments],
|
|
160
|
+
cwd=repository_root, env=environment,
|
|
161
|
+
check=True, capture_output=True, text=True,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_a_section_added_below_line_one_reports_in_a_base_selection(
|
|
166
|
+
tmp_path: Path,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""A base selection keeps only diagnostics on changed lines, so a finding
|
|
169
|
+
pinned to an unchanged line 1 vanished from the pull request check."""
|
|
170
|
+
_git(tmp_path, "init", "--quiet", "-b", "main")
|
|
171
|
+
_git(tmp_path, "config", "user.name", "Budget Tests")
|
|
172
|
+
_git(tmp_path, "config", "user.email", "budget@example.invalid")
|
|
173
|
+
_git(tmp_path, "config", "commit.gpgsign", "false")
|
|
174
|
+
_write_policy(tmp_path, _policy())
|
|
175
|
+
skill_path = tmp_path / _SKILL_PATH
|
|
176
|
+
skill_path.parent.mkdir(parents=True)
|
|
177
|
+
skill_path.write_text("# Example\n\nShort map.\n", encoding="utf-8")
|
|
178
|
+
_git(tmp_path, "add", ".")
|
|
179
|
+
_git(tmp_path, "commit", "--quiet", "-m", "fixture")
|
|
180
|
+
with skill_path.open("a", encoding="utf-8") as skill_file:
|
|
181
|
+
skill_file.write("\n" + _section("Oversee", 9))
|
|
182
|
+
_git(tmp_path, "commit", "--quiet", "-am", "inline a long section")
|
|
183
|
+
|
|
184
|
+
report = lint(LintRequest.base(tmp_path, "HEAD~1"))
|
|
185
|
+
|
|
186
|
+
assert [
|
|
187
|
+
each.rule_id for each in report.diagnostics if each.rule_id == "context-budget"
|
|
188
|
+
] == ["context-budget"]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@pytest.mark.parametrize("selection", ["staged", "base"])
|
|
192
|
+
@pytest.mark.parametrize("operation", ["delete", "rename"])
|
|
193
|
+
def test_removing_the_policy_is_rejected_by_git_selection(
|
|
194
|
+
tmp_path: Path, selection: str, operation: str
|
|
195
|
+
) -> None:
|
|
196
|
+
_git(tmp_path, "init", "--quiet", "-b", "main")
|
|
197
|
+
_git(tmp_path, "config", "user.name", "Budget Tests")
|
|
198
|
+
_git(tmp_path, "config", "user.email", "budget@example.invalid")
|
|
199
|
+
_git(tmp_path, "config", "commit.gpgsign", "false")
|
|
200
|
+
_write_policy(tmp_path, _policy())
|
|
201
|
+
_git(tmp_path, "add", ".")
|
|
202
|
+
_git(tmp_path, "commit", "--quiet", "-m", "fixture")
|
|
203
|
+
if operation == "delete":
|
|
204
|
+
_git(tmp_path, "rm", _POLICY_PATH)
|
|
205
|
+
else:
|
|
206
|
+
_git(tmp_path, "mv", _POLICY_PATH, ".claude/renamed-budget.json")
|
|
207
|
+
request = (
|
|
208
|
+
LintRequest.staged(tmp_path)
|
|
209
|
+
if selection == "staged"
|
|
210
|
+
else LintRequest.base(tmp_path, "HEAD")
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
report = lint(request)
|
|
214
|
+
|
|
215
|
+
findings = [each for each in report.diagnostics if each.rule_id == "context-budget-policy"]
|
|
216
|
+
assert len(findings) == 1
|
|
217
|
+
assert "policy file is removed" in findings[0].message
|
|
218
|
+
assert not report.failed_rules
|
|
@@ -594,3 +594,29 @@ def test_pairing_rejects_comment_rules_module_without_any_approved_suite(
|
|
|
594
594
|
) -> None:
|
|
595
595
|
all_paths = _diagnostic_paths(tmp_path, _body_change_at(_COMMENT_RULES_PRODUCTION_PATH))
|
|
596
596
|
assert all_paths == (_COMMENT_RULES_PRODUCTION_PATH,)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_pairing_treats_test_support_modules_as_test_files(tmp_path: Path) -> None:
|
|
600
|
+
all_test_support_paths = (
|
|
601
|
+
PurePosixPath("packages/claude-dev-env/scripts/_code_review_test_support.py"),
|
|
602
|
+
PurePosixPath("packages/claude-dev-env/scripts/codec_forwarding_test_support.py"),
|
|
603
|
+
)
|
|
604
|
+
all_paths = _diagnostic_paths(
|
|
605
|
+
tmp_path,
|
|
606
|
+
*(_body_change_at(each_path) for each_path in all_test_support_paths),
|
|
607
|
+
)
|
|
608
|
+
assert all_paths == ()
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_pairing_rejects_a_module_beside_only_its_changed_test_support(
|
|
612
|
+
tmp_path: Path,
|
|
613
|
+
) -> None:
|
|
614
|
+
production_path = PurePosixPath("packages/claude-dev-env/scripts/codec_forwarding.py")
|
|
615
|
+
all_paths = _diagnostic_paths(
|
|
616
|
+
tmp_path,
|
|
617
|
+
_body_change_at(production_path),
|
|
618
|
+
_body_change_at(
|
|
619
|
+
PurePosixPath("packages/claude-dev-env/scripts/codec_forwarding_test_support.py")
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
assert all_paths == (production_path,)
|