File size: 6,371 Bytes
127bfda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2641e6e
127bfda
 
 
 
 
 
 
2641e6e
 
127bfda
37ee895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127bfda
 
 
 
 
 
 
 
 
2641e6e
127bfda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2641e6e
127bfda
 
 
 
 
 
 
 
 
2641e6e
 
 
 
127bfda
 
 
 
 
 
 
 
 
 
 
2641e6e
127bfda
 
 
 
 
 
 
 
 
 
 
37ee895
 
 
 
 
 
127bfda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37ee895
 
 
 
 
 
127bfda
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import asyncio
import unittest
from unittest.mock import AsyncMock, MagicMock, patch

from bot import autonomy_runtime


class TestAutonomyRuntime(unittest.IsolatedAsyncioTestCase):
    async def test_bind_unbind(self):
        runtime = autonomy_runtime.AutonomyRuntime()
        loop = asyncio.get_running_loop()

        with patch.object(runtime, "_ensure_loop_task") as mock_ensure:
            runtime.bind(loop=loop, mode="irc")
            self.assertEqual(runtime._mode, "irc")

        runtime.unbind()
        self.assertIsNone(runtime._loop)
        self.assertIsNone(runtime._loop_task)

    @patch("bot.autonomy_runtime.control_plane")
    @patch("bot.autonomy_runtime.process_autonomy_goal", new_callable=AsyncMock)
    async def test_run_tick(self, mock_process, mock_cp):
        runtime = autonomy_runtime.AutonomyRuntime()
        mock_cp.consume_due_goals.return_value = ["goal1"]
        mock_cp.get_config.return_value = {"twitch_channel_login": "default"}
        mock_process.return_value = {"status": "success"}

        result = await runtime._run_tick(force=True, reason="test")

        self.assertTrue(result["ok"])
        self.assertEqual(result["due_goals"], 1)
        mock_cp.register_tick.assert_called_with(reason="test")
        # Deve agora incluir o channel_id
        mock_process.assert_called_once_with("goal1", None, channel_id="default")

    @patch("bot.autonomy_runtime.observability")
    @patch("bot.autonomy_runtime.control_plane")
    @patch("bot.autonomy_runtime.process_autonomy_goal", new_callable=AsyncMock)
    async def test_run_tick_registers_goal_session_kpi(self, mock_process, mock_cp, mock_obs):
        runtime = autonomy_runtime.AutonomyRuntime()
        mock_cp.consume_due_goals.return_value = [{"id": "g1", "risk": "suggest_streamer"}]
        mock_cp.get_config.return_value = {"twitch_channel_login": "default"}
        mock_cp.register_goal_session_result.return_value = {"met": True}
        mock_process.return_value = {
            "goal_id": "g1",
            "risk": "suggest_streamer",
            "outcome": "queued",
        }

        result = await runtime._run_tick(force=True, reason="test")

        mock_cp.register_goal_session_result.assert_called_once_with(
            goal_id="g1",
            outcome="queued",
        )
        self.assertTrue(result["processed"][0]["session_kpi"]["met"])
        mock_obs.record_autonomy_goal.assert_called_once()

    @patch("bot.autonomy_runtime.control_plane")
    @patch("bot.autonomy_runtime.asyncio.sleep", new_callable=AsyncMock)
    async def test_heartbeat_loop(self, mock_sleep, mock_cp):
        runtime = autonomy_runtime.AutonomyRuntime()
        loop = asyncio.get_running_loop()
        runtime.bind(loop=loop, mode="test")

        # Mock _run_tick to stop the loop after one call
        with patch.object(runtime, "_run_tick", new_callable=AsyncMock) as mock_tick:

            def side_effect(*args, **kwargs):
                runtime.unbind()
                return {"ok": True}

            mock_tick.side_effect = side_effect

            await runtime._heartbeat_loop()

            mock_tick.assert_called_once()
            mock_cp.set_loop_running.assert_called()

    @patch("bot.autonomy_runtime.control_plane")
    @patch("bot.autonomy_runtime.sentiment_engine")
    @patch("bot.autonomy_runtime.process_autonomy_goal", new_callable=AsyncMock)
    async def test_run_tick_with_sentiment_triggers(self, mock_process, mock_sent, mock_cp):
        runtime = autonomy_runtime.AutonomyRuntime()
        mock_cp.consume_due_goals.return_value = []
        mock_cp.get_config.return_value = {"twitch_channel_login": "default"}
        mock_sent.should_trigger_anti_boredom.return_value = True
        mock_sent.should_trigger_anti_confusion.return_value = True
        mock_process.return_value = {"status": "success"}

        result = await runtime._run_tick(force=False, reason="test")

        self.assertTrue(result["ok"])
        self.assertEqual(len(result["processed"]), 2)

        # Verifica chamadas com channel_id
        mock_sent.should_trigger_anti_boredom.assert_called_with("default")
        mock_sent.should_trigger_anti_confusion.assert_called_with("default")

        # Verify call arguments
        calls = [call[0][0] for call in mock_process.call_args_list]
        self.assertTrue(any(g["id"] == "dynamic-anti-boredom" for g in calls))
        self.assertTrue(any(g["id"] == "dynamic-anti-confusion" for g in calls))

    @patch("bot.autonomy_runtime.control_plane")
    @patch("bot.autonomy_runtime.sentiment_engine")
    @patch("bot.autonomy_runtime.process_autonomy_goal", new_callable=AsyncMock)
    async def test_run_tick_skips_sentiment_on_force(self, mock_process, mock_sent, mock_cp):
        runtime = autonomy_runtime.AutonomyRuntime()
        mock_cp.consume_due_goals.return_value = []
        mock_cp.get_config.return_value = {"twitch_channel_login": "default"}
        mock_sent.should_trigger_anti_boredom.return_value = True

        await runtime._run_tick(force=True, reason="test")

        # Should NOT have added dynamic goals because force=True
        mock_process.assert_not_called()

    @patch("bot.autonomy_runtime.asyncio.run")
    def test_run_manual_tick_no_loop(self, mock_run):
        runtime = autonomy_runtime.AutonomyRuntime()
        runtime._loop = None  # Ensure no loop

        def run_side_effect(coroutine):
            coroutine.close()
            return {"ok": True}

        mock_run.side_effect = run_side_effect

        result = runtime.run_manual_tick(force=True)
        self.assertTrue(result["ok"])
        mock_run.assert_called_once()

    @patch("bot.autonomy_runtime.asyncio.run_coroutine_threadsafe")
    def test_run_manual_tick_threadsafe(self, mock_run):
        runtime = autonomy_runtime.AutonomyRuntime()
        loop = MagicMock()
        loop.is_closed.return_value = False
        loop.is_running.return_value = True
        runtime._loop = loop

        mock_future = MagicMock()
        mock_future.result.return_value = {"ok": True}

        def submit_side_effect(coroutine, _loop):
            coroutine.close()
            return mock_future

        mock_run.side_effect = submit_side_effect

        result = runtime.run_manual_tick(force=True)
        self.assertTrue(result["ok"])
        mock_run.assert_called_once()