Skip to content

Commit 4b105b8

Browse files
test: expand prompt_builder unit tests for log_context, multi-turn hi… (#204)
* test: expand prompt_builder unit tests for log_context, multi-turn history, and edge cases * test: add tests for handling whitespace-only log context in prompt builder --------- Co-authored-by: Bervianto Leo Pratama <[email protected]>
1 parent d03d050 commit 4b105b8

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

chatbot-core/tests/unit/prompts/test_prompt_builder.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from langchain.memory import ConversationBufferMemory
44
from api.prompts.prompt_builder import build_prompt, SYSTEM_INSTRUCTION
5+
from api.prompts.prompts import LOG_ANALYSIS_INSTRUCTION
56

67

78
def test_build_prompt_with_full_history_and_context():
@@ -76,6 +77,182 @@ def test_build_prompt_with_none_memory():
7677
assert context in context_section
7778
assert user_query in question_section
7879

80+
def test_build_prompt_with_log_context_uses_log_analysis_instruction():
81+
"""Test that providing log_context switches to LOG_ANALYSIS_INSTRUCTION
82+
and includes the 'User-Provided Log Data' section in the prompt."""
83+
memory = ConversationBufferMemory(return_messages=True)
84+
context = "Jenkins pipeline documentation."
85+
user_query = "Why did my build fail?"
86+
log_context = "ERROR: Build step 'Execute shell' marked build as failure"
87+
88+
prompt = build_prompt(user_query, context, memory, log_context=log_context)
89+
90+
# Should use LOG_ANALYSIS_INSTRUCTION, NOT SYSTEM_INSTRUCTION
91+
assert LOG_ANALYSIS_INSTRUCTION.strip() in prompt
92+
assert SYSTEM_INSTRUCTION.strip() not in prompt
93+
94+
# Log section must be present with the actual log data
95+
assert "User-Provided Log Data:" in prompt
96+
assert log_context in prompt
97+
98+
# Standard structural sections still present and in order
99+
chat_idx, context_idx, question_idx, answer_idx = get_prompt_indexes(prompt)
100+
assert chat_idx < context_idx < question_idx < answer_idx
101+
102+
# User query still appears correctly
103+
_, _, question_section = get_prompt_sections(prompt)
104+
assert user_query.strip() in question_section
105+
106+
107+
def test_build_prompt_with_log_context_and_history():
108+
"""Test the full combination: log_context + conversation history + context."""
109+
memory = ConversationBufferMemory(return_messages=True)
110+
memory.chat_memory.add_user_message("My build failed.") # pylint: disable=no-member
111+
memory.chat_memory.add_ai_message("Can you share the logs?") # pylint: disable=no-member
112+
context = "Check Jenkins console output for errors."
113+
user_query = "Here are my logs, can you analyze?"
114+
log_context = "java.lang.OutOfMemoryError: Java heap space"
115+
116+
prompt = build_prompt(user_query, context, memory, log_context=log_context)
117+
118+
# Uses log analysis instruction
119+
assert LOG_ANALYSIS_INSTRUCTION.strip() in prompt
120+
assert SYSTEM_INSTRUCTION.strip() not in prompt
121+
122+
# History is preserved
123+
history_section, context_section, question_section = get_prompt_sections(prompt)
124+
assert "User: My build failed." in history_section
125+
assert "Jenkins Assistant: Can you share the logs?" in history_section
126+
127+
# Context and log data are both present
128+
assert context in context_section
129+
assert "User-Provided Log Data:" in prompt
130+
assert log_context in prompt
131+
132+
# Question appears correctly
133+
assert user_query.strip() in question_section
134+
135+
136+
def test_build_prompt_with_empty_string_log_context_uses_system_instruction():
137+
"""Test that an empty string log_context (falsy) does NOT trigger the
138+
log analysis branch — should fall back to SYSTEM_INSTRUCTION."""
139+
memory = ConversationBufferMemory(return_messages=True)
140+
context = "Some context."
141+
user_query = "How do I configure agents?"
142+
143+
prompt = build_prompt(user_query, context, memory, log_context="")
144+
145+
# Should use standard instruction since "" is falsy
146+
assert SYSTEM_INSTRUCTION.strip() in prompt
147+
assert LOG_ANALYSIS_INSTRUCTION.strip() not in prompt
148+
assert "User-Provided Log Data:" not in prompt
149+
150+
151+
def test_build_prompt_with_none_log_context_uses_system_instruction():
152+
"""Test that log_context=None (the default) does NOT trigger the
153+
log analysis branch."""
154+
memory = ConversationBufferMemory(return_messages=True)
155+
context = "Some context."
156+
user_query = "How do I set up a pipeline?"
157+
158+
prompt = build_prompt(user_query, context, memory, log_context=None)
159+
160+
assert SYSTEM_INSTRUCTION.strip() in prompt
161+
assert LOG_ANALYSIS_INSTRUCTION.strip() not in prompt
162+
assert "User-Provided Log Data:" not in prompt
163+
164+
def test_build_prompt_with_multiple_conversation_turns():
165+
"""Test that multiple rounds of user/assistant messages are all
166+
captured in the Chat History section in the correct order."""
167+
memory = ConversationBufferMemory(return_messages=True)
168+
memory.chat_memory.add_user_message("What is a Jenkinsfile?") # pylint: disable=no-member
169+
memory.chat_memory.add_ai_message("A Jenkinsfile defines your pipeline.") # pylint: disable=no-member
170+
memory.chat_memory.add_user_message("Can I use it with GitHub?") # pylint: disable=no-member
171+
memory.chat_memory.add_ai_message("Yes, you can integrate it with GitHub.") # pylint: disable=no-member
172+
context = "Jenkinsfile supports declarative and scripted syntax."
173+
user_query = "Show me an example."
174+
175+
prompt = build_prompt(user_query, context, memory)
176+
177+
history_section, _, _ = get_prompt_sections(prompt)
178+
179+
# All four messages must appear in history
180+
assert "User: What is a Jenkinsfile?" in history_section
181+
assert "Jenkins Assistant: A Jenkinsfile defines your pipeline." in history_section
182+
assert "User: Can I use it with GitHub?" in history_section
183+
assert "Jenkins Assistant: Yes, you can integrate it with GitHub." in history_section
184+
185+
# Order: first user message appears before second user message
186+
first_user_pos = history_section.index("User: What is a Jenkinsfile?")
187+
second_user_pos = history_section.index("User: Can I use it with GitHub?")
188+
assert first_user_pos < second_user_pos
189+
190+
191+
def test_build_prompt_with_none_content_message():
192+
"""Test that a message with None content is handled gracefully
193+
(the `msg.content or ''` guard in the source)."""
194+
memory = ConversationBufferMemory(return_messages=True)
195+
memory.chat_memory.add_user_message("Hello") # pylint: disable=no-member
196+
# Simulate a None content message by directly manipulating memory
197+
memory.chat_memory.messages[-1].content = None # pylint: disable=no-member
198+
context = "Jenkins docs."
199+
user_query = "Test query"
200+
201+
prompt = build_prompt(user_query, context, memory)
202+
203+
history_section, _, _ = get_prompt_sections(prompt)
204+
# Should show "User: " with empty content, not crash
205+
assert "User: " in history_section
206+
assert "Hello" not in history_section
207+
208+
209+
def test_build_prompt_with_special_characters_in_query():
210+
"""Test that special characters (unicode, newlines) in the user query
211+
are preserved correctly in the output prompt."""
212+
memory = ConversationBufferMemory(return_messages=True)
213+
context = "Jenkins configuration docs."
214+
user_query = " How do I use the 'pipeline' with \"quotes\" & <angle> brackets?\n "
215+
216+
prompt = build_prompt(user_query, context, memory)
217+
218+
_, _, question_section = get_prompt_sections(prompt)
219+
assert user_query.strip() in question_section
220+
221+
222+
def test_build_prompt_log_data_section_appears_between_context_and_question():
223+
"""Test that the 'User-Provided Log Data' section is placed
224+
after the context section and before the user question."""
225+
memory = ConversationBufferMemory(return_messages=True)
226+
context = "Pipeline troubleshooting guide."
227+
user_query = "Analyze this error."
228+
log_context = "FATAL: command execution failed"
229+
230+
prompt = build_prompt(user_query, context, memory, log_context=log_context)
231+
232+
context_idx = prompt.index("Context (Documentation & Knowledge Base):")
233+
log_data_idx = prompt.index("User-Provided Log Data:")
234+
question_idx = prompt.index("User Question:")
235+
236+
# Log data must appear AFTER context and BEFORE question
237+
assert context_idx < log_data_idx < question_idx
238+
239+
def test_build_prompt_with_whitespace_only_log_context_triggers_log_branch():
240+
"""Test that a whitespace-only log_context is truthy and triggers the
241+
log analysis branch. This is the current intended behavior — the
242+
log_context check uses `if log_context:` which treats non-empty
243+
whitespace strings as truthy. If this behavior should change,
244+
the source should be updated to use `if log_context and log_context.strip():`."""
245+
memory = ConversationBufferMemory(return_messages=True)
246+
context = "Some context."
247+
user_query = "Why did my build fail?"
248+
249+
prompt = build_prompt(user_query, context, memory, log_context=" ")
250+
251+
# Whitespace-only string is truthy, so log analysis branch is triggered
252+
assert LOG_ANALYSIS_INSTRUCTION.strip() in prompt
253+
assert SYSTEM_INSTRUCTION.strip() not in prompt
254+
assert "User-Provided Log Data:" in prompt
255+
79256
def get_prompt_indexes(prompt: str) -> tuple[int, int, int, int]:
80257
"""Helper to extract section positions in the prompt."""
81258
chat_idx = prompt.index("Chat History:")

0 commit comments

Comments
 (0)