philjosephcohen commited on
Commit
b6ca1e8
·
1 Parent(s): fdc9b46

improve CI: fix tests and better reports to slack

Browse files
.github/workflows/test.yml CHANGED
@@ -27,6 +27,9 @@ jobs:
27
  python -m pip install --upgrade pip
28
  pip install -r requirements_minimal.txt
29
  pip install llamafirewall
 
 
 
30
  pip install nemoguardrails
31
  pip install presidio-analyzer presidio-anonymizer
32
 
@@ -111,15 +114,15 @@ jobs:
111
  id: test_alignment_check
112
  continue-on-error: true
113
  env:
114
- TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
115
  run: |
116
- if [ -z "$TOGETHER_API_KEY" ]; then
117
- echo "⏭️ Skipping AlignmentCheck test (TOGETHER_API_KEY not configured)"
118
- echo " To enable: Add TOGETHER_API_KEY to GitHub repository secrets"
119
  echo "outcome=skipped" >> $GITHUB_OUTPUT
120
  exit 0
121
  fi
122
- echo "Running AlignmentCheck test with API key..."
123
  python test_alignment_check.py
124
  TEST_RESULT=$?
125
  if [ $TEST_RESULT -eq 0 ]; then
@@ -221,12 +224,13 @@ jobs:
221
  # Optional tests (AlignmentCheck - requires API key)
222
  # Test 1: AlignmentCheck dual dimensions
223
  ALIGNMENT_DUAL_OUTCOME="${{ steps.test_alignment_dual_dimensions.outputs.outcome }}"
 
224
  if [ "$ALIGNMENT_DUAL_OUTCOME" == "skipped" ]; then
225
  SKIPPED=$((SKIPPED + 1))
226
  elif [ "$ALIGNMENT_DUAL_OUTCOME" == "success" ]; then
227
  PASSED=$((PASSED + 1))
228
  TOTAL=$((TOTAL + 1))
229
- elif [ "$ALIGNMENT_DUAL_OUTCOME" == "failure" ]; then
230
  FAILED=$((FAILED + 1))
231
  TOTAL=$((TOTAL + 1))
232
  FAILED_TESTS="${FAILED_TESTS}• test_alignment_dual_dimensions.py\n"
@@ -234,12 +238,13 @@ jobs:
234
 
235
  # Test 2: AlignmentCheck vs FactChecker separation
236
  ALIGNMENT_VS_FACTCHECKER_OUTCOME="${{ steps.test_alignment_vs_factchecker.outputs.outcome }}"
 
237
  if [ "$ALIGNMENT_VS_FACTCHECKER_OUTCOME" == "skipped" ]; then
238
  SKIPPED=$((SKIPPED + 1))
239
  elif [ "$ALIGNMENT_VS_FACTCHECKER_OUTCOME" == "success" ]; then
240
  PASSED=$((PASSED + 1))
241
  TOTAL=$((TOTAL + 1))
242
- elif [ "$ALIGNMENT_VS_FACTCHECKER_OUTCOME" == "failure" ]; then
243
  FAILED=$((FAILED + 1))
244
  TOTAL=$((TOTAL + 1))
245
  FAILED_TESTS="${FAILED_TESTS}• test_alignment_vs_factchecker.py\n"
@@ -247,12 +252,13 @@ jobs:
247
 
248
  # Test 3: AlignmentCheck scanner test
249
  ALIGNMENT_OUTCOME="${{ steps.test_alignment_check.outputs.outcome }}"
 
250
  if [ "$ALIGNMENT_OUTCOME" == "skipped" ]; then
251
  SKIPPED=$((SKIPPED + 1))
252
  elif [ "$ALIGNMENT_OUTCOME" == "success" ]; then
253
  PASSED=$((PASSED + 1))
254
  TOTAL=$((TOTAL + 1))
255
- elif [ "$ALIGNMENT_OUTCOME" == "failure" ]; then
256
  FAILED=$((FAILED + 1))
257
  TOTAL=$((TOTAL + 1))
258
  FAILED_TESTS="${FAILED_TESTS}• test_alignment_check.py\n"
@@ -260,12 +266,13 @@ jobs:
260
 
261
  # Test 4: Native LlamaFirewall scanner test
262
  NATIVE_LLAMAFIREWALL_OUTCOME="${{ steps.test_native_llamafirewall.outputs.outcome }}"
 
263
  if [ "$NATIVE_LLAMAFIREWALL_OUTCOME" == "skipped" ]; then
264
  SKIPPED=$((SKIPPED + 1))
265
  elif [ "$NATIVE_LLAMAFIREWALL_OUTCOME" == "success" ]; then
266
  PASSED=$((PASSED + 1))
267
  TOTAL=$((TOTAL + 1))
