66import pytest
77
88from ai_server .chain .question_generation_chain import GeneratedQuestionPool
9+ from ai_server .core .client import EmbeddingSearchHit
910from ai_server .messaging .consumers .questions_consumer import (
1011 QuestionsConsumer ,
1112 _build_context ,
@@ -85,6 +86,7 @@ async def test_consumer_generates_questions_and_publishes_callback():
8586 }
8687 ],
8788 "maxQuestions" : 5 ,
89+ "initialQuestionCount" : 2 ,
8890 }
8991 )
9092 await consumer .handle (_StubMessage (body ))
@@ -93,8 +95,8 @@ async def test_consumer_generates_questions_and_publishes_callback():
9395 call = generator .generate .await_args
9496 assert call .kwargs ["job_category" ] == "BACKEND"
9597 assert call .kwargs ["mode" ] == "TECHNICAL"
96- # envelope.max_questions(=5) 무시하고 initial_pool_size(default 1) 로 강제. Core 가 첫 질문만 사용 .
97- assert call .kwargs ["max_questions" ] == 1
98+ # maxQuestions is the session limit; initialQuestionCount controls this result .
99+ assert call .kwargs ["max_questions" ] == 2
98100 assert "Java" in call .kwargs ["context" ]
99101
100102 publisher .publish .assert_awaited_once ()
@@ -137,6 +139,166 @@ async def test_consumer_skips_when_message_id_already_seen():
137139 publisher .publish .assert_not_awaited ()
138140
139141
142+ @pytest .mark .asyncio
143+ async def test_consumer_defaults_initial_question_count_to_one ():
144+ generator = MagicMock ()
145+ generator .generate = AsyncMock (return_value = GeneratedQuestionPool (questions = []))
146+ publisher = MagicMock ()
147+ publisher .publish = AsyncMock ()
148+
149+ consumer = QuestionsConsumer (
150+ generator = generator ,
151+ publisher = publisher ,
152+ idempotency = LruIdempotencyStore (max_size = 10 ),
153+ callback_routing_key = "callback.questions" ,
154+ initial_pool_size = 3 ,
155+ )
156+ body = _envelope (
157+ {
158+ "sessionId" : 99 ,
159+ "mode" : "TECHNICAL" ,
160+ "jobCategory" : "BACKEND" ,
161+ "documents" : [],
162+ "maxQuestions" : 5 ,
163+ }
164+ )
165+ await consumer .handle (_StubMessage (body ))
166+
167+ assert generator .generate .await_args .kwargs ["max_questions" ] == 1
168+
169+
170+ @pytest .mark .asyncio
171+ async def test_consumer_clamps_initial_question_count_to_at_least_one ():
172+ generator = MagicMock ()
173+ generator .generate = AsyncMock (return_value = GeneratedQuestionPool (questions = []))
174+ publisher = MagicMock ()
175+ publisher .publish = AsyncMock ()
176+
177+ consumer = QuestionsConsumer (
178+ generator = generator ,
179+ publisher = publisher ,
180+ idempotency = LruIdempotencyStore (max_size = 10 ),
181+ callback_routing_key = "callback.questions" ,
182+ initial_pool_size = 3 ,
183+ )
184+ body = _envelope (
185+ {
186+ "sessionId" : 99 ,
187+ "mode" : "TECHNICAL" ,
188+ "jobCategory" : "BACKEND" ,
189+ "documents" : [],
190+ "initialQuestionCount" : 0 ,
191+ "maxQuestions" : 5 ,
192+ }
193+ )
194+ await consumer .handle (_StubMessage (body ))
195+
196+ assert generator .generate .await_args .kwargs ["max_questions" ] == 1
197+
198+
199+ @pytest .mark .asyncio
200+ async def test_consumer_injects_initial_rag_chunks_when_available ():
201+ generator = MagicMock ()
202+ generator .generate = AsyncMock (return_value = GeneratedQuestionPool (questions = []))
203+ publisher = MagicMock ()
204+ publisher .publish = AsyncMock ()
205+ core = MagicMock ()
206+ core .search_embeddings = AsyncMock (
207+ return_value = [
208+ EmbeddingSearchHit (
209+ document_id = 1 ,
210+ chunk_index = 4 ,
211+ chunk_text = "Outbox table uses status and retry count" ,
212+ distance = 0.11 ,
213+ )
214+ ]
215+ )
216+ embedder = MagicMock ()
217+ embedder .embed = AsyncMock (return_value = [[0.1 , 0.2 ]])
218+
219+ consumer = QuestionsConsumer (
220+ generator = generator ,
221+ publisher = publisher ,
222+ idempotency = LruIdempotencyStore (max_size = 10 ),
223+ callback_routing_key = "callback.questions" ,
224+ core_client = core ,
225+ embedder = embedder ,
226+ rag_top_k = 2 ,
227+ )
228+ body = _envelope (
229+ {
230+ "sessionId" : 99 ,
231+ "mode" : "TECHNICAL" ,
232+ "jobCategory" : "BACKEND" ,
233+ "documents" : [
234+ {
235+ "documentId" : 1 ,
236+ "sourceType" : "REPOSITORY" ,
237+ "summary" : "outbox 구현" ,
238+ "techStack" : ["Spring" ],
239+ "markdown" : "transactional publisher" ,
240+ }
241+ ],
242+ "initialQuestionCount" : 1 ,
243+ "maxQuestions" : 5 ,
244+ }
245+ )
246+ await consumer .handle (_StubMessage (body ))
247+
248+ embedder .embed .assert_awaited_once ()
249+ core .search_embeddings .assert_awaited_once_with (
250+ query_embedding = [0.1 , 0.2 ],
251+ document_ids = [1 ],
252+ top_k = 2 ,
253+ )
254+ context = generator .generate .await_args .kwargs ["context" ]
255+ assert "Outbox table uses status and retry count" in context
256+ assert "outbox 구현" in context
257+
258+
259+ @pytest .mark .asyncio
260+ async def test_consumer_falls_back_to_document_context_when_rag_fails ():
261+ generator = MagicMock ()
262+ generator .generate = AsyncMock (return_value = GeneratedQuestionPool (questions = []))
263+ publisher = MagicMock ()
264+ publisher .publish = AsyncMock ()
265+ core = MagicMock ()
266+ core .search_embeddings = AsyncMock (side_effect = RuntimeError ("core down" ))
267+ embedder = MagicMock ()
268+ embedder .embed = AsyncMock (return_value = [[0.1 , 0.2 ]])
269+
270+ consumer = QuestionsConsumer (
271+ generator = generator ,
272+ publisher = publisher ,
273+ idempotency = LruIdempotencyStore (max_size = 10 ),
274+ callback_routing_key = "callback.questions" ,
275+ core_client = core ,
276+ embedder = embedder ,
277+ )
278+ body = _envelope (
279+ {
280+ "sessionId" : 99 ,
281+ "mode" : "TECHNICAL" ,
282+ "jobCategory" : "BACKEND" ,
283+ "documents" : [
284+ {
285+ "documentId" : 1 ,
286+ "sourceType" : "RESUME" ,
287+ "summary" : "Java/Spring backend" ,
288+ "techStack" : ["Java" ],
289+ "markdown" : "payment service" ,
290+ }
291+ ],
292+ "maxQuestions" : 5 ,
293+ }
294+ )
295+ await consumer .handle (_StubMessage (body ))
296+
297+ context = generator .generate .await_args .kwargs ["context" ]
298+ assert "Java/Spring backend" in context
299+ assert "Retrieved document chunks" not in context
300+
301+
140302def test_build_context_handles_empty_documents ():
141303 assert _build_context ([]) == "(no documents)"
142304
0 commit comments