@@ -1501,6 +1501,198 @@ def orchestrator(ctx: task.OrchestrationContext, _):
15011501 assert complete_action .result .value == encoded_output
15021502
15031503
1504+ def test_replay_safe_logger_suppresses_during_replay ():
1505+ """Validates that the replay-safe logger suppresses log messages during replay."""
1506+ log_calls : list [str ] = []
1507+
1508+ class _RecordingHandler (logging .Handler ):
1509+ def emit (self , record : logging .LogRecord ) -> None :
1510+ log_calls .append (record .getMessage ())
1511+
1512+ handler = _RecordingHandler ()
1513+ inner_logger = logging .getLogger ("test_replay_safe_logger" )
1514+ inner_logger .setLevel (logging .DEBUG )
1515+ original_propagate = inner_logger .propagate
1516+ inner_logger .propagate = False
1517+ inner_logger .addHandler (handler )
1518+
1519+ try :
1520+ activity_name = "say_hello"
1521+
1522+ def say_hello (_ , name : str ) -> str :
1523+ return f"Hello, { name } !"
1524+
1525+ def orchestrator (ctx : task .OrchestrationContext , _ ):
1526+ replay_logger = ctx .create_replay_safe_logger (inner_logger )
1527+ replay_logger .info ("Starting orchestration" )
1528+ result = yield ctx .call_activity (say_hello , input = "World" )
1529+ replay_logger .info ("Activity completed: %s" , result )
1530+ return result
1531+
1532+ registry = worker ._Registry ()
1533+ activity_name = registry .add_activity (say_hello )
1534+ orchestrator_name = registry .add_orchestrator (orchestrator )
1535+
1536+ # First execution: starts the orchestration. The orchestrator runs without
1537+ # replay, emits the initial log message, and then schedules the activity.
1538+ new_events = [
1539+ helpers .new_orchestrator_started_event (datetime .now ()),
1540+ helpers .new_execution_started_event (orchestrator_name , TEST_INSTANCE_ID , encoded_input = None ),
1541+ ]
1542+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER )
1543+ result = executor .execute (TEST_INSTANCE_ID , [], new_events )
1544+ assert result .actions # should have scheduled the activity
1545+
1546+ assert log_calls == ["Starting orchestration" ]
1547+ log_calls .clear ()
1548+
1549+ # Second execution: the orchestrator replays from history and then processes the
1550+ # activity completion. The "Starting orchestration" message is emitted during
1551+ # replay and should be suppressed; "Activity completed" is emitted after replay
1552+ # ends and should appear exactly once.
1553+ old_events = new_events + [
1554+ helpers .new_task_scheduled_event (1 , activity_name ),
1555+ ]
1556+ encoded_output = json .dumps (say_hello (None , "World" ))
1557+ new_events = [helpers .new_task_completed_event (1 , encoded_output )]
1558+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER )
1559+ result = executor .execute (TEST_INSTANCE_ID , old_events , new_events )
1560+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result .actions )
1561+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_COMPLETED
1562+
1563+ assert log_calls == ["Activity completed: Hello, World!" ]
1564+ finally :
1565+ inner_logger .removeHandler (handler )
1566+ inner_logger .propagate = original_propagate
1567+
1568+
1569+ def test_replay_safe_logger_all_levels ():
1570+ """Validates that all log levels are suppressed during replay and emitted otherwise."""
1571+ log_levels : list [str ] = []
1572+
1573+ class _LevelRecorder (logging .Handler ):
1574+ def emit (self , record : logging .LogRecord ) -> None :
1575+ log_levels .append (record .levelname )
1576+
1577+ handler = _LevelRecorder ()
1578+ inner_logger = logging .getLogger ("test_replay_safe_logger_levels" )
1579+ inner_logger .setLevel (logging .DEBUG )
1580+ original_propagate = inner_logger .propagate
1581+ inner_logger .propagate = False
1582+ inner_logger .addHandler (handler )
1583+
1584+ try :
1585+ def orchestrator (ctx : task .OrchestrationContext , _ ):
1586+ replay_logger = ctx .create_replay_safe_logger (inner_logger )
1587+ replay_logger .debug ("debug msg" )
1588+ replay_logger .info ("info msg" )
1589+ replay_logger .warning ("warning msg" )
1590+ replay_logger .error ("error msg" )
1591+ replay_logger .critical ("critical msg" )
1592+ return "done"
1593+
1594+ registry = worker ._Registry ()
1595+ orchestrator_name = registry .add_orchestrator (orchestrator )
1596+
1597+ new_events = [
1598+ helpers .new_orchestrator_started_event (datetime .now ()),
1599+ helpers .new_execution_started_event (orchestrator_name , TEST_INSTANCE_ID , encoded_input = None ),
1600+ ]
1601+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER )
1602+ result = executor .execute (TEST_INSTANCE_ID , [], new_events )
1603+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result .actions )
1604+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_COMPLETED
1605+
1606+ assert log_levels == ["DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" ]
1607+ finally :
1608+ inner_logger .removeHandler (handler )
1609+ inner_logger .propagate = original_propagate
1610+
1611+
1612+ def test_replay_safe_logger_direct ():
1613+ """Unit test for ReplaySafeLogger — verifies suppression based on is_replaying flag."""
1614+ log_calls : list [str ] = []
1615+
1616+ class _RecordingHandler (logging .Handler ):
1617+ def emit (self , record : logging .LogRecord ) -> None :
1618+ log_calls .append (record .getMessage ())
1619+
1620+ handler = _RecordingHandler ()
1621+ inner_logger = logging .getLogger ("test_replay_safe_logger_direct" )
1622+ inner_logger .setLevel (logging .DEBUG )
1623+ original_propagate = inner_logger .propagate
1624+ inner_logger .propagate = False
1625+ inner_logger .addHandler (handler )
1626+
1627+ try :
1628+ replaying = True
1629+ replay_logger = task .ReplaySafeLogger (inner_logger , lambda : replaying )
1630+
1631+ replay_logger .info ("should be suppressed" )
1632+ assert log_calls == []
1633+
1634+ replaying = False
1635+ replay_logger .info ("should appear" )
1636+ assert log_calls == ["should appear" ]
1637+ finally :
1638+ inner_logger .removeHandler (handler )
1639+ inner_logger .propagate = original_propagate
1640+
1641+
1642+ def test_replay_safe_logger_log_method ():
1643+ """Validates the generic log() method respects the replay flag."""
1644+ log_calls : list [str ] = []
1645+
1646+ class _RecordingHandler (logging .Handler ):
1647+ def emit (self , record : logging .LogRecord ) -> None :
1648+ log_calls .append (record .getMessage ())
1649+
1650+ handler = _RecordingHandler ()
1651+ inner_logger = logging .getLogger ("test_replay_safe_logger_log_method" )
1652+ inner_logger .setLevel (logging .DEBUG )
1653+ original_propagate = inner_logger .propagate
1654+ inner_logger .propagate = False
1655+ inner_logger .addHandler (handler )
1656+
1657+ try :
1658+ replaying = True
1659+ replay_logger = task .ReplaySafeLogger (inner_logger , lambda : replaying )
1660+
1661+ replay_logger .log (logging .WARNING , "suppressed warning" )
1662+ assert log_calls == []
1663+
1664+ replaying = False
1665+ replay_logger .log (logging .WARNING , "visible warning" )
1666+ assert log_calls == ["visible warning" ]
1667+ finally :
1668+ inner_logger .removeHandler (handler )
1669+ inner_logger .propagate = original_propagate
1670+
1671+
1672+ def test_replay_safe_logger_is_enabled_for ():
1673+ """Validates isEnabledFor returns False during replay."""
1674+ inner_logger = logging .getLogger ("test_replay_safe_logger_enabled" )
1675+ inner_logger .setLevel (logging .DEBUG )
1676+
1677+ replaying = True
1678+ replay_logger = task .ReplaySafeLogger (inner_logger , lambda : replaying )
1679+
1680+ # During replay, isEnabledFor should always return False
1681+ assert replay_logger .isEnabledFor (logging .DEBUG ) is False
1682+ assert replay_logger .isEnabledFor (logging .INFO ) is False
1683+ assert replay_logger .isEnabledFor (logging .CRITICAL ) is False
1684+
1685+ # After replay, delegates to the inner logger
1686+ replaying = False
1687+ assert replay_logger .isEnabledFor (logging .DEBUG ) is True
1688+ assert replay_logger .isEnabledFor (logging .INFO ) is True
1689+
1690+ # If a level is below the inner logger's level, should return False
1691+ inner_logger .setLevel (logging .WARNING )
1692+ assert replay_logger .isEnabledFor (logging .DEBUG ) is False
1693+ assert replay_logger .isEnabledFor (logging .WARNING ) is True
1694+
1695+
15041696def test_when_any_with_retry ():
15051697 """Tests that a when_any pattern works correctly with retries"""
15061698 def dummy_activity (_ , inp : str ):
0 commit comments