claude-dev-env 8.75.0 → 8.75.1
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/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
|
@@ -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
|
[
|