268
- elif [ "$NATIVE_LLAMAFIREWALL_OUTCOME" == "failure" ]; then
269
  FAILED=$((FAILED + 1))
270
  TOTAL=$((TOTAL + 1))
271
  FAILED_TESTS="${FAILED_TESTS}• test_native_llamafirewall_scanner.py\n"
@@ -273,12 +280,14 @@ jobs:
273
 
274
  # Test 5: FactsChecker scanner test
275
  FACTS_CHECKER_OUTCOME="${{ steps.test_facts_checker.outputs.outcome }}"
 
276
  if [ "$FACTS_CHECKER_OUTCOME" == "skipped" ]; then
277
  SKIPPED=$((SKIPPED + 1))
278
  elif [ "$FACTS_CHECKER_OUTCOME" == "success" ]; then
279
  PASSED=$((PASSED + 1))
280
  TOTAL=$((TOTAL + 1))
281
- elif [ "$FACTS_CHECKER_OUTCOME" == "failure" ]; then
 
282
  FAILED=$((FAILED + 1))
283
  TOTAL=$((TOTAL + 1))
284
  FAILED_TESTS="${FAILED_TESTS}• test_facts_checker_scanner.py\n"
@@ -365,7 +374,7 @@ jobs:
365
  "type": "section",
366
  "text": {
367
  "type": "mrkdwn",
368
- "text": "*Core Tests (5):*\n• test_data_disclosure_fix.py: ${{ steps.test_disclosure_fix.outcome }}\n• test_alignment_fix.py: ${{ steps.test_alignment_fix.outcome }}\n• test_user_provided_notification_contact.py: ${{ steps.test_notification_contact.outcome }}\n• test_deviations.py: ${{ steps.test_deviations.outcome }}\n• test_prompt_guard_scanner.py: ${{ steps.test_prompt_guard.outcome }}\n\n*Optional Tests (require API keys):*\n• test_alignment_dual_dimensions.py: ${{ steps.test_alignment_dual_dimensions.outputs.outcome }} (TOGETHER_API_KEY)\n• test_alignment_vs_factchecker.py: ${{ steps.test_alignment_vs_factchecker.outputs.outcome }} (TOGETHER_API_KEY)\n• test_alignment_check.py: ${{ steps.test_alignment_check.outputs.outcome }} (TOGETHER_API_KEY)\n• test_native_llamafirewall_scanner.py: ${{ steps.test_native_llamafirewall.outputs.outcome }} (TOGETHER_API_KEY)\n• test_facts_checker_scanner.py: ${{ steps.test_facts_checker.outputs.outcome }} (OPENAI_API_KEY)"
369
  }
370
  },
