File size: 14,258 Bytes
aef804e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
"""
Coverage expansion tests for WebSocket broadcast integration with canvas.

Target: Verify WebSocket broadcast functionality for canvas operations:
- WebSocket broadcast called for all canvas presentations
- WebSocket error handling
- Session-based routing
- User channel routing

Tests use mocked WebSocket manager to focus on broadcast logic.
"""
import pytest
from unittest.mock import AsyncMock, Mock, patch

# Add parent directory to path for imports
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))


# ============================================================================
# Fixtures
# ============================================================================

@pytest.fixture
def mock_db_context():
    """Mock database context manager."""
    with patch('core.database.get_db_session') as mock_ctx:
        db = Mock()
        db.add = Mock()
        db.commit = Mock()
        db.refresh = Mock()
        db.rollback = Mock()

        # Track different types of objects
        mock_agent = None
        mock_agent_execution = None
        mock_canvas_audit = None

        def capture_add(obj):
            """Capture added objects."""
            nonlocal mock_agent, mock_agent_execution, mock_canvas_audit
            if hasattr(obj, 'module_path'):  # AgentRegistry
                mock_agent = obj
            elif hasattr(obj, 'triggered_by'):  # AgentExecution
                mock_agent_execution = obj
            elif hasattr(obj, 'canvas_id'):  # CanvasAudit
                mock_canvas_audit = obj

        db.add = Mock(side_effect=capture_add)

        def mock_query(model):
            """Mock query that returns appropriate object."""
            query_obj = Mock()

            def mock_filter(*args, **kwargs):
                result = Mock()
                if hasattr(model, '__tablename__'):
                    if model.__tablename__ == 'agent_registry':
                        result.first = Mock(return_value=mock_agent)
                    elif model.__tablename__ == 'agent_execution':
                        result.first = Mock(return_value=mock_agent_execution)
                    elif model.__tablename__ == 'canvas_audit':
                        result.first = Mock(return_value=mock_canvas_audit)
                    else:
                        result.first = Mock(return_value=None)
                else:
                    result.first = Mock(return_value=None)
                return result

            query_obj.filter = mock_filter
            return query_obj

        db.query = Mock(side_effect=mock_query)

        mock_ctx.return_value.__enter__ = Mock(return_value=db)
        mock_ctx.return_value.__exit__ = Mock(return_value=False)
        yield mock_ctx


@pytest.fixture
def mock_ws_manager():
    """Mock WebSocket manager with AsyncMock for async broadcast methods."""
    # Create mock manager with AsyncMock for async methods
    mock_mgr = Mock()
    mock_mgr.broadcast = AsyncMock()

    # Patch ws_manager in canvas_tool module
    with patch('tools.canvas_tool.ws_manager', mock_mgr):
        yield mock_mgr


# ============================================================================
# Test WebSocket Broadcast
# ============================================================================

