2323Exit codes:
2424 0 - No corruption detected (success)
2525 1 - Corruption detected or errors encountered (failure)
26-
27- Note:
28- This is a placeholder implementation. The comprehensive E2E test in
29- test_label_operations.py::TestRegressionIssue396 provides full validation.
30- This manual script can be extended for longer stress tests (1000+ operations)
31- and custom scenarios.
3226"""
3327
3428import asyncio
3529import logging
3630import os
31+ import random
3732import sys
33+ import time
3834
3935logging .basicConfig (
4036 level = logging .INFO ,
@@ -48,6 +44,9 @@ async def main():
4844 """Main fuzzing entry point."""
4945 import argparse
5046
47+ from ha_mcp .client .rest_client import HomeAssistantClient
48+ from ha_mcp .client .websocket_client import HomeAssistantWebSocketClient
49+
5150 parser = argparse .ArgumentParser (
5251 description = "Fuzz label operations to detect entity registry corruption (Issue #396)"
5352 )
@@ -90,19 +89,7 @@ async def main():
9089 logger .info ("=" * 80 )
9190 logger .info ("" )
9291
93- logger .info ("⚠️ PLACEHOLDER IMPLEMENTATION" )
94- logger .info ("" )
95- logger .info ("This is a simplified placeholder. For comprehensive testing:" )
96- logger .info (" Run: pytest tests/src/e2e/workflows/labels/test_label_operations.py" )
97- logger .info (" -k TestRegressionIssue396" )
98- logger .info ("" )
99- logger .info ("The E2E test performs:" )
100- logger .info (" - 15+ rapid operations (add/remove/set cycles)" )
101- logger .info (" - Registry health validation" )
102- logger .info (" - Corruption detection" )
103- logger .info ("" )
104-
105- # Get credentials (even though we're not using them yet)
92+ # Get credentials
10693 url = args .url or os .getenv ("HOMEASSISTANT_URL" )
10794 token = args .token or os .getenv ("HOMEASSISTANT_TOKEN" )
10895
@@ -118,33 +105,225 @@ async def main():
118105 logger .info (f"Target: { url } " )
119106 logger .info ("" )
120107
121- logger .info ("Expected behavior with ha_manage_entity_labels:" )
122- logger .info (" ✅ No corruption after 100+ operations" )
123- logger .info (" ✅ All health checks pass" )
124- logger .info (" ✅ Registry remains accessible" )
125- logger .info ("" )
108+ # Initialize clients
109+ rest_client = HomeAssistantClient (url , token )
110+ ws_client = HomeAssistantWebSocketClient (url , token )
126111
127- logger .info ("Expected behavior with old ha_assign_label:" )
128- logger .info (" ❌ Corruption after 5-10 operations" )
129- logger .info (" ❌ Health checks fail" )
130- logger .info (" ❌ UI becomes inaccessible" )
131- logger .info ("" )
112+ created_labels = []
113+ test_entities = []
132114
133- logger .info ("=" * 80 )
134- logger .info ("To extend this script:" )
135- logger .info (" 1. Import ha_mcp.client.websocket_client" )
136- logger .info (" 2. Create test labels via label_registry/create" )
137- logger .info (" 3. Find entities via REST API get_states()" )
138- logger .info (" 4. Perform operations via entity_registry/update" )
139- logger .info (" 5. Check health via label_registry/list" )
140- logger .info (" 6. Report results and cleanup" )
141- logger .info ("=" * 80 )
142- logger .info ("" )
115+ try :
116+ # Connect WebSocket
117+ await ws_client .connect ()
118+ logger .info ("✅ Connected to Home Assistant" )
119+ logger .info ("" )
143120
144- logger .info ("✅ Placeholder test complete (no actual operations performed)" )
145- logger .info ("" )
121+ # Step 1: Create test labels
122+ logger .info (f"Creating { args .labels } test labels..." )
123+ for i in range (args .labels ):
124+ result = await ws_client .send_command (
125+ "config/label_registry/create" ,
126+ name = f"fuzz_test_label_{ i + 1 } " ,
127+ icon = "mdi:test-tube" ,
128+ )
129+ if result .get ("success" ):
130+ label_id = result ["result" ]["label_id" ]
131+ created_labels .append (label_id )
132+ logger .info (f" Created label { i + 1 } /{ args .labels } : { label_id } " )
133+ else :
134+ logger .warning (f" Failed to create label { i + 1 } : { result } " )
135+
136+ if len (created_labels ) < args .labels :
137+ logger .warning (
138+ f"⚠️ Only created { len (created_labels )} /{ args .labels } labels"
139+ )
140+ logger .info ("" )
141+
142+ # Step 2: Find test entities
143+ logger .info (f"Finding { args .entities } test entities..." )
144+ states = await rest_client .get_states ()
145+ light_entities = [s ["entity_id" ] for s in states if s ["entity_id" ].startswith ("light." )]
146+
147+ if len (light_entities ) < args .entities :
148+ logger .warning (
149+ f"⚠️ Only found { len (light_entities )} light entities, requested { args .entities } "
150+ )
151+ test_entities = light_entities
152+ else :
153+ test_entities = light_entities [: args .entities ]
154+
155+ for i , entity_id in enumerate (test_entities , 1 ):
156+ logger .info (f" Using entity { i } /{ len (test_entities )} : { entity_id } " )
157+ logger .info ("" )
158+
159+ # Step 3: Perform fuzzing operations
160+ logger .info (f"Starting fuzzing operations ({ args .operations } operations)..." )
161+ start_time = time .time ()
162+
163+ successful_ops = 0
164+ failed_ops = 0
165+
166+ for op_num in range (1 , args .operations + 1 ):
167+ # Pick random entity and operation
168+ entity_id = random .choice (test_entities )
169+ operation_type = random .choice (["add" , "remove" , "set" ])
170+
171+ # Pick random labels
172+ if operation_type == "add" :
173+ label_ids = random .sample (created_labels , k = random .randint (1 , min (3 , len (created_labels ))))
174+ elif operation_type == "remove" :
175+ label_ids = random .sample (created_labels , k = random .randint (1 , min (2 , len (created_labels ))))
176+ else : # set
177+ num_labels = random .randint (0 , min (5 , len (created_labels )))
178+ label_ids = random .sample (created_labels , k = num_labels ) if num_labels > 0 else []
179+
180+ try :
181+ # Get current labels
182+ get_result = await ws_client .send_command (
183+ "config/entity_registry/get" ,
184+ entity_id = entity_id ,
185+ )
186+
187+ if not get_result .get ("success" ):
188+ logger .warning (f" [{ op_num :4d} /{ args .operations } ] Failed to get entity { entity_id } " )
189+ failed_ops += 1
190+ continue
146191
147- return 0
192+ current_labels = get_result ["result" ].get ("labels" , [])
193+
194+ # Calculate new labels based on operation
195+ if operation_type == "add" :
196+ new_labels = list (set (current_labels + label_ids ))
197+ elif operation_type == "remove" :
198+ new_labels = [lbl for lbl in current_labels if lbl not in label_ids ]
199+ else : # set
200+ new_labels = label_ids
201+
202+ # Update entity
203+ update_result = await ws_client .send_command (
204+ "config/entity_registry/update" ,
205+ entity_id = entity_id ,
206+ labels = new_labels ,
207+ )
208+
209+ if update_result .get ("success" ):
210+ logger .info (
211+ f" [{ op_num :4d} /{ args .operations } ] { operation_type :6s} on { entity_id :30s} "
212+ f"with { len (label_ids )} label(s) ✅"
213+ )
214+ successful_ops += 1
215+ else :
216+ logger .warning (
217+ f" [{ op_num :4d} /{ args .operations } ] { operation_type :6s} on { entity_id :30s} FAILED"
218+ )
219+ failed_ops += 1
220+
221+ except Exception as e :
222+ logger .error (
223+ f" [{ op_num :4d} /{ args .operations } ] Exception during { operation_type } : { e } "
224+ )
225+ failed_ops += 1
226+
227+ # Health check every 20 operations
228+ if op_num % 20 == 0 :
229+ logger .info ("" )
230+ logger .info (f" Checking registry health after { op_num } operations..." )
231+
232+ try :
233+ # Try to list labels
234+ list_result = await ws_client .send_command ("config/label_registry/list" )
235+ if not list_result .get ("success" ):
236+ logger .error (" ❌ CORRUPTION DETECTED: Cannot list labels!" )
237+ return 1
238+
239+ # Try to get an entity
240+ test_entity = test_entities [0 ]
241+ get_result = await ws_client .send_command (
242+ "config/entity_registry/get" ,
243+ entity_id = test_entity ,
244+ )
245+ if not get_result .get ("success" ):
246+ logger .error (f" ❌ CORRUPTION DETECTED: Cannot get entity { test_entity } !" )
247+ return 1
248+
249+ logger .info (" ✅ Registry health check passed" )
250+ except Exception as e :
251+ logger .error (f" ❌ CORRUPTION DETECTED: Health check exception: { e } " )
252+ return 1
253+
254+ logger .info ("" )
255+
256+ elapsed = time .time () - start_time
257+ logger .info ("" )
258+ logger .info ("=" * 80 )
259+ logger .info ("FUZZING COMPLETE" )
260+ logger .info ("=" * 80 )
261+ logger .info (f"Total operations: { args .operations } " )
262+ logger .info (f"Successful: { successful_ops } " )
263+ logger .info (f"Failed: { failed_ops } " )
264+ logger .info (f"Time elapsed: { elapsed :.2f} s" )
265+ logger .info (f"Operations/sec: { args .operations / elapsed :.2f} " )
266+ logger .info ("" )
267+
268+ # Final comprehensive health check
269+ logger .info ("Performing final comprehensive health check..." )
270+ try :
271+ # List all labels
272+ list_result = await ws_client .send_command ("config/label_registry/list" )
273+ if not list_result .get ("success" ):
274+ logger .error ("❌ FINAL HEALTH CHECK FAILED: Cannot list labels" )
275+ return 1
276+
277+ # Get all test entities
278+ for entity_id in test_entities :
279+ get_result = await ws_client .send_command (
280+ "config/entity_registry/get" ,
281+ entity_id = entity_id ,
282+ )
283+ if not get_result .get ("success" ):
284+ logger .error (f"❌ FINAL HEALTH CHECK FAILED: Cannot get entity { entity_id } " )
285+ return 1
286+
287+ # Try one more label operation
288+ test_entity = test_entities [0 ]
289+ update_result = await ws_client .send_command (
290+ "config/entity_registry/update" ,
291+ entity_id = test_entity ,
292+ labels = [],
293+ )
294+ if not update_result .get ("success" ):
295+ logger .error ("❌ FINAL HEALTH CHECK FAILED: Cannot perform label operation" )
296+ return 1
297+
298+ logger .info ("✅ FINAL HEALTH CHECK PASSED - NO CORRUPTION DETECTED" )
299+ logger .info ("=" * 80 )
300+ return 0
301+
302+ except Exception as e :
303+ logger .error (f"❌ FINAL HEALTH CHECK FAILED: { e } " )
304+ return 1
305+
306+ except Exception as e :
307+ logger .error (f"❌ Fatal error: { e } " , exc_info = True )
308+ return 1
309+
310+ finally :
311+ # Cleanup: Delete created labels
312+ logger .info ("" )
313+ logger .info ("Cleaning up test labels..." )
314+ for label_id in created_labels :
315+ try :
316+ await ws_client .send_command (
317+ "config/label_registry/delete" ,
318+ label_id = label_id ,
319+ )
320+ logger .info (f" Deleted label: { label_id } " )
321+ except Exception as e :
322+ logger .warning (f" Failed to delete label { label_id } : { e } " )
323+
324+ await ws_client .disconnect ()
325+ logger .info ("✅ Cleanup complete" )
326+ logger .info ("" )
148327
149328
150329if __name__ == "__main__" :
0 commit comments