Skip to content

Commit 0a78d43

Browse files
committed
Improve HookJSONOutput tests: fix misleading comment and add comprehensive validation tests
- Fix misleading comment in test_hook_json_output_import method that claimed to test import when import already occurred at module level - Rename method to test_hook_json_output_basic_usage with accurate docstring - Add comprehensive validation tests covering all HookJSONOutput fields and combinations - Add tests for type constraints and runtime behavior with TypedDict - Add integration tests demonstrating real hook usage patterns (PreToolUse, SessionStart, UserPromptSubmit) - Add tests for invalid field names and edge cases - All tests pass and maintain backward compatibility
1 parent 0dbd99d commit 0a78d43

1 file changed

Lines changed: 185 additions & 3 deletions

File tree

tests/test_types.py

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for Claude SDK type definitions."""
22

3+
from typing import Any
4+
35
from claude_agent_sdk import (
46
AssistantMessage,
57
ClaudeAgentOptions,
@@ -18,9 +20,8 @@
1820
class TestHookTypes:
1921
"""Test hook type definitions."""
2022

21-
def test_hook_json_output_import(self):
22-
"""Test that HookJSONOutput can be imported from main module."""
23-
# Verify HookJSONOutput is accessible from main module
23+
def test_hook_json_output_basic_usage(self):
24+
"""Test basic usage: ensure a dict literal can be annotated as HookJSONOutput and used at runtime."""
2425
hook_output: HookJSONOutput = {"decision": "block"}
2526
assert hook_output["decision"] == "block"
2627

@@ -38,6 +39,187 @@ def test_hook_json_output_with_hook_specific(self):
3839
hook_output: HookJSONOutput = {"hookSpecificOutput": {"key": "value"}}
3940
assert hook_output["hookSpecificOutput"]["key"] == "value"
4041

42+
def test_hook_json_output_all_fields(self):
43+
"""Test HookJSONOutput with all possible fields."""
44+
hook_output: HookJSONOutput = {
45+
"decision": "block",
46+
"systemMessage": "Custom message",
47+
"hookSpecificOutput": {"key": "value"}
48+
}
49+
assert hook_output["decision"] == "block"
50+
assert hook_output["systemMessage"] == "Custom message"
51+
assert hook_output["hookSpecificOutput"]["key"] == "value"
52+
53+
def test_hook_json_output_empty_dict(self):
54+
"""Test HookJSONOutput with empty dict (all fields are optional)."""
55+
hook_output: HookJSONOutput = {}
56+
# Should not raise any errors - all fields are NotRequired
57+
assert isinstance(hook_output, dict)
58+
59+
def test_hook_json_output_decision_only(self):
60+
"""Test HookJSONOutput with only decision field."""
61+
hook_output: HookJSONOutput = {"decision": "block"}
62+
assert hook_output["decision"] == "block"
63+
assert "systemMessage" not in hook_output
64+
assert "hookSpecificOutput" not in hook_output
65+
66+
def test_hook_json_output_system_message_only(self):
67+
"""Test HookJSONOutput with only systemMessage field."""
68+
hook_output: HookJSONOutput = {"systemMessage": "Test message"}
69+
assert hook_output["systemMessage"] == "Test message"
70+
assert "decision" not in hook_output
71+
assert "hookSpecificOutput" not in hook_output
72+
73+
def test_hook_json_output_hook_specific_only(self):
74+
"""Test HookJSONOutput with only hookSpecificOutput field."""
75+
hook_output: HookJSONOutput = {"hookSpecificOutput": {"custom": "data"}}
76+
assert hook_output["hookSpecificOutput"]["custom"] == "data"
77+
assert "decision" not in hook_output
78+
assert "systemMessage" not in hook_output
79+
80+
def test_hook_json_output_complex_hook_specific(self):
81+
"""Test HookJSONOutput with complex hookSpecificOutput structure."""
82+
complex_data = {
83+
"hookEventName": "PreToolUse",
84+
"additionalContext": "Complex nested data",
85+
"nested": {
86+
"level1": {
87+
"level2": ["item1", "item2", "item3"]
88+
}
89+
}
90+
}
91+
hook_output: HookJSONOutput = {"hookSpecificOutput": complex_data}
92+
assert hook_output["hookSpecificOutput"]["hookEventName"] == "PreToolUse"
93+
assert hook_output["hookSpecificOutput"]["nested"]["level1"]["level2"][0] == "item1"
94+
95+
def test_hook_json_output_invalid_field_names(self):
96+
"""Test that HookJSONOutput allows additional fields (TypedDict behavior)."""
97+
# TypedDict allows extra fields at runtime, but type checkers may warn
98+
hook_output: HookJSONOutput = {
99+
"decision": "block",
100+
"invalidField": "should be allowed at runtime",
101+
"anotherInvalid": 123
102+
}
103+
assert hook_output["decision"] == "block"
104+
assert hook_output["invalidField"] == "should be allowed at runtime"
105+
assert hook_output["anotherInvalid"] == 123
106+
107+
def test_hook_json_output_type_constraints(self):
108+
"""Test HookJSONOutput type constraints and runtime behavior."""
109+
# Note: TypedDict doesn't enforce runtime type checking, but documents expected types
110+
# These tests verify the structure can hold various types as documented
111+
112+
# decision should be "block" or not present
113+
hook_output: HookJSONOutput = {"decision": "block"}
114+
assert hook_output["decision"] == "block"
115+
116+
# systemMessage should be a string
117+
hook_output: HookJSONOutput = {"systemMessage": "Test message"}
118+
assert isinstance(hook_output["systemMessage"], str)
119+
120+
# hookSpecificOutput can be any type (Any)
121+
hook_output: HookJSONOutput = {"hookSpecificOutput": "string"}
122+
assert hook_output["hookSpecificOutput"] == "string"
123+
124+
hook_output: HookJSONOutput = {"hookSpecificOutput": 123}
125+
assert hook_output["hookSpecificOutput"] == 123
126+
127+
hook_output: HookJSONOutput = {"hookSpecificOutput": None}
128+
assert hook_output["hookSpecificOutput"] is None
129+
130+
def test_hook_json_output_integration_pretooluse_block(self):
131+
"""Test HookJSONOutput integration pattern for PreToolUse blocking."""
132+
# Simulate a hook that blocks certain commands
133+
def mock_pretooluse_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> HookJSONOutput:
134+
tool_name = input_data.get("tool_name", "")
135+
tool_input = input_data.get("tool_input", {})
136+
137+
if tool_name == "Bash":
138+
command = tool_input.get("command", "")
139+
if "rm -rf" in command:
140+
return {
141+
"decision": "block",
142+
"systemMessage": "Dangerous command blocked",
143+
"hookSpecificOutput": {
144+
"hookEventName": "PreToolUse",
145+
"permissionDecision": "deny",
146+
"permissionDecisionReason": "Command contains dangerous pattern: rm -rf"
147+
}
148+
}
149+
return {}
150+
151+
# Test blocking case
152+
result = mock_pretooluse_hook(
153+
{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}},
154+
"tool-123",
155+
None
156+
)
157+
assert result["decision"] == "block"
158+
assert result["systemMessage"] == "Dangerous command blocked"
159+
assert result["hookSpecificOutput"]["permissionDecision"] == "deny"
160+
161+
# Test non-blocking case
162+
result = mock_pretooluse_hook(
163+
{"tool_name": "Bash", "tool_input": {"command": "ls -la"}},
164+
"tool-123",
165+
None
166+
)
167+
assert result == {}
168+
169+
def test_hook_json_output_integration_session_start(self):
170+
"""Test HookJSONOutput integration pattern for SessionStart hook."""
171+
def mock_session_start_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> HookJSONOutput:
172+
return {
173+
"hookSpecificOutput": {
174+
"hookEventName": "SessionStart",
175+
"additionalContext": "Custom session instructions",
176+
"userPreferences": {
177+
"theme": "dark",
178+
"language": "python"
179+
}
180+
}
181+
}
182+
183+
result = mock_session_start_hook({}, None, None)
184+
assert "decision" not in result
185+
assert "systemMessage" not in result
186+
assert result["hookSpecificOutput"]["hookEventName"] == "SessionStart"
187+
assert result["hookSpecificOutput"]["additionalContext"] == "Custom session instructions"
188+
assert result["hookSpecificOutput"]["userPreferences"]["theme"] == "dark"
189+
190+
def test_hook_json_output_integration_user_prompt_submit(self):
191+
"""Test HookJSONOutput integration pattern for UserPromptSubmit hook."""
192+
def mock_user_prompt_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> HookJSONOutput:
193+
prompt = input_data.get("prompt", "")
194+
195+
if "password" in prompt.lower():
196+
return {
197+
"systemMessage": "Warning: Prompt contains sensitive information",
198+
"hookSpecificOutput": {
199+
"hookEventName": "UserPromptSubmit",
200+
"securityWarning": "Prompt may contain sensitive data",
201+
"recommendation": "Consider removing sensitive information"
202+
}
203+
}
204+
return {}
205+
206+
# Test warning case
207+
result = mock_user_prompt_hook(
208+
{"prompt": "What is my password for the system?"},
209+
None,
210+
None
211+
)
212+
assert result["systemMessage"] == "Warning: Prompt contains sensitive information"
213+
assert result["hookSpecificOutput"]["securityWarning"] == "Prompt may contain sensitive data"
214+
215+
# Test normal case
216+
result = mock_user_prompt_hook(
217+
{"prompt": "How do I create a new file?"},
218+
None,
219+
None
220+
)
221+
assert result == {}
222+
41223

42224
class TestMessageTypes:
43225
"""Test message type creation and validation."""

0 commit comments

Comments
 (0)