class TestWebSocketBroadcast:
    """Test WebSocket broadcast for canvas operations."""

    @pytest.mark.asyncio
    async def test_chart_broadcast_called(self, mock_db_context, mock_ws_manager):
        """Test that chart presentation triggers WebSocket broadcast."""
        from tools.canvas_tool import present_chart

        result = await present_chart(
            user_id="test_user",
            chart_type="line_chart",
            data=[{"x": 1, "y": 2}],
            title="Test Chart"
        )

        assert result["success"] is True
        assert mock_ws_manager.broadcast.called

        # Verify broadcast structure
        call_args = mock_ws_manager.broadcast.call_args
        assert "user:test_user" in call_args[0][0]
        assert call_args[0][1]["type"] == "canvas:update"
        assert call_args[0][1]["data"]["action"] == "present"

    @pytest.mark.asyncio
    async def test_form_broadcast_called(self, mock_db_context, mock_ws_manager):
        """Test that form presentation triggers WebSocket broadcast."""
        from tools.canvas_tool import present_form

        result = await present_form(
            user_id="test_user",
            form_schema={"fields": [{"name": "field1", "type": "text"}]},
            title="Test Form"
        )

        assert result["success"] is True
        assert mock_ws_manager.broadcast.called

        # Verify broadcast includes form data
        call_args = mock_ws_manager.broadcast.call_args
        assert call_args[0][1]["data"]["component"] == "form"

    @pytest.mark.asyncio
    async def test_markdown_broadcast_called(self, mock_db_context, mock_ws_manager):
        """Test that markdown presentation triggers WebSocket broadcast."""
        from tools.canvas_tool import present_markdown

        result = await present_markdown(
            user_id="test_user",
            content="# Test Markdown\n\nThis is a test.",
            title="Test MD"
        )

        assert result["success"] is True
        assert mock_ws_manager.broadcast.called

        # Verify broadcast includes markdown content
        call_args = mock_ws_manager.broadcast.call_args
        assert call_args[0][1]["data"]["component"] == "markdown"

    @pytest.mark.asyncio
    async def test_update_broadcast_called(self, mock_db_context, mock_ws_manager):
        """Test that canvas update triggers WebSocket broadcast."""
        from tools.canvas_tool import update_canvas

        result = await update_canvas(
            user_id="test_user",
            canvas_id="canvas-123",
            updates={"title": "Updated Title"}
        )

        assert result["success"] is True
        assert mock_ws_manager.broadcast.called

        # Verify broadcast includes update action
        call_args = mock_ws_manager.broadcast.call_args
        assert call_args[0][1]["data"]["action"] == "update"

    @pytest.mark.asyncio
    async def test_close_broadcast_called(self, mock_ws_manager):
        """Test that canvas close triggers WebSocket broadcast."""
        from tools.canvas_tool import close_canvas

        result = await close_canvas(user_id="test_user")

        assert result["success"] is True
        assert mock_ws_manager.broadcast.called

        # Verify broadcast includes close action
        call_args = mock_ws_manager.broadcast.call_args
        assert call_args[0][1]["data"]["action"] == "close"


# ============================================================================
# Test WebSocket Routing
# ============================================================================

class TestWebSocketRouting:
    """Test WebSocket channel routing for canvas operations."""

    @pytest.mark.asyncio
    async def test_user_channel_routing(self, mock_db_context, mock_ws_manager):
        """Test that canvas broadcasts to correct user channel."""
        from tools.canvas_tool import present_chart

        await present_chart(
            user_id="user-12345",
            chart_type="line_chart",
            data=[{"x": 1, "y": 2}]
        )

        call_args = mock_ws_manager.broadcast.call_args
        assert "user:user-12345" in call_args[0][0]

    @pytest.mark.asyncio
    async def test_session_channel_routing(self, mock_db_context, mock_ws_manager):
        """Test that canvas broadcasts to session-specific channel."""
        from tools.canvas_tool import present_chart

        await present_chart(
            user_id="test_user",
            chart_type="line_chart",
            data=[{"x": 1, "y": 2}],
            session_id="session-abc"
        )

        call_args = mock_ws_manager.broadcast.call_args
        # Should include session ID in channel
        assert "session:session-abc" in call_args[0][0]

    @pytest.mark.asyncio
    async def test_multiple_sessions_isolated(self, mock_db_context, mock_ws_manager):
        """Test that different sessions are isolated."""
        from tools.canvas_tool import present_chart

        # Present to session 1
        await present_chart(
            user_id="test_user",
            chart_type="line_chart",
            data=[{"x": 1, "y": 2}],
            session_id="session-1"
        )

        # Present to session 2
        await present_chart(
            user_id="test_user",
            chart_type="bar_chart",
            data=[{"category": "A", "value": 100}],
            session_id="session-2"
        )

        # Verify two separate calls
        assert mock_ws_manager.broadcast.call_count == 2

        # Check first call (session-1)
        first_call = mock_ws_manager.broadcast.call_args_list[0]
        assert "session:session-1" in first_call[0][0]

        # Check second call (session-2)
        second_call = mock_ws_manager.broadcast.call_args_list[1]
        assert "session:session-2" in second_call[0][0]


# ============================================================================
# Test WebSocket Error Handling
# ============================================================================

