Skip to content

Commit 0683040

Browse files
authored
fix: 50+ fixes across recorder, providers, CI, competitive matrix (#133)
## Summary Addresses all bucket (c) and (d) items from PR #122's 7-agent CR, plus correctness issues discovered during this PR's own iterative CR process (6 rounds, 4 fix cycles). ### Server core - `readBody` 10MB size limit (DoS surface closed) - Control API: error detail in responses, one-shot race fix (synchronous splice), fixtures_loaded gauge updates on mutations - `normalizeCompatPath` magic number replaced with readable expression - `source` field ("fixture" | "proxy") added to JournalEntry type ### Recorder correctness - Crash hardening: `headersSent` guards, `clientDisconnected` tracking, encoding_format logging - Content+toolCalls: preserve both (previously dropped content) - Cohere v2 native detection branch in `buildFixtureResponse` - Tool-call IDs preserved from 5 provider formats - Reasoning/thinking extracted from 4 provider formats (including tool-call-only) - Multi-block text: `filter+join` instead of `find` (Anthropic/Gemini/Bedrock) - Thinking-only Anthropic responses handled (not error fixture) - Empty-content responses recorded correctly (not error fixture) - Ollama `/api/generate` format detection - Streaming collapse reasoning propagated ### Provider handlers - **Bedrock/Converse**: ContentWithToolCallsResponse guards, ResponseOverrides wired into all builders + streaming, Converse-wrapped stream format (matches collapse parser), `text_delta` type field, Converse error envelope, webSearches warnings - **Cohere v2**: reasoning in all builders + streaming, webSearches warnings, response_format forwarding, assistant tool_calls preservation, full ResponseOverrides (finish_reason, usage, id) - **Ollama**: ContentWithToolCallsResponse, default stream:true, field validation - **Gemini**: tool_call_id collision fix (shared callCounter), thought-part filtering - **All**: journal source field on proxy paths, Azure embedding routing ### Competitive matrix pipeline - `computeChanges` matches actual HTML structure (span class="no") - `applyChanges` regex matches actual td>span structure - `updateProviderCounts` scoped to competitor column (not global) - `extractFeatures` tightened keyword patterns - Added mokksy/ai-mocks competitor ### CI + framework - `--auto` instead of `--admin` on fix-drift merge - Slack secrets via env vars across 5 workflows - Script injection prevention in notify-pr.yml (env vars + jq) - Portable `grep -oE`, SLACK_WEBHOOK guards - Router: RegExp `g`-flag `lastIndex` reset - Jest/Vitest: save/restore pre-existing env vars, loadFixtures warnings ## Test plan - [ ] 2573/2573 tests passing (69 test files) - [ ] TSC clean - [ ] Prettier + ESLint clean - [ ] 0 merge conflicts with main - [ ] 6 CR rounds converged (R1-R4 on original, R1-R6 on additions) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents b7b305e + 903acf9 commit 0683040

45 files changed

Lines changed: 3889 additions & 1724 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/fix-drift.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ jobs:
119119
if: success() && steps.check.outputs.skip != 'true'
120120
run: |
121121
npx tsx scripts/fix-drift.ts --create-pr 2>&1 | tee /tmp/pr-output.txt
122-
PR_URL=$(grep -oP 'https://github.com/[^ ]+/pull/\d+' /tmp/pr-output.txt | head -1)
122+
PR_URL=$(grep -oE 'https://github.com/[^ ]+/pull/[0-9]+' /tmp/pr-output.txt | head -1)
123+
if [ -z "$PR_URL" ]; then echo "No PR URL found"; exit 1; fi
123124
echo "url=$PR_URL" >> $GITHUB_OUTPUT
124125
env:
125126
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -129,7 +130,7 @@ jobs:
129130
if: success() && steps.pr.outputs.url != ''
130131
run: |
131132
PR_URL="${{ steps.pr.outputs.url }}"
132-
gh pr merge "$PR_URL" --merge --admin
133+
gh pr merge "$PR_URL" --merge --auto
133134
env:
134135
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
135136

@@ -144,15 +145,19 @@ jobs:
144145
- name: Notify Slack on fix success
145146
if: success() && steps.pr.outputs.url != ''
146147
run: |
147-
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
148+
if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi
149+
curl -s -X POST "$SLACK_WEBHOOK" \
148150
-H "Content-Type: application/json" \
149151
-d "{\"text\":\"✅ *Drift auto-fixed and merged*\nPR: ${{ steps.pr.outputs.url }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}"
152+
env:
153+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
150154

151155
# Step 8: Slack notification on fix failure
152156
- name: Notify Slack on fix failure
153157
if: failure() && steps.check.outputs.skip != 'true'
154158
run: |
155-
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
159+
if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi
160+
curl -s -X POST "$SLACK_WEBHOOK" \
156161
-H "Content-Type: application/json" \
157162
-d "{\"text\":\"❌ *Drift auto-fix failed* — issue created\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}"
158163
env:

.github/workflows/notify-pr.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ jobs:
77
runs-on: ubuntu-latest
88
steps:
99
- name: Notify Slack
10+
env:
11+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
12+
PR_TITLE: ${{ github.event.pull_request.title }}
13+
PR_URL: ${{ github.event.pull_request.html_url }}
14+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
15+
PR_NUM: ${{ github.event.pull_request.number }}
1016
run: |
11-
PR_TITLE="${{ github.event.pull_request.title }}"
12-
PR_URL="${{ github.event.pull_request.html_url }}"
13-
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
14-
PR_NUM="${{ github.event.pull_request.number }}"
15-
curl -sf -X POST "${{ secrets.SLACK_WEBHOOK }}" \
17+
if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi
18+
PAYLOAD=$(jq -n \
19+
--arg text "New PR #${PR_NUM} on aimock by *${PR_AUTHOR}*: <${PR_URL}|${PR_TITLE}>" \
20+
'{text: $text}')
21+
curl -sf -X POST "$SLACK_WEBHOOK" \
1622
-H "Content-Type: application/json" \
17-
-d "{\"text\": \"🔀 New PR #${PR_NUM} on aimock by *${PR_AUTHOR}*: <${PR_URL}|${PR_TITLE}>\"}"
23+
-d "$PAYLOAD"

.github/workflows/publish-release.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ jobs:
9696
- name: Notify Slack
9797
if: steps.check.outputs.published == 'false'
9898
run: |
99+
if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi
99100
VERSION="v${{ steps.check.outputs.version }}"
100-
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
101+
curl -s -X POST "$SLACK_WEBHOOK" \
101102
-H "Content-Type: application/json" \
102103
-d "{\"text\":\"📦 *@copilotkit/aimock ${VERSION} published*\nnpm: https://www.npmjs.com/package/@copilotkit/aimock/v/${{ steps.check.outputs.version }}\nRelease: https://github.com/${{ github.repository }}/releases/tag/${VERSION}\"}"
104+
env:
105+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

.github/workflows/test-drift.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
- name: Notify Slack
8080
if: always()
8181
run: |
82-
if [ -z "${{ secrets.SLACK_WEBHOOK }}" ]; then exit 0; fi
82+
if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi
8383
8484
PREV="${{ steps.prev.outputs.conclusion }}"
8585
NOW="${{ job.status }}"
@@ -102,7 +102,7 @@ jobs:
102102
exit 0
103103
fi
104104
105-
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
105+
curl -s -X POST "$SLACK_WEBHOOK" \
106106
-H "Content-Type: application/json" \
107107
-d "{\"text\": \"${EMOJI} ${MSG}\"}"
108108
env:

.github/workflows/update-competitive-matrix.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
- name: Notify Slack
5959
if: always()
6060
run: |
61+
if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi
6162
if [ "${{ steps.changes.outputs.changed }}" = "true" ]; then
6263
EMOJI="📊"
6364
MSG="*Competitive matrix changes detected* — PR created with updated migration pages. <https://github.com/CopilotKit/aimock/actions/runs/${{ github.run_id }}|View run>"
@@ -68,6 +69,8 @@ jobs:
6869
EMOJI="❌"
6970
MSG="*Competitive matrix update failed*. <https://github.com/CopilotKit/aimock/actions/runs/${{ github.run_id }}|View run>"
7071
fi
71-
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
72+
curl -s -X POST "$SLACK_WEBHOOK" \
7273
-H "Content-Type: application/json" \
7374
-d "{\"text\": \"${EMOJI} ${MSG}\"}"
75+
env:
76+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

CHANGELOG.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
# @copilotkit/aimock
22

3-
## 1.15.0
3+
## 1.15.1
44

5-
### Added
5+
### Fixed
66

7-
- Chaos injection in proxy mode: drop and disconnect fire pre-flight (upstream is never
8-
contacted), malformed proxies the request then corrupts the response body before
9-
delivering it to the client.
10-
- SSE streaming bypass: when upstream responds with `text/event-stream`, malformed chaos
11-
is silently skipped (bytes are already on the wire). A bypass metric
12-
(`aimock_chaos_bypassed_total`) is emitted so operators can see the configured action
13-
did not fire; the normal `aimock_chaos_triggered_total` counter does not increment.
14-
- Chaos source label (`fixture` vs `proxy`) on Prometheus metrics and journal entries,
15-
distinguishing where the chaos decision was made.
16-
- CORS `Access-Control-Allow-Headers` now includes `X-Aimock-Chaos-Drop`,
17-
`X-Aimock-Chaos-Malformed`, `X-Aimock-Chaos-Disconnect`, and `X-Test-Id`, enabling
18-
browser-based clients to send per-request chaos overrides via preflight-safe headers.
19-
- `handleVideoStatus` (`GET /v1/videos/:id`) now evaluates chaos before returning video
20-
state, consistent with all other handler endpoints.
7+
- **Recorder**: crash hardening (headersSent guards, clientDisconnected tracking),
8+
preserve content alongside toolCalls, Cohere v2 native detection, tool-call ID
9+
extraction from 5 providers, reasoning/thinking extraction from 4 providers,
10+
multi-block text join (filter+join instead of find), thinking-only and empty-content
11+
response handling, Ollama /api/generate format detection, streaming collapse
12+
reasoning propagation.
13+
- **Bedrock/Converse**: ContentWithToolCallsResponse support, ResponseOverrides wired
14+
into all non-streaming and streaming builders, Converse-wrapped stream event format,
15+
text_delta type field on text deltas, proper error envelope on Converse errors,
16+
webSearches warnings.
17+
- **Cohere v2**: reasoning in all builders + streaming, webSearches warnings,
18+
response_format forwarding, assistant tool_calls preservation, full
19+
ResponseOverrides (finish_reason, usage, id) in non-streaming and streaming paths.
20+
- **Server**: readBody 10MB size limit, control API error detail, one-shot error fixture
21+
race fix, normalizeCompatPath clarity, fixtures_loaded gauge updates on mutations.
22+
- **Competitive matrix**: HTML pipeline fixed (computeChanges, applyChanges,
23+
updateProviderCounts, extractFeatures all aligned with actual DOM structure).
24+
- **CI workflows**: --auto merge (respects branch protection), Slack secrets via env
25+
vars, script injection prevention in notify-pr.yml, portable grep.
26+
- **Router**: RegExp g-flag lastIndex reset prevents alternating match/no-match.
27+
- **Jest/Vitest**: save/restore pre-existing env vars in afterAll, loadFixtures
28+
console.warn on failure.
29+
- **Gemini**: tool_call_id collision fix (shared callCounter), thought-part filtering.
30+
- **Ollama**: ContentWithToolCallsResponse support, default stream:true, field validation.
2131

2232
## 1.14.9
2333

docs/chaos-testing/index.html

Lines changed: 7 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ <h2>Failure Modes</h2>
6969
<td>HTTP 500</td>
7070
<td>
7171
Returns a 500 error with
72-
<code
73-
>{"error":{"message":"Chaos: request
74-
dropped","type":"server_error","code":"chaos_drop"}}</code
75-
>
72+
<code>{"error":{"message":"Chaos: request dropped","code":"chaos_drop"}}</code>
7673
</td>
7774
</tr>
7875
<tr>
@@ -207,7 +204,7 @@ <h2>Per-Request Headers</h2>
207204
<span class="str">"Content-Type"</span>: <span class="str">"application/json"</span>,
208205
<span class="str">"x-aimock-chaos-disconnect"</span>: <span class="str">"1.0"</span>,
209206
},
210-
<span class="prop">body</span>: <span class="fn">JSON.stringify</span>({ <span class="prop">model</span>: <span class="str">"gpt-4"</span>, <span class="prop">messages</span>: [{ <span class="prop">role</span>: <span class="str">"user"</span>, <span class="prop">content</span>: <span class="str">"hello"</span> }] }),
207+
<span class="prop">body</span>: <span class="fn">JSON.stringify</span>({ <span class="prop">model</span>: <span class="str">"gpt-4"</span>, <span class="prop">messages</span>: [...] }),
211208
});</code></pre>
212209
</div>
213210

