|
2 | 2 |
|
3 | 3 | from langchain.memory import ConversationBufferMemory |
4 | 4 | from api.prompts.prompt_builder import build_prompt, SYSTEM_INSTRUCTION |
| 5 | +from api.prompts.prompts import LOG_ANALYSIS_INSTRUCTION |
5 | 6 |
|
6 | 7 |
|
7 | 8 | def test_build_prompt_with_full_history_and_context(): |
@@ -76,6 +77,182 @@ def test_build_prompt_with_none_memory(): |
76 | 77 | assert context in context_section |
77 | 78 | assert user_query in question_section |
78 | 79 |
|
| 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 | + |
79 | 256 | def get_prompt_indexes(prompt: str) -> tuple[int, int, int, int]: |
80 | 257 | """Helper to extract section positions in the prompt.""" |
81 | 258 | chat_idx = prompt.index("Chat History:") |
|
0 commit comments