class TestWebSocketErrorHandling:
    """Test WebSocket error handling."""

    @pytest.mark.asyncio
    async def test_broadcast_failure_graceful_degradation(self, mock_db_context):
        """Test that broadcast failures are handled gracefully."""
        from tools.canvas_tool import present_chart

        # Mock broadcast to raise exception
        with patch('tools.canvas_tool.ws_manager') as mock_mgr:
            mock_mgr.broadcast = AsyncMock(side_effect=Exception("WebSocket connection failed"))

            result = await present_chart(
                user_id="test_user",
                chart_type="line_chart",
                data=[{"x": 1, "y": 2}]
            )

            # Should handle error and return failure result
            assert isinstance(result, dict)
            assert "success" in result

    @pytest.mark.asyncio
    async def test_broadcast_timeout_handling(self, mock_db_context):
        """Test handling of broadcast timeouts."""
        from tools.canvas_tool import present_chart

        # Mock broadcast to timeout
        with patch('tools.canvas_tool.ws_manager') as mock_mgr:
            mock_mgr.broadcast = AsyncMock(side_effect=TimeoutError("Broadcast timeout"))

            result = await present_chart(
                user_id="test_user",
                chart_type="line_chart",
                data=[{"x": 1, "y": 2}]
            )

            # Should handle timeout gracefully
            assert isinstance(result, dict)

    @pytest.mark.asyncio
    async def test_multiple_broadcast_retries(self, mock_db_context, mock_ws_manager):
        """Test that broadcast attempts are made (if retry logic exists)."""
        from tools.canvas_tool import present_chart

        # Mock broadcast to fail once then succeed
        call_count = [0]

        async def failing_then_succeeding(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                raise Exception("Temporary failure")
            return None

        mock_ws_manager.broadcast.side_effect = failing_then_succeeding

        result = await present_chart(
            user_id="test_user",
            chart_type="line_chart",
            data=[{"x": 1, "y": 2}]
        )

        # Function should complete (may or may not have retry logic)
        assert isinstance(result, dict)


# ============================================================================
# Test WebSocket Data Integrity
# ============================================================================

class TestWebSocketDataIntegrity:
    """Test WebSocket broadcast data integrity."""

    @pytest.mark.asyncio
    async def test_chart_data_integrity(self, mock_db_context, mock_ws_manager):
        """Test that chart data is correctly passed through WebSocket."""
        from tools.canvas_tool import present_chart

        chart_data = [
            {"x": "Jan", "y": 100},
            {"x": "Feb", "y": 200},
            {"x": "Mar", "y": 150}
        ]

        await present_chart(
            user_id="test_user",
            chart_type="line_chart",
            data=chart_data,
            title="Monthly Sales"
        )

        call_args = mock_ws_manager.broadcast.call_args
        broadcast_data = call_args[0][1]["data"]["data"]

        # Verify data integrity
        assert broadcast_data["data"] == chart_data
        assert broadcast_data["title"] == "Monthly Sales"

    @pytest.mark.asyncio
    async def test_form_schema_integrity(self, mock_db_context, mock_ws_manager):
        """Test that form schema is correctly passed through WebSocket."""
        from tools.canvas_tool import present_form

        form_schema = {
            "fields": [
                {"name": "email", "type": "email", "required": True},
                {"name": "age", "type": "number", "min": 18}
            ]
        }

        await present_form(
            user_id="test_user",
            form_schema=form_schema,
            title="User Form"
        )

        call_args = mock_ws_manager.broadcast.call_args
        broadcast_schema = call_args[0][1]["data"]["data"]["schema"]

        # Verify schema integrity
        assert broadcast_schema == form_schema

    @pytest.mark.asyncio
    async def test_canvas_id_consistency(self, mock_db_context, mock_ws_manager):
        """Test that canvas_id is consistent across presentation and audit."""
        from tools.canvas_tool import present_chart

        result = await present_chart(
            user_id="test_user",
            chart_type="line_chart",
            data=[{"x": 1, "y": 2}]
        )

        canvas_id = result["canvas_id"]

        # Verify canvas_id in broadcast (it's at data["canvas_id"], not data["data"]["canvas_id"])
        call_args = mock_ws_manager.broadcast.call_args
        broadcast_canvas_id = call_args[0][1]["data"]["canvas_id"]

        assert canvas_id == broadcast_canvas_id