def test_get_contextual_events_filtering():
    """
    Tests that specified event types and hidden events are filtered out.
    """
    mock_event_stream = MagicMock(spec=EventStream)
    target_event_id = 3  # Target a non-filtered event

    all_event_specs = [
        # Before target_event_id = 3. Context size 4. Search limit 4.
        # search_events(start_id=3, reverse=True, limit=4)
        {'id': 0, 'type': NullAction},  # Filtered
        {'id': 1, 'type': MessageAction, 'content': 'message_1_VISIBLE'},  # Visible
        {
            'id': 2,
            'type': ChangeAgentStateAction,
            'agent_state': 'thinking',
            'thought': 'abc_FILTERED',
        },  # Filtered
        {
            'id': 3,
            'type': CmdRunAction,
            'command': 'command_3_TARGET_VISIBLE',
        },  # Target, Visible
        # After target_event_id = 3. Context size 4 + 1 = 5. Search limit 5.
        # search_events(start_id=4, limit=5)
        {
            'id': 4,
            'type': CmdOutputObservation,
            'content': 'obs_4_HIDDEN_FILTERED',
            'command_id': 3,
            'hidden': True,
        },  # Filtered (hidden)
        {
            'id': 5,
            'type': AgentStateChangedObservation,
            'agent_state': 'running',
            'content': 'state_change_5_FILTERED',
        },  # Filtered
        {'id': 6, 'type': MessageAction, 'content': 'message_6_VISIBLE'},  # Visible
        {
            'id': 7,
            'type': NullObservation,
            'content': 'null_obs_7_FILTERED',
        },  # Filtered
        {'id': 8, 'type': CmdRunAction, 'command': 'command_8_VISIBLE'},  # Visible
        {
            'id': 9,
            'type': MessageAction,
            'content': 'message_9_VISIBLE',
        },  # Visible (within limit of 5 for 'after' search)
        {
            'id': 10,
            'type': MessageAction,
            'content': 'message_10_EXTRA',
        },  # Extra, should not be fetched by 'after' search
    ]
    all_events_objects = create_test_events(all_event_specs)
    events_by_id = {e.id: e for e in all_events_objects}

    # Expected events to be returned by search_events AFTER internal filtering by EventFilter
    # For "before" call (start_id=3, reverse=True, limit=4):
    # Raw available before/incl target: [cmd3, state2_filt, msg1, null0_filt]
    # After EventFilter: [cmd3, msg1] -> search_events should return these
    [events_by_id[3], events_by_id[1]]

    # For "after" call (start_id=4, limit=5):
    # Raw available after target: [hidden_obs4_filt, agent_state5_filt, msg6, null_obs7_filt, cmd8, msg9, msg10_extra]
    # After EventFilter: [msg6, cmd8, msg9, msg10_extra] -> search_events should return first 5 of these if available
    # Limit is 5, so it should return [msg6, cmd8, msg9] (msg10_extra is out of original context_size+1 scope)
    # Correcting this: the mock search_events should simulate what EventStream.search_events does.
    # EventStream.search_events applies the filter internally.
    # So, the lists passed to side_effect should be the *already filtered* lists.

    # Simulating EventStream.search_events behavior:
    # It iterates, applies filter, then takes limit.

    # Before: start_id=3, reverse=True, limit=4. Candidates: [3,2,1,0]. Filtered: [3,1]. Result: [3,1]
    simulated_search_before = [events_by_id[3], events_by_id[1]]

    # After: start_id=4, limit=5. Candidates: [4,5,6,7,8,9,10]. Filtered: [6,8,9]. Result: [6,8,9]
    simulated_search_after = [events_by_id[6], events_by_id[8], events_by_id[9]]

    mock_event_stream.search_events.side_effect = [
        simulated_search_before,
        simulated_search_after,
    ]

    result_str = _get_contextual_events(mock_event_stream, target_event_id)

    expected_final_event_objects = [
        events_by_id[1],  # from before, reversed
        events_by_id[3],  # from before, reversed (target)
        events_by_id[6],  # from after
        events_by_id[8],  # from after
        events_by_id[9],  # from after
    ]
    expected_output_str = '\n'.join(str(e) for e in expected_final_event_objects)

    assert result_str == expected_output_str

    # Verify the EventFilter used in search_events
    calls = mock_event_stream.search_events.call_args_list
    assert len(calls) == 2

    expected_filtered_types = (
        NullAction,
        NullObservation,
        ChangeAgentStateAction,
        AgentStateChangedObservation,
    )

    # Check filter for "before" call
    filter_before = calls[0][1]['filter']  # kwargs['filter']
    assert isinstance(filter_before, EventFilter)
    assert filter_before.exclude_hidden is True
    assert set(filter_before.exclude_types) == set(expected_filtered_types)

    # Check filter for "after" call
    filter_after = calls[1][1]['filter']  # kwargs['filter']
    assert isinstance(filter_after, EventFilter)
    assert filter_after.exclude_hidden is True
    assert set(filter_after.exclude_types) == set(expected_filtered_types)

    Tests that specified event types and hidden events are filtered out.
    
tests/unit/test_contextual_events.py