371
  {
 
27
  python -m pip install --upgrade pip
28
  pip install -r requirements_minimal.txt
29
  pip install llamafirewall
30
+ # Pin compatible versions to avoid stream_usage parameter error with NeMo GuardRails
31
+ # Issue: Recent OpenAI SDK (1.58.0+) has breaking changes with langchain-community
32
+ pip install "openai<1.58.0"
33
  pip install nemoguardrails
34
  pip install presidio-analyzer presidio-anonymizer
35
 
 
114
  id: test_alignment_check
115
  continue-on-error: true
116
  env:
117
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
118
  run: |
119
+ if [ -z "$OPENAI_API_KEY" ]; then
120
+ echo "⏭️ Skipping AlignmentCheck test (OPENAI_API_KEY not configured)"
121
+ echo " To enable: Add OPENAI_API_KEY to GitHub repository secrets"
122
  echo "outcome=skipped" >> $GITHUB_OUTPUT
123
  exit 0
124
  fi
125
+ echo "Running AlignmentCheck test with GPT-4o-mini..."
126
  python test_alignment_check.py
127
  TEST_RESULT=$?
128
  if [ $TEST_RESULT -eq 0 ]; then
 
224
  # Optional tests (AlignmentCheck - requires API key)
225
  # Test 1: AlignmentCheck dual dimensions
226
  ALIGNMENT_DUAL_OUTCOME="${{ steps.test_alignment_dual_dimensions.outputs.outcome }}"
227
+ ALIGNMENT_DUAL_STEP_OUTCOME="${{ steps.test_alignment_dual_dimensions.outcome }}"
228
  if [ "$ALIGNMENT_DUAL_OUTCOME" == "skipped" ]; then
229
  SKIPPED=$((SKIPPED + 1))
230
  elif [ "$ALIGNMENT_DUAL_OUTCOME" == "success" ]; then
231
  PASSED=$((PASSED + 1))
232
  TOTAL=$((TOTAL + 1))
233
+ elif [ "$ALIGNMENT_DUAL_OUTCOME" == "failure" ] || [ "$ALIGNMENT_DUAL_STEP_OUTCOME" == "failure" ]; then
234
  FAILED=$((FAILED + 1))
235
  TOTAL=$((TOTAL + 1))
236
  FAILED_TESTS="${FAILED_TESTS}• test_alignment_dual_dimensions.py\n"
 
238
 
239
  # Test 2: AlignmentCheck vs FactChecker separation
240
  ALIGNMENT_VS_FACTCHECKER_OUTCOME="${{ steps.test_alignment_vs_factchecker.outputs.outcome }}"
241
+ ALIGNMENT_VS_FACTCHECKER_STEP_OUTCOME="${{ steps.test_alignment_vs_factchecker.outcome }}"
242
  if [ "$ALIGNMENT_VS_FACTCHECKER_OUTCOME" == "skipped" ]; then
243
  SKIPPED=$((SKIPPED + 1))
244
  elif [ "$ALIGNMENT_VS_FACTCHECKER_OUTCOME" == "success" ]; then
245
  PASSED=$((PASSED + 1))
246
  TOTAL=$((TOTAL + 1))
247
+ elif [ "$ALIGNMENT_VS_FACTCHECKER_OUTCOME" == "failure" ] || [ "$ALIGNMENT_VS_FACTCHECKER_STEP_OUTCOME" == "failure" ]; then
248
  FAILED=$((FAILED + 1))
249
  TOTAL=$((TOTAL + 1))
250
  FAILED_TESTS="${FAILED_TESTS}• test_alignment_vs_factchecker.py\n"
 
252
 
253
  # Test 3: AlignmentCheck scanner test
254
  ALIGNMENT_OUTCOME="${{ steps.test_alignment_check.outputs.outcome }}"
255
+ ALIGNMENT_STEP_OUTCOME="${{ steps.test_alignment_check.outcome }}"
256
  if [ "$ALIGNMENT_OUTCOME" == "skipped" ]; then
257
  SKIPPED=$((SKIPPED + 1))
258
  elif [ "$ALIGNMENT_OUTCOME" == "success" ]; then
259
  PASSED=$((PASSED + 1))
260
  TOTAL=$((TOTAL + 1))
261
+ elif [ "$ALIGNMENT_OUTCOME" == "failure" ] || [ "$ALIGNMENT_STEP_OUTCOME" == "failure" ]; then
262
  FAILED=$((FAILED + 1))
263
  TOTAL=$((TOTAL + 1))
264
  FAILED_TESTS="${FAILED_TESTS}• test_alignment_check.py\n"
 
266
 
267
  # Test 4: Native LlamaFirewall scanner test
268
  NATIVE_LLAMAFIREWALL_OUTCOME="${{ steps.test_native_llamafirewall.outputs.outcome }}"
269
+ NATIVE_LLAMAFIREWALL_STEP_OUTCOME="${{ steps.test_native_llamafirewall.outcome }}"
270
  if [ "$NATIVE_LLAMAFIREWALL_OUTCOME" == "skipped" ]; then
271
  SKIPPED=$((SKIPPED + 1))
272
  elif [ "$NATIVE_LLAMAFIREWALL_OUTCOME" == "success" ]; then
273
  PASSED=$((PASSED + 1))
274
  TOTAL=$((TOTAL + 1))
275
+ elif [ "$NATIVE_LLAMAFIREWALL_OUTCOME" == "failure" ] || [ "$NATIVE_LLAMAFIREWALL_STEP_OUTCOME" == "failure" ]; then
276
  FAILED=$((FAILED + 1))
277
  TOTAL=$((TOTAL + 1))
278
  FAILED_TESTS="${FAILED_TESTS}• test_native_llamafirewall_scanner.py\n"
 
280
 
281
  # Test 5: FactsChecker scanner test
282
  FACTS_CHECKER_OUTCOME="${{ steps.test_facts_checker.outputs.outcome }}"
283
+ FACTS_CHECKER_STEP_OUTCOME="${{ steps.test_facts_checker.outcome }}"
284
  if [ "$FACTS_CHECKER_OUTCOME" == "skipped" ]; then
285
  SKIPPED=$((SKIPPED + 1))
286
  elif [ "$FACTS_CHECKER_OUTCOME" == "success" ]; then
287
  PASSED=$((PASSED + 1))
288
  TOTAL=$((TOTAL + 1))
289
+ elif [ "$FACTS_CHECKER_OUTCOME" == "failure" ] || [ "$FACTS_CHECKER_STEP_OUTCOME" == "failure" ]; then
290
+ # Count as failure if either the output says failure OR the step itself failed
291
  FAILED=$((FAILED + 1))
292
  TOTAL=$((TOTAL + 1))
293
  FAILED_TESTS="${FAILED_TESTS}• test_facts_checker_scanner.py\n"
 
374
  "type": "section",
375
  "text": {
376
  "type": "mrkdwn",
377
+ "text": "*Core Tests (5):*\n• test_data_disclosure_fix.py: ${{ steps.test_disclosure_fix.outcome }}\n• test_alignment_fix.py: ${{ steps.test_alignment_fix.outcome }}\n• test_user_provided_notification_contact.py: ${{ steps.test_notification_contact.outcome }}\n• test_deviations.py: ${{ steps.test_deviations.outcome }}\n• test_prompt_guard_scanner.py: ${{ steps.test_prompt_guard.outcome }}\n\n*Optional Tests (require API keys):*\n• test_alignment_dual_dimensions.py: ${{ steps.test_alignment_dual_dimensions.outputs.outcome }} (TOGETHER_API_KEY)\n• test_alignment_vs_factchecker.py: ${{ steps.test_alignment_vs_factchecker.outputs.outcome }} (TOGETHER_API_KEY)\n• test_native_llamafirewall_scanner.py: ${{ steps.test_native_llamafirewall.outputs.outcome }} (TOGETHER_API_KEY)\n• test_alignment_check.py: ${{ steps.test_alignment_check.outputs.outcome }} (OPENAI_API_KEY)\n• test_facts_checker_scanner.py: ${{ steps.test_facts_checker.outputs.outcome }} (OPENAI_API_KEY)"
378
  }
379
  },
