11"""Tests for Claude SDK type definitions."""
22
3+ from typing import Any
4+
35from claude_agent_sdk import (
46 AssistantMessage ,
57 ClaudeAgentOptions ,
1820class 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
42224class TestMessageTypes :
43225 """Test message type creation and validation."""
0 commit comments