@@ -220,7 +217,7 @@ <h2>CLI Flags</h2>
220217
<div class="code-block-header">
221218
CLI chaos flags <span class="lang-tag">shell</span>
222219
</div>
223-
<pre><code>$ npx -p @copilotkit/aimock aimock --fixtures ./fixtures \
220+
<pre><code>$ npx -p @copilotkit/aimock llmock --fixtures ./fixtures \
224221
--chaos-drop 0.1 \
225222
--chaos-malformed 0.05 \
226223
--chaos-disconnect 0.02</code></pre>
@@ -242,55 +239,6 @@ <h2>CLI Flags</h2>
242239
</div>
243240
</div>
244241

245-
<h2>Proxy Mode</h2>
246-
<p>
247-
When aimock is configured as a record/replay proxy (<code>--record</code>), chaos applies
248-
to proxied requests too &mdash; so a staging environment pointed at real upstream APIs
249-
still sees the failure modes your tests expect. Chaos is rolled <em>once per request</em>,
250-
after fixture matching, with the same headers&nbsp;&gt;&nbsp;fixture&nbsp;&gt;&nbsp;server
251-
precedence.
252-
</p>
253-
<table class="endpoint-table">
254-
<thead>
255-
<tr>
256-
<th>Mode</th>
257-
<th>When upstream is contacted</th>
258-
<th>What the client sees</th>
259-
</tr>
260-
</thead>
261-
<tbody>
262-
<tr>
263-
<td><code>drop</code></td>
264-
<td>Never &mdash; upstream not contacted</td>
265-
<td>HTTP 500 chaos body; upstream is not called</td>
266-
</tr>
267-
<tr>
268-
<td><code>disconnect</code></td>
269-
<td>Never &mdash; upstream not contacted</td>
270-
<td>Connection destroyed; upstream is not called</td>
271-
</tr>
272-
<tr>
273-
<td><code>malformed</code></td>
274-
<td>Called &mdash; post-response</td>
275-
<td>
276-
Request proxies normally; the upstream response is captured, then the body is
277-
replaced with invalid JSON before relay. The recorded fixture (if recording) keeps
278-
the real upstream response &mdash; chaos is a live-traffic decoration, not a fixture
279-
mutation.
280-
</td>
281-
</tr>
282-
</tbody>
283-
</table>
284-
<p>
285-
<strong>SSE bypass.</strong> If upstream returns
286-
<code>Content-Type: text/event-stream</code>, aimock streams chunks to the client
287-
progressively. By the time <code>malformed</code> would fire, the bytes are already on the
288-
wire &mdash; the chaos action cannot be applied. This bypass is observable via the
289-
<code>aimock_chaos_bypassed_total</code> counter (see Prometheus Metrics below) and a
290-
warning in the server log, so a configured chaos rate doesn't silently drop to 0% on SSE
291-
traffic. Streaming mutation is planned for a future phase.
292-
</p>
293-
294242
<h2>Journal Tracking</h2>
295243
<p>
296244
When chaos triggers, the journal entry includes a <code>chaosAction</code> field recording
@@ -306,7 +254,6 @@ <h2>Journal Tracking</h2>
306254
"path": "/v1/chat/completions",
307255
"response": {
308256
"status": 500,
309-
"source": "fixture",
310257
"fixture": { "...": "elided for brevity" },
311258
"chaosAction": "drop"
312259
}
@@ -321,31 +268,15 @@ <h2>Journal Tracking</h2>
321268
<h2>Prometheus Metrics</h2>
322269
<p>
323270
When metrics are enabled (<code>--metrics</code>), each chaos trigger increments the
324-
<code>aimock_chaos_triggered_total</code> counter, tagged with <code>action</code> and
325-
<code>source</code>. <code>source="fixture"</code> means a fixture matched (or would have,
326-
before chaos intervened); <code>source="proxy"</code> means the request was on the proxy
327-
dispatch path.
271+
<code>aimock_chaos_triggered_total</code> counter with an <code>action</code> label:
328272
</p>
329273