380
  {
GITHUB_ACTIONS_ERROR_FIXES.md ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GitHub Actions Error Fixes
2
+
3
+ This document tracks errors encountered in GitHub Actions tests and their fixes.
4
+
5
+ ## Error #1: NeMo GuardRails - stream_usage Parameter Error
6
+
7
+ ### Error Message
8
+ ```
9
+ TypeError: AsyncCompletions.create() got an unexpected keyword argument 'stream_usage'
10
+ WARNING! stream_usage is not default parameter.
11
+ Error invoking LLM (model=gpt-4o-mini): AsyncCompletions.create() got an unexpected keyword argument 'stream_usage'
12
+ ```
13
+
14
+ ### Affected Tests
15
+ - `test_facts_checker_scanner.py` (all 7 tests)
16
+
17
+ ### Root Cause
18
+ Version incompatibility between:
19
+ - OpenAI SDK version 1.58.0+ (breaking changes)
20
+ - langchain-community (used by NeMo GuardRails, hasn't updated yet)
21
+
22
+ ### Fix Applied
23
+ **Pinned OpenAI SDK version** in both:
24
+
25
+ 1. `.github/workflows/test.yml`:
26
+ ```yaml
27
+ pip install "openai<1.58.0" # Avoid stream_usage parameter error
28
+ ```
29
+
30
+ 2. `requirements_minimal.txt`:
31
+ ```python
32
+ openai<1.58.0 # Compatibility with NeMo GuardRails
33
+ ```
34
+
35
+ ### Status
36
+ ✅ **FIXED** - Tests will now use OpenAI SDK < 1.58.0
37
+
38
+ ### Future Action
39
+ When NeMo GuardRails/langchain-community updates for OpenAI 1.58.0+, remove the version pin.
40
+
41
+ ---
42
+
43
+ ## Error #2: AlignmentCheck Test - Missing 'counts' Key
44
+
45
+ ### Error Message
46
+ ```
47
+ ❌ CRITICAL ERROR: Test execution failed: 'counts'
48
+ Error: Process completed with exit code 1.
49
+ Overall Decision: None
50
+ ```
51
+
52
+ ### Affected Tests
53
+ - `test_alignment_check.py` (all 4 tests)
54
+
55
+ ### Root Cause
56
+ The test was accessing `result['counts']` directly without checking if the key exists. When the scan function returns an error (e.g., API key missing or API failure), the result dictionary only contains `{"error": "...", "scanner": "..."}` without the 'counts' key.
57
+
58
+ **Test code before fix:**
59
+ ```python
60
+ result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
61
+ print(f"Counts: Safe={result['counts']['safe']}, ...") # KeyError if 'counts' missing!
62
+ ```
63
+
64
+ ### Fix Applied
65
+ **Added error checking** in `test_alignment_check.py` for all test functions:
66
+
67
+ ```python
68
+ result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
69
+
70
+ # Check for errors first
71
+ if "error" in result:
72
+ print(f"\n❌ ERROR: {result.get('error')}")
73
+ return False
74
+
75
+ print(f"\nOverall Decision: {result.get('overall_decision')}")
76
+ counts = result.get('counts', {}) # Safe access with default
77
+ print(f"Counts: Safe={counts.get('safe', 0)}, Warning={counts.get('warning', 0)}, Block={counts.get('block', 0)}")
78
+ ```
79
+
80
+ ### Changes Made
81
+ Fixed 3 test functions in `test_alignment_check.py`:
82
+ 1. ✅ `test_aligned_conversation()`
83
+ 2. ✅ `test_goal_hijacking()`
84
+ 3. ✅ `test_off_topic_redirect()`
85
+
86
+ ### Status
87
+ ✅ **FIXED** - Tests now handle error cases gracefully
88
+
89
+ ### Why This Happened
90
+ The `scan_alignment_check_per_message()` function returns different result formats:
91
+
92
+ **Success case:**
93
+ ```python
94
+ {
95
+ "scanner": "AlignmentCheck",
96
+ "overall_decision": "SAFE",
97
+ "counts": {"safe": 2, "warning": 0, "block": 0, "total": 2},
98
+ "message_results": [...]
99
+ }
100
+ ```
101
+
102
+ **Error case:**
103
+ ```python
104
+ {
105
+ "error": "OPENAI_API_KEY not configured",
106
+ "scanner": "AlignmentCheck"
107
+ }
108
+ ```
109
+
110
+ The test needs to handle both cases.
111
+
112
+ ---
113
+
114
+ ## Error #3: Slack Failure Notifications Not Sent
115
+
116
+ ### Problem
117
+ When tests failed with exceptions (like KeyError), **only success notifications** were sent to Slack, never failure notifications.
118
+
119
+ ### Root Cause
120
+ **Silent failure detection bug** in the workflow:
121
+
122
+ 1. Test runs and crashes with exception (e.g., KeyError accessing `result['counts']`)
123
+ 2. Python exits with code 1
124
+ 3. Because `continue-on-error: true`, the workflow continues
125
+ 4. BUT: The shell script never reaches `echo "outcome=failure" >> $GITHUB_OUTPUT`
126
+ 5. The output variable is empty/undefined
127
+ 6. "Collect test results" checks: `if [ "$OUTCOME" == "failure" ]`
128
+ 7. Empty string != "failure", so FAILED counter is not incremented
129
+ 8. FAILED = 0, so workflow succeeds ✅ (incorrectly!)
130
+ 9. Only success notification sent, failures silently ignored ❌
131
+
132
+ **Example of the bug:**
133
+ ```yaml
134
+ run: |
135
+ python test_facts_checker_scanner.py # Crashes with KeyError
136
+ TEST_RESULT=$? # Never reached!
137
+ echo "outcome=failure" >> $GITHUB_OUTPUT # Never reached!
138
+ ```
139
+
140
+ ### Fix Applied
141
+ **Check BOTH the output AND the step outcome** in the "Collect test results" step:
142
+
143
+ **Before:**
144
+ ```bash
145
+ FACTS_CHECKER_OUTCOME="${{ steps.test_facts_checker.outputs.outcome }}"
146
+ if [ "$FACTS_CHECKER_OUTCOME" == "failure" ]; then
147
+ FAILED=$((FAILED + 1))
148
+ fi
149
+ ```
150
+
151
+ **After:**
152
+ ```bash
153
+ FACTS_CHECKER_OUTCOME="${{ steps.test_facts_checker.outputs.outcome }}"
154
+ FACTS_CHECKER_STEP_OUTCOME="${{ steps.test_facts_checker.outcome }}"
155
+ if [ "$FACTS_CHECKER_OUTCOME" == "failure" ] || [ "$FACTS_CHECKER_STEP_OUTCOME" == "failure" ]; then
156
+ FAILED=$((FAILED + 1))
157
+ fi
158
+ ```
159
+
160
+ Now we check:
161
+ - ✅ The custom output (if test completed and set it)
162
+ - ✅ The actual step outcome (catches crashes and exceptions)
163
+
164
+ ### Changes Made
165
+ Updated failure detection for all 5 optional tests:
166
+ 1. ✅ test_alignment_dual_dimensions.py
167
+ 2. ✅ test_alignment_vs_factchecker.py
168
+ 3. ✅ test_alignment_check.py
169
+ 4. ✅ test_native_llamafirewall_scanner.py
170
+ 5. ✅ test_facts_checker_scanner.py
171
+
172
+ ### Status
173
+ ✅ **FIXED** - Failures will now be properly detected and reported to Slack
174
+
175
+ ### Why This Is Critical
176
+ Without this fix:
177
+ - ❌ Tests could fail silently
178
+ - ❌ Broken code could be merged
179
+ - ❌ Team wouldn't know about failures
180
+ - ❌ Only saw success messages, creating false confidence
181
+
182
+ With this fix:
183
+ - ✅ All failures are detected
184
+ - ✅ Slack notifications sent for every failure
185
+ - ✅ Team can respond to issues immediately
186
+ - ✅ Proper CI/CD visibility
187
+
188
+ ---
189
+
190
+ ## Summary
191
+
192
+ All three critical errors are now fixed:
193
+
194
+ 1. ✅ **NeMo GuardRails compatibility** - Pinned OpenAI SDK version to avoid `stream_usage` parameter error
195
+ 2. ✅ **Test error handling** - Added proper error checking in tests to prevent KeyError crashes
196
+ 3. ✅ **Slack failure notifications** - Fixed silent failure detection bug so ALL failures are reported
197
+
198
+ ### What Changed
199
+ - **`.github/workflows/test.yml`**:
200
+ - Added OpenAI SDK version pin
201
+ - Fixed failure detection to check both output and step outcome
202
+ - **`test_alignment_check.py`**:
203
+ - Added error handling for missing 'counts' key
204
+ - **`requirements_minimal.txt`**:
205
+ - Added OpenAI SDK version constraint
206
+
207
+ ### Expected Behavior Now
208
+ ✅ Tests will run with compatible dependencies
209
+ ✅ Tests will handle errors gracefully
210
+ ✅ **ALL failures will send Slack notifications**
211
+ ✅ **ALL successes will send Slack notifications**
212
+ ✅ Full visibility into CI/CD pipeline health
213
+
214
+ **Next run should succeed AND properly report any issues!** 🎉
215
+
216
+ ---
217
+
218
+ **Last Updated:** 2026-02-07
GITHUB_ACTIONS_TESTS.md CHANGED
@@ -19,25 +19,38 @@ These tests only run when the required API keys are configured in GitHub Secrets
19
  **Requires TOGETHER_API_KEY:**
20
  6. ⚙️ `test_alignment_dual_dimensions.py` - AlignmentCheck dual dimensions test
21
  7. ⚙️ `test_alignment_vs_factchecker.py` - AlignmentCheck vs FactChecker separation test
22
- 8. ⚙️ `test_alignment_check.py` - AlignmentCheck scanner test
23
- 9. ⚙️ `test_native_llamafirewall_scanner.py` - Native LlamaFirewall scanner test
24
 
25
  **Requires OPENAI_API_KEY:**
 
26
  10. ⚙️ `test_facts_checker_scanner.py` - FactsChecker scanner test (7 subtests including temporal awareness)
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  ## Required GitHub Secrets
29
 
30
  To run all 10 tests, configure these secrets in your repository:
31
 
32
- ### 1. TOGETHER_API_KEY ⚠️ REQUIRED FOR 4 TESTS
33
- - Used by: AlignmentCheck tests (6, 7, 8) and Native LlamaFirewall test (9)
34
  - Provider: [Together AI](https://api.together.xyz/)
35
- - Purpose: Powers the AlignmentCheck scanner for behavioral drift detection
36
 
37
- ### 2. OPENAI_API_KEY ⚠️ REQUIRED FOR 1 TEST
38
- - Used by: FactsChecker test (10)
39
  - Provider: [OpenAI](https://platform.openai.com/)
40
- - Purpose: Powers NeMo GuardRails fact-checking with GPT-4o-mini
41
 
42
  ## How to Add Secrets
43
 
 
19
  **Requires TOGETHER_API_KEY:**
20
  6. ⚙️ `test_alignment_dual_dimensions.py` - AlignmentCheck dual dimensions test
21
  7. ⚙️ `test_alignment_vs_factchecker.py` - AlignmentCheck vs FactChecker separation test
22
+ 8. ⚙️ `test_native_llamafirewall_scanner.py` - Native LlamaFirewall scanner test
 
23
 
24
  **Requires OPENAI_API_KEY:**
25
+ 9. ⚙️ `test_alignment_check.py` - AlignmentCheck scanner test (GPT-4o-mini fallback implementation)
26
  10. ⚙️ `test_facts_checker_scanner.py` - FactsChecker scanner test (7 subtests including temporal awareness)
27
 
28
+ ## Known Issues
29
+
30
+ ### NeMo GuardRails - OpenAI SDK Compatibility
31
+
32
+ **Issue:** NeMo GuardRails may fail with:
33
+ ```
34
+ TypeError: AsyncCompletions.create() got an unexpected keyword argument 'stream_usage'
35
+ ```
36
+
37
+ **Fix:** The workflow pins `openai<1.58.0` to avoid this issue. See `NEMO_GUARDRAILS_FIX.md` for details.
38
+
39
+ **Status:** ✅ FIXED (version constraint applied in workflow)
40
+
41
  ## Required GitHub Secrets
42
 
43
  To run all 10 tests, configure these secrets in your repository:
44
 
45
+ ### 1. TOGETHER_API_KEY ⚠️ REQUIRED FOR 3 TESTS
46
+ - Used by: AlignmentCheck tests (6, 7) and Native LlamaFirewall test (8)
47
  - Provider: [Together AI](https://api.together.xyz/)
48
+ - Purpose: Powers native LlamaFirewall for behavioral drift detection
49
 
50
+ ### 2. OPENAI_API_KEY ⚠️ REQUIRED FOR 2 TESTS
51
+ - Used by: AlignmentCheck GPT-4o-mini fallback test (9) and FactsChecker test (10)
52
  - Provider: [OpenAI](https://platform.openai.com/)
53
+ - Purpose: Powers GPT-4o-mini based tests (AlignmentCheck fallback and NeMo GuardRails fact-checking)
54
 
55
  ## How to Add Secrets
56
 
NEMO_GUARDRAILS_FIX.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NeMo GuardRails - OpenAI SDK Compatibility Fix
2
+
3
+ ## Problem
4
+
5
+ When running FactsChecker tests in GitHub Actions, you may see this error:
6
+
7
+ ```
8
+ TypeError: AsyncCompletions.create() got an unexpected keyword argument 'stream_usage'
9
+ ```
10
+
11
+ Full error trace:
12
+ ```
13
+ WARNING! stream_usage is not default parameter.
14
+ stream_usage was transferred to model_kwargs.
15
+ Please confirm that stream_usage is what you intended.
16
+ Error invoking LLM (model=gpt-4o-mini): AsyncCompletions.create() got an unexpected keyword argument 'stream_usage'
17
+ ```
18
+
19
+ ## Root Cause
20
+
21
+ **Version incompatibility** between:
22
+ - `openai` (OpenAI Python SDK) version **1.58.0 or newer**
23
+ - `langchain-community` (used internally by NeMo GuardRails)
24
+
25
+ The OpenAI SDK version 1.58.0+ introduced breaking changes to the API, and `langchain-community` (which NeMo GuardRails depends on) hasn't been updated to handle these changes yet.
26
+
27
+ Specifically:
28
+ 1. NeMo GuardRails uses `langchain-community` to call OpenAI's API
29
+ 2. `langchain-community` tries to pass a `stream_usage` parameter
30
+ 3. OpenAI SDK 1.58.0+ doesn't accept this parameter
31
+ 4. Result: TypeError
32
+
33
+ ## Solution
34
+
35
+ ### Fix #1: Pin OpenAI SDK Version (APPLIED)
36
+
37
+ Constrain the OpenAI SDK to versions before 1.58.0:
38
+
39
+ ```bash
40
+ pip install "openai<1.58.0"
41
+ ```
42
+
43
+ This is applied in `.github/workflows/test.yml`:
44
+
45
+ ```yaml
46
+ - name: Install dependencies
47
+ run: |
48
+ pip install "openai<1.58.0" # Avoid stream_usage parameter error
49
+ pip install nemoguardrails
50
+ ```
51
+
52
+ ### Fix #2: Wait for Upstream Updates
53
+
54
+ Alternatively, wait for:
55
+ - NeMo GuardRails to update to use newer langchain versions
56
+ - OR langchain-community to be updated for OpenAI SDK 1.58.0+
57
+
58
+ Monitor these repositories:
59
+ - https://github.com/NVIDIA/NeMo-Guardrails/issues
60
+ - https://github.com/langchain-ai/langchain/issues
61
+
62
+ ### Fix #3: Use Alternative LLM Provider
63
+
64
+ If you don't want to pin versions, switch NeMo GuardRails to use a different LLM provider in `nemo_config/config.yml`:
65
+
66
+ ```yaml
67
+ # Example: Use Anthropic Claude instead
68
+ models:
69
+ - type: main
70
+ engine: anthropic
71
+ model: claude-3-5-sonnet-20241022
72
+ ```
73
+
74
+ Then use `ANTHROPIC_API_KEY` instead of `OPENAI_API_KEY`.
75
+
76
+ ## Verification
77
+
78
+ After applying the fix, the FactsChecker test should pass:
79
+
80
+ ```bash
81
+ python test_facts_checker_scanner.py
82
+ ```
83
+
84
+ Expected output:
85
+ ```
86
+ ✅ PASS: FactsChecker Available
87
+ ✅ PASS: Self-Contradiction Detection
88
+ ✅ PASS: RAG Ungroundedness - Fabricated API
89
+ ...
90
+ ✅ ALL TESTS PASSED (7/7)
91
+ ```
92
+
93
+ ## Related Issues
94
+
95
+ - OpenAI SDK breaking changes: https://github.com/openai/openai-python/releases
96
+ - NeMo GuardRails compatibility: https://github.com/NVIDIA/NeMo-Guardrails/issues
97
+ - Langchain OpenAI integration: https://github.com/langchain-ai/langchain/issues
98
+
99
+ ## Timeline
100
+
101
+ - **Issue Discovered:** 2026-02-07
102
+ - **Fix Applied:** 2026-02-07
103
+ - **Status:** FIXED (pinned openai<1.58.0)
104
+
105
+ ## Future Action
106
+
107
+ When NeMo GuardRails or langchain-community release updates that are compatible with OpenAI SDK 1.58.0+, remove the version constraint:
108
+
109
+ ```diff
110
+ - pip install "openai<1.58.0"
111
+ + pip install openai
112
+ ```
113
+
114
+ Test thoroughly before removing the constraint!
requirements_minimal.txt CHANGED
@@ -9,4 +9,10 @@ python-dotenv>=1.0.0
9
 
10
  # Core libraries without heavy dependencies
11
  pydantic>=2.0.0
12
- requests>=2.28.0 # Required for AlignmentCheck API calls
 
 
 
 
 
 
 
9
 
10
  # Core libraries without heavy dependencies
11
  pydantic>=2.0.0
12
+ requests>=2.28.0 # Required for AlignmentCheck API calls
13
+
14
+ # OpenAI SDK version constraint (compatibility with NeMo GuardRails)
15
+ # Issue: OpenAI SDK 1.58.0+ has breaking changes that cause stream_usage parameter errors
16
+ # with langchain-community (used by NeMo GuardRails). Pin to earlier version until fixed.
17
+ # See NEMO_GUARDRAILS_FIX.md for details.
18
+ openai<1.58.0
test_alignment_check.py CHANGED
@@ -28,8 +28,14 @@ def test_aligned_conversation():
28
 
29
  result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
30
 
 
 
 
 
 
31
  print(f"\nOverall Decision: {result.get('overall_decision')}")
32
- print(f"Counts: Safe={result['counts']['safe']}, Warning={result['counts']['warning']}, Block={result['counts']['block']}")
 
33
 
34
  # Verify expected result
35
  if result.get('overall_decision') == 'SAFE':
@@ -60,8 +66,14 @@ def test_goal_hijacking():
60
 
61
  result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
62
 
 
 
 
 
 
63
  print(f"\nOverall Decision: {result.get('overall_decision')}")
64
- print(f"Counts: Safe={result['counts']['safe']}, Warning={result['counts']['warning']}, Block={result['counts']['block']}")
 
65
 
66
  # Verify expected result
67
  if result.get('overall_decision') != 'SAFE':
@@ -92,8 +104,14 @@ def test_off_topic_redirect():
92
 
93
  result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
94
 
 
 
 
 
 
95
  print(f"\nOverall Decision: {result.get('overall_decision')}")
96
- print(f"Counts: Safe={result['counts']['safe']}, Warning={result['counts']['warning']}, Block={result['counts']['block']}")
 
97
 
98
  # Verify expected result
99
  if result.get('overall_decision') != 'SAFE':
 
28
 
29
  result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
30
 
31
+ # Check for errors first
32
+ if "error" in result:
33
+ print(f"\n❌ ERROR: {result.get('error')}")
34
+ return False
35
+
36
  print(f"\nOverall Decision: {result.get('overall_decision')}")
37
+ counts = result.get('counts', {})
38
+ print(f"Counts: Safe={counts.get('safe', 0)}, Warning={counts.get('warning', 0)}, Block={counts.get('block', 0)}")
39
 
40
  # Verify expected result
41
  if result.get('overall_decision') == 'SAFE':
 
66
 
67
  result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
68
 
69
+ # Check for errors first
70
+ if "error" in result:
71
+ print(f"\n❌ ERROR: {result.get('error')}")
72
+ return False
73
+
74
  print(f"\nOverall Decision: {result.get('overall_decision')}")
75
+ counts = result.get('counts', {})
76
+ print(f"Counts: Safe={counts.get('safe', 0)}, Warning={counts.get('warning', 0)}, Block={counts.get('block', 0)}")
77
 
78
  # Verify expected result
79
  if result.get('overall_decision') != 'SAFE':
 
104
 
105
  result = scan_alignment_check_per_message(messages=messages, purpose=purpose)
106
 
107
+ # Check for errors first
108
+ if "error" in result:
109
+ print(f"\n❌ ERROR: {result.get('error')}")
110
+ return False
111
+
112
  print(f"\nOverall Decision: {result.get('overall_decision')}")
113
+ counts = result.get('counts', {})
114
+ print(f"Counts: Safe={counts.get('safe', 0)}, Warning={counts.get('warning', 0)}, Block={counts.get('block', 0)}")
115
 
116
  # Verify expected result
117
  if result.get('overall_decision') != 'SAFE':