11"""Tests for Celery tasks."""
22
3+ import pytest
4+
35from src .celery_app import celery_app
4- from src .tasks import hello_world
6+ from src .tasks import (
7+ build_classify_and_update_chain ,
8+ classify_turn_message ,
9+ update_turn_message_label ,
10+ )
11+ from src .turn_client import TurnAPIClientError , TurnAPIServerError
512
613# Enable eager mode for all tests in this module (run tasks synchronously)
714celery_app .conf .task_always_eager = True
815celery_app .conf .task_eager_propagates = True
916
1017
11- class TestHelloWorldTask :
12- """Tests for the hello_world task."""
13-
14- def test_hello_world_default (self ):
15- """Test hello world task with default parameter."""
16- result = hello_world ()
17- assert result == {"status" : "success" , "message" : "Hello, World!" }
18-
19- def test_hello_world_custom_name (self ):
20- """Test hello world task with custom name."""
21- result = hello_world ("Celery" )
22- assert result == {"status" : "success" , "message" : "Hello, Celery!" }
23-
24- def test_hello_world_synchronous (self ):
25- """Test hello world task called synchronously."""
26- result = hello_world ("Testing" )
27- assert result == {"status" : "success" , "message" : "Hello, Testing!" }
28-
29-
3018class TestCeleryConfiguration :
3119 """Tests for Celery app configuration."""
3220
@@ -45,3 +33,150 @@ def test_task_serializer_is_json(self):
4533 """Test that task serializer is set to JSON."""
4634 assert celery_app .conf .task_serializer == "json"
4735 assert "json" in celery_app .conf .accept_content
36+
37+
38+ class TestClassifyTurnMessageTask :
39+ """Tests for the classify_turn_message task."""
40+
41+ def test_classify_success (self , mocker ):
42+ """Test successful classification."""
43+ # Mock classifier
44+ mock_classifier = mocker .Mock ()
45+ mock_classifier .classify .return_value = ("compliment" , 0.95 )
46+ mocker .patch ("src.tasks.classifier" , mock_classifier )
47+
48+ # Execute task
49+ result = classify_turn_message ("msg-123" , "Thank you so much!" )
50+
51+ # Verify classifier was called
52+ mock_classifier .classify .assert_called_once_with ("Thank you so much!" )
53+
54+ # Verify result
55+ assert result ["message_id" ] == "msg-123"
56+ assert result ["intent" ] == "compliment"
57+ assert result ["confidence" ] == 0.95
58+
59+ def test_classify_classifier_not_loaded (self , mocker ):
60+ """Test error when classifier is not loaded."""
61+ # Mock classifier as None (not loaded)
62+ mocker .patch ("src.tasks.classifier" , None )
63+
64+ # Execute task - should raise ValueError
65+ with pytest .raises (ValueError , match = "Intent classifier not loaded" ):
66+ classify_turn_message ("msg-789" , "Hello world" )
67+
68+ def test_classify_unclassified_intent (self , mocker ):
69+ """Test handling of unclassified intent."""
70+ # Mock classifier to return Unclassified
71+ mock_classifier = mocker .Mock ()
72+ mock_classifier .classify .return_value = ("Unclassified" , 0.42 )
73+ mocker .patch ("src.tasks.classifier" , mock_classifier )
74+
75+ # Execute task
76+ result = classify_turn_message ("msg-unclass" , "asdfghjkl" )
77+
78+ # Verify result
79+ assert result ["intent" ] == "Unclassified"
80+ assert result ["confidence" ] == 0.42
81+
82+
83+ class TestUpdateTurnMessageLabelTask :
84+ """Tests for the update_turn_message_label task."""
85+
86+ def test_update_turn_label_success (self , mocker , monkeypatch ):
87+ """Test successful Turn API update."""
88+
89+ # Mock Turn API client
90+ mock_turn_client = mocker .Mock ()
91+ mock_turn_client .update_message_label .return_value = {
92+ "status" : "success" ,
93+ "message_id" : "msg-123" ,
94+ }
95+ mocker .patch ("src.tasks.TurnAPIClient" , return_value = mock_turn_client )
96+
97+ # Execute task
98+ result = update_turn_message_label (
99+ {"message_id" : "msg-123" , "intent" : "compliment" }
100+ )
101+
102+ # Verify Turn API was called
103+ mock_turn_client .update_message_label .assert_called_once_with (
104+ "msg-123" , "compliment"
105+ )
106+
107+ # Verify result
108+ assert result ["message_id" ] == "msg-123"
109+ assert result ["intent" ] == "compliment"
110+ assert result ["turn_response" ]["status" ] == "success"
111+
112+ def test_update_turn_label_client_error (self , mocker , monkeypatch ):
113+ """Test that 4xx client errors do NOT trigger retry."""
114+ # Set up Turn API env vars
115+ monkeypatch .setenv ("TURN_API_BASE_URL" , "https://api.turn.io" )
116+ monkeypatch .setenv ("TURN_API_TOKEN" , "test-token" )
117+
118+ # Mock Turn API client to raise client error (4xx)
119+ mock_turn_client = mocker .Mock ()
120+ mock_turn_client .update_message_label .side_effect = TurnAPIClientError (
121+ "Turn API client error (HTTP 400): Invalid label"
122+ )
123+ mocker .patch ("src.tasks.TurnAPIClient" , return_value = mock_turn_client )
124+
125+ # Execute task - should raise TurnAPIClientError without retry
126+ with pytest .raises (TurnAPIClientError , match = "Turn API client error" ):
127+ update_turn_message_label ({"message_id" : "msg-400" , "intent" : "greeting" })
128+
129+ # Verify Turn API was called only once (no retry)
130+ mock_turn_client .update_message_label .assert_called_once ()
131+
132+ def test_update_turn_label_server_error_retry (self , mocker , monkeypatch ):
133+ """Test that 5xx server errors trigger automatic retry."""
134+ # Set up Turn API env vars
135+ monkeypatch .setenv ("TURN_API_BASE_URL" , "https://api.turn.io" )
136+ monkeypatch .setenv ("TURN_API_TOKEN" , "test-token" )
137+
138+ # Mock Turn API client to raise server error (5xx)
139+ mock_turn_client = mocker .Mock ()
140+ mock_turn_client .update_message_label .side_effect = TurnAPIServerError (
141+ "Turn API server error (HTTP 500): Internal server error"
142+ )
143+ mocker .patch ("src.tasks.TurnAPIClient" , return_value = mock_turn_client )
144+
145+ # Execute task - should raise TurnAPIServerError (retry will be handled by Celery)
146+ with pytest .raises (TurnAPIServerError , match = "Turn API server error" ):
147+ update_turn_message_label ({"message_id" : "msg-500" , "intent" : "question" })
148+
149+ # Verify Turn API was called
150+ mock_turn_client .update_message_label .assert_called_once ()
151+
152+
153+ class TestClassifyAndUpdateChain :
154+ """Tests for the classify and update chain helper."""
155+
156+ def test_build_chain_signatures (self ):
157+ """Ensure chain is composed of classify then update tasks."""
158+ chain_sig = build_classify_and_update_chain ("msg-123" , "Hello there" )
159+
160+ assert len (chain_sig .tasks ) == 2
161+ assert chain_sig .tasks [0 ].name == classify_turn_message .name
162+ assert chain_sig .tasks [1 ].name == update_turn_message_label .name
163+ assert chain_sig .tasks [0 ].args == ("msg-123" , "Hello there" )
164+ assert chain_sig .tasks [1 ].args == ()
165+
166+ def test_chain_passes_result_to_update (self , mocker ):
167+ """Ensure classifier result is passed to update_turn_message_label."""
168+ mock_classifier = mocker .Mock ()
169+ mock_classifier .classify .return_value = ("compliment" , 0.95 )
170+ mocker .patch ("src.tasks.classifier" , mock_classifier )
171+
172+ mock_update = mocker .patch (
173+ "src.tasks.update_turn_message_label.run" ,
174+ return_value = {"status" : "success" },
175+ )
176+
177+ chain_sig = build_classify_and_update_chain ("msg-123" , "Thank you!" )
178+ chain_sig .apply ()
179+
180+ mock_update .assert_called_once_with (
181+ {"message_id" : "msg-123" , "intent" : "compliment" , "confidence" : 0.95 }
182+ )
0 commit comments