330274
<div class="code-block">
331275
<div class="code-block-header">Metrics output <span class="lang-tag">text</span></div>
332276
<pre><code># TYPE aimock_chaos_triggered_total counter
333-
aimock_chaos_triggered_total{action="drop",source="fixture"} 3
334-
aimock_chaos_triggered_total{action="malformed",source="fixture"} 1
335-
aimock_chaos_triggered_total{action="disconnect",source="proxy"} 2</code></pre>
336-
</div>
337-
338-
<p>
339-
When a chaos action is rolled but can't be applied &mdash; today, only
340-
<code>malformed</code> on an SSE proxy response &mdash; the bypass is recorded in a
341-
separate counter so operators can distinguish "chaos didn't roll" from "chaos rolled but
342-
was bypassed":
343-
</p>
344-
345-
<div class="code-block">
346-
<div class="code-block-header">Bypass counter <span class="lang-tag">text</span></div>
347-
<pre><code># TYPE aimock_chaos_bypassed_total counter
348-
aimock_chaos_bypassed_total{action="malformed",source="proxy",reason="sse_streamed"} 4</code></pre>
277+
aimock_chaos_triggered_total{action="drop"} 3
278+
aimock_chaos_triggered_total{action="malformed"} 1
279+
aimock_chaos_triggered_total{action="disconnect"} 2</code></pre>
349280
</div>
350281
</main>
351282
<aside class="page-toc" id="page-toc"></aside>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@copilotkit/aimock",
3-
"version": "1.15.0",
3+
"version": "1.15.1",
44
"description": "Mock infrastructure for AI application testing — LLM APIs, image generation, text-to-speech, transcription, video generation, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation. One package, one port, zero dependencies.",
55
"license": "MIT",
66
"keywords": [

0 commit comments

Comments
 (0)