@@ -251,6 +251,113 @@ def fake_run_strategy(*, force_run=False, validation_only=False, validation_labe
251251 self .assertTrue (observed ["validation_only" ])
252252 self .assertEqual (observed ["validation_label" ], "precheck" )
253253
254+ def test_handle_probe_checks_account_snapshot_without_success_notification (self ):
255+ module = load_module ()
256+ observed = {"override" : None , "events" : [], "notifications" : []}
257+ snapshot = types .SimpleNamespace (
258+ buying_power = 123.0 ,
259+ total_equity = 456.0 ,
260+ positions = (types .SimpleNamespace (symbol = "SOXL" ),),
261+ )
262+
263+ class FakePortfolioPort :
264+ def get_portfolio_snapshot (self ):
265+ observed ["snapshot_called" ] = True
266+ return snapshot
267+
268+ class FakeRuntime :
269+ def __init__ (self ):
270+ self .bootstrap = lambda : ("quote-context" , "trade-context" , {"trend" : "ok" })
271+ self .portfolio_port_factory = lambda quote_context , trade_context : FakePortfolioPort ()
272+
273+ class FakeComposer :
274+ def build_reporting_adapters (self ):
275+ return types .SimpleNamespace (
276+ start_run = lambda : (types .SimpleNamespace (run_id = "run-001" ), {"status" : "pending" }),
277+ log_event = lambda context , event , ** fields : observed ["events" ].append ((event , fields )),
278+ persist_execution_report = lambda report : observed .setdefault ("report" , dict (report )) or "/tmp/report.json" ,
279+ )
280+
281+ def build_rebalance_runtime (self , * , silent_cycle_notifications = False ):
282+ observed ["silent_cycle_notifications" ] = silent_cycle_notifications
283+ return FakeRuntime ()
284+
285+ def build_notification_adapters (self ):
286+ raise AssertionError ("probe success should stay silent" )
287+
288+ def load_strategy_plugin_signals (self , * _args , ** _kwargs ):
289+ return (), None
290+
291+ def attach_strategy_plugin_report (self , * _args , ** _kwargs ):
292+ return None
293+
294+ module .build_composer = lambda * , dry_run_only_override = None : observed .__setitem__ ("override" , dry_run_only_override ) or FakeComposer ()
295+
296+ with module .app .test_request_context ("/probe" , method = "POST" ):
297+ body , status = module .handle_probe ()
298+
299+ self .assertEqual (status , 200 )
300+ self .assertEqual (body , "Probe OK" )
301+ self .assertTrue (observed ["override" ])
302+ self .assertTrue (observed ["silent_cycle_notifications" ])
303+ self .assertTrue (observed ["snapshot_called" ])
304+ self .assertEqual (
305+ [event for event , _fields in observed ["events" ]],
306+ ["health_probe_received" , "health_probe_completed" ],
307+ )
308+ self .assertEqual (observed ["report" ]["status" ], "ok" )
309+ self .assertEqual (observed ["report" ]["summary" ]["buying_power" ], 123.0 )
310+ self .assertEqual (observed ["report" ]["summary" ]["total_equity" ], 456.0 )
311+ self .assertEqual (observed ["report" ]["summary" ]["positions_count" ], 1 )
312+
313+ def test_handle_probe_failure_sends_notification (self ):
314+ module = load_module ()
315+ observed = {"events" : [], "notifications" : []}
316+
317+ class FakeRuntime :
318+ def bootstrap (self ):
319+ raise RuntimeError ("probe failed" )
320+
321+ class FakeNotifications :
322+ def publish_cycle_notification (self , ** kwargs ):
323+ observed ["notifications" ].append (kwargs )
324+
325+ class FakeComposer :
326+ def build_reporting_adapters (self ):
327+ return types .SimpleNamespace (
328+ start_run = lambda : (types .SimpleNamespace (run_id = "run-001" ), {"status" : "pending" }),
329+ log_event = lambda context , event , ** fields : observed ["events" ].append ((event , fields )),
330+ persist_execution_report = lambda report : observed .setdefault ("report" , dict (report )) or "/tmp/report.json" ,
331+ )
332+
333+ def build_rebalance_runtime (self , * , silent_cycle_notifications = False ):
334+ return FakeRuntime ()
335+
336+ def build_notification_adapters (self ):
337+ return FakeNotifications ()
338+
339+ def load_strategy_plugin_signals (self , * _args , ** _kwargs ):
340+ return (), None
341+
342+ def attach_strategy_plugin_report (self , * _args , ** _kwargs ):
343+ return None
344+
345+ module .build_composer = lambda * , dry_run_only_override = None : FakeComposer ()
346+
347+ with module .app .test_request_context ("/probe" , method = "POST" ):
348+ body , status = module .handle_probe ()
349+
350+ self .assertEqual (status , 500 )
351+ self .assertEqual (body , "Error" )
352+ self .assertEqual (observed ["report" ]["status" ], "error" )
353+ self .assertEqual (observed ["report" ]["errors" ][0 ]["stage" ], "health_probe" )
354+ self .assertEqual (
355+ [event for event , _fields in observed ["events" ]],
356+ ["health_probe_received" , "health_probe_failed" ],
357+ )
358+ self .assertEqual (len (observed ["notifications" ]), 1 )
359+ self .assertIn ("probe failed" , observed ["notifications" ][0 ]["detailed_text" ])
360+
254361 def test_run_strategy_emits_structured_runtime_events (self ):
255362 module = load_module ()
256363 observed = []
@@ -308,7 +415,8 @@ def attach_strategy_plugin_report(self, *_args, **_kwargs):
308415 def with_prefix (self , message ):
309416 return message
310417
311- def build_rebalance_runtime (self ):
418+ def build_rebalance_runtime (self , * , silent_cycle_notifications = False ):
419+ observed ["silent_cycle_notifications" ] = silent_cycle_notifications
312420 return types .SimpleNamespace ()
313421
314422 def build_rebalance_config (self , * , strategy_plugin_signals = ()):
@@ -323,6 +431,7 @@ def build_rebalance_config(self, *, strategy_plugin_signals=()):
323431 module .run_strategy (force_run = True , validation_only = True )
324432
325433 self .assertTrue (observed ["override" ])
434+ self .assertTrue (observed ["silent_cycle_notifications" ])
326435
327436 def test_run_strategy_persists_machine_readable_report (self ):
328437 module = load_module ()
0 commit comments