claude-dev-env 8.96.7 → 8.97.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/bin/install-pstack-plugin.mjs +31 -4
- package/bin/install-pstack-plugin.test.mjs +23 -0
- package/bin/install.prompt-context.test.mjs +2 -1
- package/hooks/blocking/send_user_file_worktree_root_gate.py +143 -0
- package/hooks/blocking/test_send_user_file_worktree_root_gate.py +126 -0
- package/hooks/features/conduct-gates.md +3 -0
- package/hooks/features/session-context.md +4 -0
- package/hooks/hooks.json +20 -0
- package/hooks/hooks_constants/plugin_session_baseline_constants.py +50 -0
- package/hooks/hooks_constants/send_user_file_worktree_root_gate_constants.py +36 -0
- package/hooks/plugin_session_baseline.py +190 -0
- package/hooks/session/plugin_change_warning.py +90 -0
- package/hooks/session/plugin_session_baseline_writer.py +53 -0
- package/hooks/session/test_plugin_change_warning.py +216 -0
- package/hooks/session/test_plugin_session_baseline_writer.py +136 -0
- package/hooks/test_context_budget_hooks.py +2 -0
- package/hooks/test_plugin_session_baseline.py +98 -0
- package/package.json +1 -1
- package/scripts/policy_lint/adapter_configuration.py +5 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/tests/test_adapter_configuration.py +8 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Behavior tests for the plugin session baseline record."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
import plugin_session_baseline
|
|
11
|
+
|
|
12
|
+
PLUGIN_ID = "pstack@pstack-claude"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def isolated_temp_folder(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
17
|
+
monkeypatch.setattr(
|
|
18
|
+
plugin_session_baseline.tempfile, "gettempdir", lambda: str(tmp_path)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_installed_plugins_path_should_follow_the_config_directory(
|
|
23
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
24
|
+
) -> None:
|
|
25
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path))
|
|
26
|
+
assert plugin_session_baseline.installed_plugins_path() == (
|
|
27
|
+
tmp_path / "plugins" / "installed_plugins.json"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_baseline_record_path_should_name_the_session_in_the_temp_folder(
|
|
32
|
+
tmp_path: Path,
|
|
33
|
+
) -> None:
|
|
34
|
+
assert plugin_session_baseline.baseline_record_path("a/b c") == (
|
|
35
|
+
tmp_path / "plugin-session-baseline-abc.json"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_current_plugin_snapshot_should_keep_only_version_and_install_path(
|
|
40
|
+
tmp_path: Path,
|
|
41
|
+
) -> None:
|
|
42
|
+
record_path = tmp_path / "installed_plugins.json"
|
|
43
|
+
record_path.write_text(
|
|
44
|
+
json.dumps(
|
|
45
|
+
{
|
|
46
|
+
"plugins": {
|
|
47
|
+
PLUGIN_ID: [
|
|
48
|
+
{
|
|
49
|
+
"scope": "user",
|
|
50
|
+
"installPath": "/cache/pstack/1.0.0",
|
|
51
|
+
"version": "1.0.0",
|
|
52
|
+
"lastUpdated": "2026-10-21T21:26:12.000Z",
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
)
|
|
60
|
+
assert plugin_session_baseline.current_plugin_snapshot(record_path) == {
|
|
61
|
+
PLUGIN_ID: [{"version": "1.0.0", "installPath": "/cache/pstack/1.0.0"}]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_current_plugin_snapshot_should_read_a_malformed_record_as_empty(
|
|
66
|
+
tmp_path: Path,
|
|
67
|
+
) -> None:
|
|
68
|
+
record_path = tmp_path / "installed_plugins.json"
|
|
69
|
+
record_path.write_text("{not json", encoding="utf-8")
|
|
70
|
+
assert plugin_session_baseline.current_plugin_snapshot(record_path) == {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_read_baseline_record_should_return_what_the_writer_stored() -> None:
|
|
74
|
+
snapshot = {PLUGIN_ID: [{"version": "1.0.0", "installPath": "/cache/pstack/1.0.0"}]}
|
|
75
|
+
plugin_session_baseline.write_baseline_record("session", snapshot, [PLUGIN_ID])
|
|
76
|
+
assert plugin_session_baseline.read_baseline_record("session") == {
|
|
77
|
+
"plugins": snapshot,
|
|
78
|
+
"warned": [PLUGIN_ID],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_all_changed_plugin_ids_should_name_changed_and_new_plugins_only() -> None:
|
|
83
|
+
unchanged_entries = [{"version": "1.0.0", "installPath": "/cache/a/1.0.0"}]
|
|
84
|
+
baseline_snapshot = {
|
|
85
|
+
"a@m": unchanged_entries,
|
|
86
|
+
"b@m": [{"version": "1.0.0", "installPath": "/cache/b/1.0.0"}],
|
|
87
|
+
}
|
|
88
|
+
current_snapshot = {
|
|
89
|
+
"a@m": unchanged_entries,
|
|
90
|
+
"b@m": [{"version": "2.0.0", "installPath": "/cache/b/2.0.0"}],
|
|
91
|
+
"c@m": [{"version": "1.0.0", "installPath": "/cache/c/1.0.0"}],
|
|
92
|
+
}
|
|
93
|
+
assert plugin_session_baseline.all_changed_plugin_ids(
|
|
94
|
+
baseline_snapshot, current_snapshot
|
|
95
|
+
) == [
|
|
96
|
+
"b@m",
|
|
97
|
+
"c@m",
|
|
98
|
+
]
|
package/package.json
CHANGED
|
@@ -136,6 +136,7 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
136
136
|
hooks/blocking/verify_before_acting.py -> exempt
|
|
137
137
|
hooks/blocking/session_title_format_gate.py -> exempt
|
|
138
138
|
hooks/blocking/artifact_dark_mode_gate.py -> exempt
|
|
139
|
+
hooks/blocking/send_user_file_worktree_root_gate.py -> exempt
|
|
139
140
|
hooks/blocking/build_eval_before_eval_write_gate.py -> exempt
|
|
140
141
|
hooks/blocking/merge_over_review_offer_gate.py -> exempt
|
|
141
142
|
hooks/blocking/final_reply_gate.py -> exempt
|
|
@@ -174,6 +175,10 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
174
175
|
The artifact page gate denies a page publish whose text is unreadable in
|
|
175
176
|
dark mode. It decides how a page renders and no code or safety policy.
|
|
176
177
|
|
|
178
|
+
The send user file gate denies a SendUserFile path outside the session's
|
|
179
|
+
working directory, which the desktop file pane cannot open. It decides
|
|
180
|
+
where a sent file sits and no code or safety policy.
|
|
181
|
+
|
|
177
182
|
The eval write gate denies a write to an eval file until the session
|
|
178
183
|
invokes ``/claude-api build-eval``. It holds eval writes until a skill
|
|
179
184
|
loads, and it judges no code or safety content.
|
|
@@ -74,6 +74,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
|
74
74
|
"blocking/session_title_format_gate.py",
|
|
75
75
|
"blocking/image_read_size_gate.py",
|
|
76
76
|
"blocking/artifact_dark_mode_gate.py",
|
|
77
|
+
"blocking/send_user_file_worktree_root_gate.py",
|
|
77
78
|
"blocking/haiku_spawn_slim_gate.py",
|
|
78
79
|
"blocking/build_eval_before_eval_write_gate.py",
|
|
79
80
|
"blocking/merge_over_review_offer_gate.py",
|
|
@@ -154,6 +154,14 @@ def test_new_artifact_page_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
|
154
154
|
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
def test_new_send_user_file_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
158
|
+
current_document = _hook_document(
|
|
159
|
+
["hooks/blocking/send_user_file_worktree_root_gate.py"],
|
|
160
|
+
'{"hooks": {}}',
|
|
161
|
+
)
|
|
162
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
163
|
+
|
|
164
|
+
|
|
157
165
|
def test_new_eval_write_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
158
166
|
current_document = _hook_document(
|
|
159
167
|
["hooks/blocking/build_eval_before_eval_write_gate.py"],
|