claude-dev-env 8.75.0 → 8.75.2
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/context-audit/scripts/context_audit_constants/config/test_constants.py +35 -0
- package/.agents/skills/context-audit/scripts/test_context_inventory.py +109 -0
- package/.agents/skills/context-audit/scripts/test_context_report.py +123 -0
- package/.agents/skills/context-audit/scripts/test_context_sources.py +118 -0
- package/bin/ever-shipped-skills.test.mjs +4 -0
- package/hooks/blocking/test_bash_post_call_dispatcher.py +16 -1
- package/hooks/blocking/test_pr_lifecycle_skill_gate.py +8 -0
- package/hooks/blocking/test_verify_before_acting.py +16 -0
- package/hooks/lifecycle/test_nested_project_hooks.py +26 -0
- package/hooks/routing/test_spawn_readiness_hook.py +14 -0
- package/hooks/routing/test_thread_spawn_pace_hook.py +12 -0
- package/hooks/session/test_skill_loaded_reminder.py +17 -0
- package/hooks/test_conftest.py +5 -0
- package/package.json +1 -1
- package/scripts/policy_lint/adapter_pairing.py +4 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +38 -0
- package/scripts/tests/test_runner.py +42 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from context_audit_constants.config import constants
|
|
4
|
+
from context_audit_constants.config.constants import CapKey, RowKind, Trigger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_every_cap_key_has_a_line_cap() -> None:
|
|
8
|
+
assert set(constants.ALL_LINE_CAP_BY_KEY) == set(CapKey)
|
|
9
|
+
assert constants.ALL_LINE_CAP_BY_KEY[CapKey.ROOT_INSTRUCTIONS] == 20
|
|
10
|
+
assert constants.ALL_LINE_CAP_BY_KEY[CapKey.SKILL_BODY] == 200
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_trigger_values_match_the_report_labels() -> None:
|
|
14
|
+
assert [each_trigger.value for each_trigger in Trigger] == [
|
|
15
|
+
"session-start",
|
|
16
|
+
"folder-enter",
|
|
17
|
+
"path-match",
|
|
18
|
+
"skill-invoke",
|
|
19
|
+
"on-link",
|
|
20
|
+
"file-open",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_stub_kinds_cover_instruction_files_only() -> None:
|
|
25
|
+
assert constants.ALL_STUB_KINDS == {
|
|
26
|
+
RowKind.INSTRUCTIONS,
|
|
27
|
+
RowKind.IMPORT,
|
|
28
|
+
RowKind.RULE,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_link_pattern_drops_the_anchor_from_a_target() -> None:
|
|
33
|
+
assert re.findall(constants.LINK_PATTERN, "[a](guide.md#setup) [b](x y)") == [
|
|
34
|
+
"guide.md"
|
|
35
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Behavior tests for walking a checkout into context rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
scripts_directory = str(Path(__file__).resolve().parent)
|
|
10
|
+
if scripts_directory not in sys.path:
|
|
11
|
+
sys.path.insert(0, scripts_directory)
|
|
12
|
+
|
|
13
|
+
from context_audit_constants.config.constants import DepthMode, RowKind, Trigger
|
|
14
|
+
from context_inventory import ContextInventory, audit_checkout
|
|
15
|
+
from context_sources import ContextRow
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _tracked_checkout(root: Path, text_by_relative_path: dict[str, str]) -> Path:
|
|
19
|
+
for each_relative_path, each_text in text_by_relative_path.items():
|
|
20
|
+
path = root / each_relative_path
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
path.write_text(each_text, encoding="utf-8")
|
|
23
|
+
subprocess.run(["git", "init", "-q"], cwd=root, check=True)
|
|
24
|
+
subprocess.run(["git", "add", "-A"], cwd=root, check=True)
|
|
25
|
+
return root.resolve()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _row_by_key(all_rows: list[ContextRow]) -> dict[tuple[str, RowKind], ContextRow]:
|
|
29
|
+
return {(each_row.path, each_row.kind): each_row for each_row in all_rows}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_should_mark_installed_rules_global_unless_path_scoped(tmp_path: Path) -> None:
|
|
33
|
+
root = _tracked_checkout(
|
|
34
|
+
tmp_path / "repo",
|
|
35
|
+
{
|
|
36
|
+
"rules/everywhere.md": "Always.\n",
|
|
37
|
+
"rules/scoped.md": "---\npaths:\n - src/**\n---\nScoped.\n",
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
row_by_key = _row_by_key(audit_checkout(root, root / "rules", None, ()))
|
|
42
|
+
|
|
43
|
+
everywhere_row = row_by_key[("rules/everywhere.md", RowKind.RULE)]
|
|
44
|
+
scoped_row = row_by_key[("rules/scoped.md", RowKind.RULE)]
|
|
45
|
+
assert (everywhere_row.trigger, everywhere_row.note) == (
|
|
46
|
+
Trigger.SESSION_START,
|
|
47
|
+
"installed rule: loads in every project",
|
|
48
|
+
)
|
|
49
|
+
assert (scoped_row.trigger, scoped_row.note) == (Trigger.PATH_MATCH, "")
|
|
50
|
+
assert everywhere_row.loader == "Claude Code harness, installed rules folder"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_skip_rule_folders_that_are_not_installed(tmp_path: Path) -> None:
|
|
54
|
+
root = _tracked_checkout(tmp_path / "repo", {"rules/everywhere.md": "Always.\n"})
|
|
55
|
+
|
|
56
|
+
assert audit_checkout(root, None, None, ()) == []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_should_list_installed_and_plugin_skills_at_session_start(
|
|
60
|
+
tmp_path: Path,
|
|
61
|
+
) -> None:
|
|
62
|
+
long_description = "x" * 1600
|
|
63
|
+
root = _tracked_checkout(
|
|
64
|
+
tmp_path / "repo",
|
|
65
|
+
{
|
|
66
|
+
"pack/skills/tidy/SKILL.md": "---\ndescription: Tidy up.\n---\nBody.\n",
|
|
67
|
+
"plugins/kit/skills/wide/SKILL.md": (
|
|
68
|
+
f"---\ndescription: {long_description}\n---\nBody.\n"
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
row_by_key = _row_by_key(audit_checkout(root, None, root / "pack" / "skills", ()))
|
|
74
|
+
|
|
75
|
+
installed_row = row_by_key[("pack/skills/tidy/SKILL.md", RowKind.SKILL_DESCRIPTION)]
|
|
76
|
+
plugin_row = row_by_key[
|
|
77
|
+
("plugins/kit/skills/wide/SKILL.md", RowKind.SKILL_DESCRIPTION)
|
|
78
|
+
]
|
|
79
|
+
assert installed_row.loader == "skill listing, installed skills folder"
|
|
80
|
+
assert installed_row.trigger == Trigger.SESSION_START
|
|
81
|
+
assert installed_row.depth_mode == DepthMode.POINTS
|
|
82
|
+
assert plugin_row.loader == "skill listing, plugin skills folder"
|
|
83
|
+
assert (plugin_row.bytes, plugin_row.depth_mode) == (1536, DepthMode.CARRIES)
|
|
84
|
+
assert plugin_row.note == (
|
|
85
|
+
"loads when the plugin is installed; listing truncates at 1,536 characters"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_should_name_how_codex_and_claude_reach_an_agents_file(tmp_path: Path) -> None:
|
|
90
|
+
root = _tracked_checkout(
|
|
91
|
+
tmp_path / "repo",
|
|
92
|
+
{
|
|
93
|
+
"AGENTS.md": "Root rules.\n",
|
|
94
|
+
"pkg/AGENTS.md": "Package rules.\n",
|
|
95
|
+
"pkg/CLAUDE.md": "Claude rules.\n",
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
row_by_key = _row_by_key(ContextInventory(root, None, None).collect(()))
|
|
100
|
+
|
|
101
|
+
assert row_by_key[("AGENTS.md", RowKind.INSTRUCTIONS)].loader == (
|
|
102
|
+
"Claude Code native AGENTS.md read; Codex reads it"
|
|
103
|
+
)
|
|
104
|
+
assert row_by_key[("pkg/AGENTS.md", RowKind.INSTRUCTIONS)].loader == (
|
|
105
|
+
"not read by Claude Code (sibling CLAUDE.md, no import); Codex reads it"
|
|
106
|
+
)
|
|
107
|
+
assert row_by_key[("pkg/AGENTS.md", RowKind.INSTRUCTIONS)].trigger == (
|
|
108
|
+
Trigger.FOLDER_ENTER
|
|
109
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Behavior tests for formatting inventory rows as a markdown report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
scripts_directory = str(Path(__file__).resolve().parent)
|
|
9
|
+
if scripts_directory not in sys.path:
|
|
10
|
+
sys.path.insert(0, scripts_directory)
|
|
11
|
+
|
|
12
|
+
from context_audit_constants.config.constants import DepthMode, RowKind, Trigger
|
|
13
|
+
from context_report import (
|
|
14
|
+
format_carries_section,
|
|
15
|
+
format_cleanup_list,
|
|
16
|
+
format_report,
|
|
17
|
+
format_split_section,
|
|
18
|
+
format_stub_section,
|
|
19
|
+
format_trigger_table,
|
|
20
|
+
)
|
|
21
|
+
from context_sources import ContextRow
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _row(
|
|
25
|
+
path: str,
|
|
26
|
+
kind: RowKind,
|
|
27
|
+
trigger: Trigger,
|
|
28
|
+
depth_mode: DepthMode,
|
|
29
|
+
lines: int,
|
|
30
|
+
est_tokens: int,
|
|
31
|
+
) -> ContextRow:
|
|
32
|
+
return ContextRow(
|
|
33
|
+
path=path,
|
|
34
|
+
kind=kind,
|
|
35
|
+
loader="harness",
|
|
36
|
+
trigger=trigger,
|
|
37
|
+
bytes=est_tokens * 4,
|
|
38
|
+
lines=lines,
|
|
39
|
+
est_tokens=est_tokens,
|
|
40
|
+
links_out=0,
|
|
41
|
+
depth_mode=depth_mode,
|
|
42
|
+
cap=20,
|
|
43
|
+
note="",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_ROOT_ROW = _row(
|
|
48
|
+
"AGENTS.md", RowKind.INSTRUCTIONS, Trigger.SESSION_START, DepthMode.CARRIES, 30, 300
|
|
49
|
+
)
|
|
50
|
+
_RULE_ROW = _row(
|
|
51
|
+
"rules/big.md", RowKind.RULE, Trigger.SESSION_START, DepthMode.CARRIES, 40, 500
|
|
52
|
+
)
|
|
53
|
+
_STUB_ROW = _row(
|
|
54
|
+
"pkg/CLAUDE.md", RowKind.INSTRUCTIONS, Trigger.FOLDER_ENTER, DepthMode.EMPTY, 0, 0
|
|
55
|
+
)
|
|
56
|
+
_SKILL_ROW = _row(
|
|
57
|
+
"skills/x/SKILL.md",
|
|
58
|
+
RowKind.SKILL_BODY,
|
|
59
|
+
Trigger.SKILL_INVOKE,
|
|
60
|
+
DepthMode.CARRIES,
|
|
61
|
+
250,
|
|
62
|
+
900,
|
|
63
|
+
)
|
|
64
|
+
_EMPTY_DOCSTRING_ROW = _row(
|
|
65
|
+
"src/mod.py", RowKind.DOCSTRING, Trigger.FILE_OPEN, DepthMode.EMPTY, 0, 0
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_total_each_present_trigger_in_enum_order() -> None:
|
|
70
|
+
all_lines = format_trigger_table([_STUB_ROW, _ROOT_ROW, _RULE_ROW])
|
|
71
|
+
|
|
72
|
+
assert all_lines[4:] == [
|
|
73
|
+
"| session-start | 2 | 70 | 800 |",
|
|
74
|
+
"| folder-enter | 1 | 0 | 0 |",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_should_list_carries_rows_or_say_none() -> None:
|
|
79
|
+
assert format_carries_section([])[-1] == "None."
|
|
80
|
+
assert format_carries_section([_RULE_ROW])[-1] == (
|
|
81
|
+
"| rules/big.md | rule | session-start | 40 | 20 | 500 |"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_should_list_stub_rows_or_say_none() -> None:
|
|
86
|
+
assert format_stub_section([])[-1] == "None."
|
|
87
|
+
assert format_stub_section([_STUB_ROW])[-1] == "- pkg/CLAUDE.md (instructions)"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_should_list_long_skills_or_say_none() -> None:
|
|
91
|
+
assert format_split_section([])[-1] == "None."
|
|
92
|
+
assert format_split_section([_SKILL_ROW])[-1] == (
|
|
93
|
+
"| skills/x/SKILL.md | 250 | 20 | 900 |"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_should_sort_cleanup_rows_largest_first_and_route_each_kind() -> None:
|
|
98
|
+
all_lines = format_cleanup_list(
|
|
99
|
+
[_ROOT_ROW, _RULE_ROW, _STUB_ROW, _SKILL_ROW, _EMPTY_DOCSTRING_ROW]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
carries_start = all_lines.index("## Carries depth over cap")
|
|
103
|
+
stubs_start = all_lines.index("## Empty instruction stubs")
|
|
104
|
+
all_carries_rows = all_lines[carries_start + 4 : stubs_start - 1]
|
|
105
|
+
assert all_carries_rows == [
|
|
106
|
+
"| rules/big.md | rule | session-start | 40 | 20 | 500 |",
|
|
107
|
+
"| AGENTS.md | instructions | session-start | 30 | 20 | 300 |",
|
|
108
|
+
]
|
|
109
|
+
assert "- pkg/CLAUDE.md (instructions)" in all_lines
|
|
110
|
+
assert not any("src/mod.py" in each_line for each_line in all_lines)
|
|
111
|
+
assert "| skills/x/SKILL.md | 250 | 20 | 900 |" in all_lines
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_should_summarize_session_start_load_in_the_report() -> None:
|
|
115
|
+
report = format_report("demo", [_ROOT_ROW, _RULE_ROW, _STUB_ROW])
|
|
116
|
+
|
|
117
|
+
assert report.startswith(
|
|
118
|
+
"# Context audit: demo\n\n"
|
|
119
|
+
"3 loaded files. Startup loads 70 lines, about 800 tokens, from 2 files.\n"
|
|
120
|
+
)
|
|
121
|
+
assert report.endswith(
|
|
122
|
+
"- pkg/CLAUDE.md (instructions)\n\n## Long skills to split\n\nNone.\n"
|
|
123
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Behavior tests for reading checkout files and measuring context rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
scripts_directory = str(Path(__file__).resolve().parent)
|
|
10
|
+
if scripts_directory not in sys.path:
|
|
11
|
+
sys.path.insert(0, scripts_directory)
|
|
12
|
+
|
|
13
|
+
from context_audit_constants.config.constants import DepthMode, RowKind, Trigger
|
|
14
|
+
from context_sources import (
|
|
15
|
+
RowSource,
|
|
16
|
+
classify_depth,
|
|
17
|
+
count_lines,
|
|
18
|
+
find_import_targets,
|
|
19
|
+
find_relative_links,
|
|
20
|
+
list_tracked_files,
|
|
21
|
+
measure_row,
|
|
22
|
+
parse_frontmatter,
|
|
23
|
+
read_text,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write(root: Path, relative_path: str, text: str) -> Path:
|
|
28
|
+
path = root / relative_path
|
|
29
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
path.write_text(text, encoding="utf-8")
|
|
31
|
+
return path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_should_list_tracked_files_without_cache_folders_or_untracked_files(
|
|
35
|
+
tmp_path: Path,
|
|
36
|
+
) -> None:
|
|
37
|
+
subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True)
|
|
38
|
+
_write(tmp_path, "AGENTS.md", "# Root\n")
|
|
39
|
+
_write(tmp_path, "node_modules/pkg/README.md", "vendored\n")
|
|
40
|
+
_write(tmp_path, "notes.md", "untracked\n")
|
|
41
|
+
subprocess.run(
|
|
42
|
+
["git", "add", "-f", "AGENTS.md", "node_modules/pkg/README.md"],
|
|
43
|
+
cwd=tmp_path,
|
|
44
|
+
check=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert list_tracked_files(tmp_path) == [tmp_path / "AGENTS.md"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_read_text_and_return_empty_for_a_missing_file(tmp_path: Path) -> None:
|
|
51
|
+
present_path = _write(tmp_path, "rule.md", "line one\n")
|
|
52
|
+
|
|
53
|
+
assert read_text(present_path) == "line one\n"
|
|
54
|
+
assert read_text(tmp_path / "absent.md") == ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_should_parse_frontmatter_and_join_continuation_lines() -> None:
|
|
58
|
+
text = "---\nname: demo\npaths:\n - src/**\n---\n# Body\n"
|
|
59
|
+
|
|
60
|
+
assert parse_frontmatter(text) == {"name": "demo", "paths": "- src/**"}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_return_no_frontmatter_when_the_fence_never_closes() -> None:
|
|
64
|
+
assert parse_frontmatter("---\nname: demo\n# Body\n") == {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_should_find_links_inside_the_checkout_only(tmp_path: Path) -> None:
|
|
68
|
+
source = _write(tmp_path, "docs/index.md", "")
|
|
69
|
+
guide = _write(tmp_path, "docs/guide.md", "guide\n")
|
|
70
|
+
text = (
|
|
71
|
+
"[guide](guide.md#setup) [web](https://example.com/page.md)"
|
|
72
|
+
" [missing](absent.md)"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert find_relative_links(source, text, tmp_path.resolve()) == [guide.resolve()]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_should_find_import_targets_that_exist(tmp_path: Path) -> None:
|
|
79
|
+
source = _write(tmp_path, "CLAUDE.md", "")
|
|
80
|
+
agents = _write(tmp_path, "AGENTS.md", "rules\n")
|
|
81
|
+
|
|
82
|
+
all_targets = find_import_targets(
|
|
83
|
+
source, "\n".join(["@AGENTS.md", "@missing.md", ""]), tmp_path.resolve()
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert all_targets == [agents.resolve()]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_should_classify_each_depth_mode() -> None:
|
|
90
|
+
assert classify_depth(20, 0, 0, 0) == DepthMode.EMPTY
|
|
91
|
+
assert classify_depth(None, 900, 30, 0) == DepthMode.REFERENCE
|
|
92
|
+
assert classify_depth(20, 4000, 64, 2) == DepthMode.CARRIES
|
|
93
|
+
assert classify_depth(20, 2500, 10, 0) == DepthMode.CARRIES
|
|
94
|
+
assert classify_depth(20, 600, 12, 3) == DepthMode.POINTS
|
|
95
|
+
assert classify_depth(20, 600, 12, 0) == DepthMode.LEAN
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_should_count_an_unterminated_final_line() -> None:
|
|
99
|
+
assert count_lines("") == 0
|
|
100
|
+
assert count_lines("one\ntwo\n") == 2
|
|
101
|
+
assert count_lines("one\ntwo") == 2
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_should_measure_a_row_from_its_text() -> None:
|
|
105
|
+
source = RowSource(
|
|
106
|
+
"rules/plain.md", RowKind.RULE, "harness", Trigger.SESSION_START, "note"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
row = measure_row(source, "a" * 39 + "\n", 20, 1)
|
|
110
|
+
|
|
111
|
+
assert (row.path, row.kind, row.trigger, row.note) == (
|
|
112
|
+
"rules/plain.md",
|
|
113
|
+
RowKind.RULE,
|
|
114
|
+
Trigger.SESSION_START,
|
|
115
|
+
"note",
|
|
116
|
+
)
|
|
117
|
+
assert (row.bytes, row.lines, row.est_tokens, row.links_out) == (40, 1, 10, 1)
|
|
118
|
+
assert (row.depth_mode, row.cap) == (DepthMode.POINTS, 20)
|
|
@@ -53,6 +53,10 @@ test('EVER_SHIPPED_SKILL_NAMES includes the windows scheduled task skill', () =>
|
|
|
53
53
|
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('windows-scheduled-task'), true);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test('EVER_SHIPPED_SKILL_NAMES includes the mod playbooks skill', () => {
|
|
57
|
+
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('mod-playbooks'), true);
|
|
58
|
+
});
|
|
59
|
+
|
|
56
60
|
test('build-eval ships under its matching name without the retired active directory', () => {
|
|
57
61
|
const sourceSkillsDirectory = join(
|
|
58
62
|
PACKAGE_DIRECTORY,
|
|
@@ -95,9 +95,24 @@ def test_dispatch_writes_nothing_when_no_hosted_hook_printed_context(
|
|
|
95
95
|
assert capsys.readouterr().out == ""
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def test_additional_context_from_hook_output_reads_context_under_hook_specific_output() -> None:
|
|
99
|
+
captured_stdout = json.dumps(
|
|
100
|
+
{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": "PR #7"}}
|
|
101
|
+
)
|
|
102
|
+
assert additional_context_from_hook_output(captured_stdout) == "PR #7"
|
|
103
|
+
|
|
104
|
+
|
|
98
105
|
@pytest.mark.parametrize(
|
|
99
106
|
"captured_stdout",
|
|
100
|
-
[
|
|
107
|
+
[
|
|
108
|
+
"",
|
|
109
|
+
" ",
|
|
110
|
+
"not json",
|
|
111
|
+
"[]",
|
|
112
|
+
'{"decision": "block"}',
|
|
113
|
+
'{"hookSpecificOutput": {}}',
|
|
114
|
+
'{"additionalContext": "PR #7"}',
|
|
115
|
+
],
|
|
101
116
|
)
|
|
102
117
|
def test_additional_context_from_hook_output_ignores_non_context_output(
|
|
103
118
|
captured_stdout: str,
|
|
@@ -140,6 +140,14 @@ def test_unreadable_or_missing_transcript_allows_silently(tmp_path: Path) -> Non
|
|
|
140
140
|
assert _run_main(payload) == (0, "")
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def test_unreadable_agent_transcript_beside_an_unloaded_session_allows_silently(
|
|
144
|
+
tmp_path: Path,
|
|
145
|
+
) -> None:
|
|
146
|
+
payload = _payload("git push", _transcript(tmp_path))
|
|
147
|
+
payload["agent_transcript_path"] = str(tmp_path / "missing-agent.jsonl")
|
|
148
|
+
assert _run_main(payload) == (0, "")
|
|
149
|
+
|
|
150
|
+
|
|
143
151
|
def test_action_command_shapes(tmp_path: Path) -> None:
|
|
144
152
|
path = _transcript(tmp_path)
|
|
145
153
|
commands = (
|
|
@@ -653,3 +653,19 @@ def test_should_keep_the_hedge_word_when_trimming_a_long_sentence(
|
|
|
653
653
|
assert quoted_sentence.startswith("...")
|
|
654
654
|
assert quoted_sentence.endswith("...")
|
|
655
655
|
assert len(quoted_sentence) <= 166
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@pytest.mark.parametrize(
|
|
659
|
+
"stdin_text",
|
|
660
|
+
["not json", "[]", json.dumps({"tool_input": WRITE_INPUT, "tool_use_id": TOOL_USE_ID})],
|
|
661
|
+
)
|
|
662
|
+
def test_should_allow_silently_when_the_hook_input_names_no_tool(
|
|
663
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
664
|
+
capsys: pytest.CaptureFixture[str],
|
|
665
|
+
tmp_path: Path,
|
|
666
|
+
stdin_text: str,
|
|
667
|
+
) -> None:
|
|
668
|
+
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(stdin_text.encode("utf-8"))))
|
|
669
|
+
exit_code = verify_before_acting.main()
|
|
670
|
+
assert (exit_code, capsys.readouterr().out) == (0, "")
|
|
671
|
+
assert logged_outcomes(tmp_path) == []
|
|
@@ -115,6 +115,32 @@ def test_should_carry_child_session_start_output_as_additional_context(
|
|
|
115
115
|
assert additional_context.index("alpha") < additional_context.index("beta")
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def test_should_carry_only_the_context_of_a_child_session_start_json_output(
|
|
119
|
+
tmp_path: pathlib.Path,
|
|
120
|
+
) -> None:
|
|
121
|
+
child_output = {
|
|
122
|
+
"hookSpecificOutput": {
|
|
123
|
+
"hookEventName": "SessionStart",
|
|
124
|
+
"additionalContext": "alpha loaded its rules",
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
_make_checkout(
|
|
128
|
+
tmp_path,
|
|
129
|
+
"alpha",
|
|
130
|
+
{"SessionStart": [_command_group("", f"echo '{json.dumps(child_output)}'")]},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
completed_process = _run_hook(tmp_path, _session_start_payload())
|
|
134
|
+
|
|
135
|
+
assert completed_process.returncode == 0
|
|
136
|
+
assert json.loads(completed_process.stdout) == {
|
|
137
|
+
"hookSpecificOutput": {
|
|
138
|
+
"hookEventName": "SessionStart",
|
|
139
|
+
"additionalContext": "alpha loaded its rules",
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
118
144
|
def test_should_block_a_tool_call_when_a_child_hook_exits_two(
|
|
119
145
|
tmp_path: pathlib.Path,
|
|
120
146
|
) -> None:
|
|
@@ -317,6 +317,20 @@ def test_should_read_a_workflow_script_from_its_path(tmp_path: Path) -> None:
|
|
|
317
317
|
assert log_record["outcome"] == "scope_settled"
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
def test_should_remind_a_workflow_whose_script_path_cannot_be_read(tmp_path: Path) -> None:
|
|
321
|
+
stdout = _run_hook(
|
|
322
|
+
tmp_path,
|
|
323
|
+
{
|
|
324
|
+
"tool_name": "Workflow",
|
|
325
|
+
"tool_input": {"scriptPath": str(tmp_path / "absent.js")},
|
|
326
|
+
"transcript_path": str(_write_transcript(tmp_path, [_user("Fix it."), READ_STEP])),
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
assert _reminder(stdout) == MISSING_INTERVIEW_REASON
|
|
330
|
+
[log_record] = _decision_log(tmp_path)
|
|
331
|
+
assert log_record["outcome"] == "reminded"
|
|
332
|
+
|
|
333
|
+
|
|
320
334
|
@pytest.mark.parametrize(
|
|
321
335
|
"tool_input",
|
|
322
336
|
[
|
|
@@ -94,6 +94,18 @@ def test_should_name_no_sonnet_model_in_the_reshaped_spawn(tmp_path: Path) -> No
|
|
|
94
94
|
assert "sonnet" not in stdout.lower()
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def test_should_quote_a_capped_pace_verdict_in_the_reshape_context(tmp_path: Path) -> None:
|
|
98
|
+
long_verdict = {"over_pace": True, "detail": "x" * 1000}
|
|
99
|
+
decision = _hook_specific_output(
|
|
100
|
+
_run_hook(_write_pace_stub(tmp_path, 0, long_verdict), SPAWN_INPUT)
|
|
101
|
+
)
|
|
102
|
+
context_prefix = (
|
|
103
|
+
"Usage over pace or unreadable: this thread spawn now runs on "
|
|
104
|
+
"claude-opus-5-5 at low effort. Pace verdict: "
|
|
105
|
+
)
|
|
106
|
+
assert decision["additionalContext"] == context_prefix + json.dumps(long_verdict)[:600]
|
|
107
|
+
|
|
108
|
+
|
|
97
109
|
@pytest.mark.parametrize(
|
|
98
110
|
("tool_input", "reason"),
|
|
99
111
|
[
|
|
@@ -291,6 +291,23 @@ class TestReminderFor:
|
|
|
291
291
|
== COMPACTION_REMINDER
|
|
292
292
|
)
|
|
293
293
|
|
|
294
|
+
def test_session_start_after_compaction_reminds_a_session_that_reloaded_the_skill(
|
|
295
|
+
self, tmp_path: Path
|
|
296
|
+
) -> None:
|
|
297
|
+
transcript_path = _write_transcript(
|
|
298
|
+
tmp_path, [SKILL_CALL_LINE, COMPACT_BOUNDARY_LINE, SKILL_CALL_LINE]
|
|
299
|
+
)
|
|
300
|
+
assert (
|
|
301
|
+
reminder.reminder_for(
|
|
302
|
+
{
|
|
303
|
+
"hook_event_name": "SessionStart",
|
|
304
|
+
"source": "compact",
|
|
305
|
+
"transcript_path": str(transcript_path),
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
== COMPACTION_REMINDER
|
|
309
|
+
)
|
|
310
|
+
|
|
294
311
|
def test_session_start_after_compaction_without_a_prior_skill_call_prints_nothing(
|
|
295
312
|
self, tmp_path: Path
|
|
296
313
|
) -> None:
|
package/hooks/test_conftest.py
CHANGED
|
@@ -9,12 +9,17 @@ import sys
|
|
|
9
9
|
from hooks_constants.harness_scratchpad_constants import (
|
|
10
10
|
CLAUDE_SESSION_ID_ENVIRONMENT_VARIABLE_NAME,
|
|
11
11
|
)
|
|
12
|
+
from hooks_constants.headless_claude_broker_gate_constants import CLOUD_SESSION_ENV_VAR
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def test_live_session_id_is_absent_inside_each_test() -> None:
|
|
15
16
|
assert CLAUDE_SESSION_ID_ENVIRONMENT_VARIABLE_NAME not in os.environ
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
def test_cloud_session_marker_is_absent_inside_each_test() -> None:
|
|
20
|
+
assert CLOUD_SESSION_ENV_VAR not in os.environ
|
|
21
|
+
|
|
22
|
+
|
|
18
23
|
def test_hook_subprocess_inherits_no_live_session_id() -> None:
|
|
19
24
|
completed = subprocess.run(
|
|
20
25
|
[
|
package/package.json
CHANGED
|
@@ -300,6 +300,10 @@ def _is_unpaired_production(
|
|
|
300
300
|
return False
|
|
301
301
|
if _is_test_path(production_path):
|
|
302
302
|
return False
|
|
303
|
+
if production_path.name in constants.ALL_PAIRING_EXEMPT_FILE_NAMES:
|
|
304
|
+
return False
|
|
305
|
+
if not production_document.text.strip():
|
|
306
|
+
return False
|
|
303
307
|
if _is_constants_only_python_document(production_document, load_module):
|
|
304
308
|
return False
|
|
305
309
|
if _is_docstring_only_python_document(production_document):
|
|
@@ -198,6 +198,7 @@ ALL_INVENTORY_FILE_NAMES = frozenset(
|
|
|
198
198
|
{"CHANGELOG.md", "README.md", "ever-shipped-skills.mjs"}
|
|
199
199
|
)
|
|
200
200
|
ALL_UNCALLED_EXEMPT_FILE_NAMES = frozenset({"__init__.py", "conftest.py"})
|
|
201
|
+
ALL_PAIRING_EXEMPT_FILE_NAMES = frozenset({"conftest.py"})
|
|
201
202
|
CALLER_NAME_PATTERN_TEMPLATE = r"(?<![\w-]){name}(?![\w-])"
|
|
202
203
|
|
|
203
204
|
CONTEXT_BUDGET_RULE_ID = "context-budget"
|
|
@@ -149,6 +149,44 @@ def test_pairing_keeps_non_python_changes_on_existing_matching_rules(
|
|
|
149
149
|
assert len(all_diagnostics) == 1
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
def test_pairing_accepts_a_new_empty_package_marker(tmp_path: Path) -> None:
|
|
153
|
+
package_marker_path = PurePosixPath("src/feature_constants/__init__.py")
|
|
154
|
+
all_paths = _diagnostic_paths(
|
|
155
|
+
tmp_path,
|
|
156
|
+
Document(
|
|
157
|
+
package_marker_path, "", None, frozenset(), ContentOrigin.REVISION_DIFF
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
assert all_paths == ()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_pairing_accepts_a_module_emptied_of_its_body(tmp_path: Path) -> None:
|
|
164
|
+
all_paths = _diagnostic_paths(
|
|
165
|
+
tmp_path,
|
|
166
|
+
Document(
|
|
167
|
+
PurePosixPath("src/feature.py"),
|
|
168
|
+
"\n",
|
|
169
|
+
_BODY_BEFORE,
|
|
170
|
+
frozenset({1}),
|
|
171
|
+
ContentOrigin.REVISION_DIFF,
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
assert all_paths == ()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_pairing_accepts_a_conftest_body_change(tmp_path: Path) -> None:
|
|
178
|
+
all_paths = _diagnostic_paths(
|
|
179
|
+
tmp_path, _body_change_at(PurePosixPath("src/conftest.py"))
|
|
180
|
+
)
|
|
181
|
+
assert all_paths == ()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_pairing_rejects_a_package_marker_with_a_body(tmp_path: Path) -> None:
|
|
185
|
+
package_marker_path = PurePosixPath("src/feature/__init__.py")
|
|
186
|
+
all_paths = _diagnostic_paths(tmp_path, _body_change_at(package_marker_path))
|
|
187
|
+
assert all_paths == (package_marker_path,)
|
|
188
|
+
|
|
189
|
+
|
|
152
190
|
_APPROVED_PRODUCTION_PATH = PurePosixPath(
|
|
153
191
|
"packages/claude-dev-env/scripts/automatic_advisory/state.py"
|
|
154
192
|
)
|
|
@@ -12,10 +12,17 @@ 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
|
+
ALL_CHECK_IDS,
|
|
16
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
17
|
+
CHECK_ID_ENV_VAR_DOCUMENTATION,
|
|
18
|
+
CHECK_ID_PACKAGE_INVENTORY,
|
|
19
|
+
CHECK_ID_PYTEST_TESTPATHS,
|
|
20
|
+
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
15
21
|
CHECK_ID_TRACKED_PRIVATE_TERMS,
|
|
16
22
|
FAILED_CHECK_EXIT_CODE,
|
|
17
23
|
FINDINGS_EXIT_CODE,
|
|
18
24
|
)
|
|
25
|
+
from repository_checks.runner import run_repository_checks
|
|
19
26
|
from repository_policy_test_support import (
|
|
20
27
|
commit_tracked_files,
|
|
21
28
|
initialize_repository,
|
|
@@ -77,3 +84,38 @@ def test_should_fail_closed_when_the_private_term_check_cannot_read_a_file(
|
|
|
77
84
|
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
78
85
|
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
79
86
|
assert f"error: rule failed: {CHECK_ID_TRACKED_PRIVATE_TERMS}" in stdout_text
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_COLLECTOR_NAME_BY_CHECK_ID = {
|
|
90
|
+
CHECK_ID_CLAUDE_MD_ORPHANS: "collect_claude_md_orphan_findings",
|
|
91
|
+
CHECK_ID_ENV_VAR_DOCUMENTATION: "collect_env_var_documentation_findings",
|
|
92
|
+
CHECK_ID_PACKAGE_INVENTORY: "collect_package_inventory_findings",
|
|
93
|
+
CHECK_ID_PYTEST_TESTPATHS: "collect_pytest_testpath_findings",
|
|
94
|
+
CHECK_ID_TRACKED_PERSONAL_DATA: "collect_tracked_secret_findings",
|
|
95
|
+
CHECK_ID_TRACKED_PRIVATE_TERMS: "collect_tracked_private_term_findings",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_name_a_collector_for_every_check_id() -> None:
|
|
100
|
+
assert tuple(_COLLECTOR_NAME_BY_CHECK_ID) == ALL_CHECK_IDS
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.parametrize(
|
|
104
|
+
("check_id", "collector_name"), sorted(_COLLECTOR_NAME_BY_CHECK_ID.items())
|
|
105
|
+
)
|
|
106
|
+
def test_should_fail_closed_under_the_check_id_of_the_collector_that_raised(
|
|
107
|
+
tmp_path: Path,
|
|
108
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
109
|
+
check_id: str,
|
|
110
|
+
collector_name: str,
|
|
111
|
+
) -> None:
|
|
112
|
+
repository_root = seed_clean_repository(tmp_path / "repo")
|
|
113
|
+
|
|
114
|
+
def fail_collector(_repository_root: Path, _all_tracked_paths: object) -> list[object]:
|
|
115
|
+
raise OSError("collector failed")
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(f"repository_checks.runner.{collector_name}", fail_collector)
|
|
118
|
+
|
|
119
|
+
report = run_repository_checks(repository_root)
|
|
120
|
+
|
|
121
|
+
assert report.all_failed_check_ids == (check_id,)
|