Skip to content

Commit 538a061

Browse files
authored
fix: aimock v1.33.0 — proxy buffer cap + stabilization (#272)
Combined release **v1.33.0** — folds the proxy-buffer-cap memory-leak fix (formerly #275) into this stabilization branch so a single version ships both. `src/recorder.ts` auto-merged cleanly (disjoint regions); combined test suite green (3968 pass / 0 fail). ## Added - `--max-proxy-buffer-bytes` / `--max-proxy-buffer-frames` to bound proxy-record buffering ## Fixed - **Proxy memory leak:** the proxy/record path accumulated per-frame state (`frameTimestamps`, frame buffers, `chunks`) for a stream's whole lifetime — long-lived proxied SSE streams drove ~8.4 GB/25 h → OOM. Now byte+frame-capped per-frame (incl. binary EventStream), cleared on trip, with disconnect cleanup; relay preserved (fail-loud 502 above the 256 MiB hard ceiling). (was #275) - **`Invalid string length` crash:** oversized proxied responses no longer build a >512 MB string. (was #275) - Sanitize `X-AIMock-Context` to prevent fixture path traversal. - Scope one-shot error injection to compatible endpoints. - Improve recorder fixture fidelity across providers. - `matchesPattern` no longer mutates the caller's RegExp `lastIndex`. ## Release Merging this PR autopublishes `@copilotkit/aimock` **v1.33.0** (publish-release.yml: push-to-main gated by "version not yet on npm"). Supersedes #275. ## Verification - Buffer-cap fix: 3 CR rounds to zero; local red-green (never-ending stream heap flat after cap); staging heap bounded (oscillates+reclaims, no OOM trajectory). - Combined: tsc clean, full suite 3968 pass / 0 fail, build OK.
2 parents d165b91 + 4740992 commit 538a061

21 files changed

Lines changed: 3176 additions & 133 deletions

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"source": {
1010
"source": "npm",
1111
"package": "@copilotkit/aimock",
12-
"version": "^1.32.0"
12+
"version": "^1.33.0"
1313
},
1414
"description": "Fixture authoring skill for @copilotkit/aimock — LLM, multimedia (image/TTS/transcription/video), MCP, A2A, AG-UI, vector, embeddings, structured output, sequential responses, streaming physics, record/replay, agent loop patterns, and debugging"
1515
}

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aimock",
3-
"version": "1.32.0",
3+
"version": "1.33.0",
44
"description": "Fixture authoring guidance for @copilotkit/aimock — LLM, multimedia, MCP, A2A, AG-UI, vector, and service mocking",
55
"author": {
66
"name": "CopilotKit"

CHANGELOG.md

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

33
## [Unreleased]
44

5+
## [1.33.0] - 2026-06-23
6+
7+
### Added
8+
9+
- `--max-proxy-buffer-bytes` / `--max-proxy-buffer-frames` flags to cap proxy buffering (#275)
10+
11+
### Fixed
12+
13+
- Proxy path no longer leaks memory on long-lived upstream streams (#275)
14+
- Oversized proxied responses no longer crash with `Invalid string length` (#275)
15+
- Sanitize `X-AIMock-Context` to prevent fixture path traversal (#272)
16+
- Scope one-shot error injection, improve recorder fixture fidelity, fix `matchesPattern` lastIndex (#272)
17+
518
## [1.32.0] - 2026-06-22
619

720
### Added

charts/aimock/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ name: aimock
33
description: Mock infrastructure for AI application testing (OpenAI, Anthropic, Gemini, MCP, A2A, vector)
44
type: application
55
version: 0.1.0
6-
appVersion: "1.32.0"
6+
appVersion: "1.33.0"

docs/error-injection/index.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,11 @@ <h2>Interruption Behavior</h2>
179179
<div class="info-box">
180180
<p>
181181
<strong>See also: <a href="/chaos-testing">Chaos Testing</a></strong> &mdash; for
182-
probabilistic failure injection. Chaos testing adds configurable error rates, random
183-
latency spikes, and stream corruption that trigger based on probability rather than
184-
deterministic fixture matching. Use error injection for specific, reproducible failure
185-
scenarios; use chaos testing for resilience testing under unpredictable conditions.
182+
probabilistic failure injection. Chaos testing adds configurable rates for dropped
183+
requests (500), malformed JSON responses, and disconnects that trigger based on
184+
probability rather than deterministic fixture matching. Use error injection for
185+
specific, reproducible failure scenarios; use chaos testing for resilience testing under
186+
unpredictable conditions.
186187
</p>
187188
</div>
188189
</main>

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.32.0",
3+
"version": "1.33.0",
44
"description": "Mock infrastructure for AI application testing — LLM APIs, image generation, image editing, text-to-speech, transcription, audio translation, audio generation, video generation, embeddings, 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": [

packages/aimock-pytest/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ aimock.reset() # alias for reset_fixtures()
7373

7474
```
7575
--aimock-node PATH Path to node binary
76-
--aimock-version VER aimock npm version (default: 1.32.0)
76+
--aimock-version VER aimock npm version (default: 1.33.0)
7777
```
7878

7979
## Environment Variables

packages/aimock-pytest/src/aimock_pytest/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
control routes ship in the next release). Keep it tracking npm releases.
99
"""
1010

11-
AIMOCK_VERSION = "1.32.0"
11+
AIMOCK_VERSION = "1.33.0"

src/__tests__/llmock.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,37 @@ function post(url: string, body: object): Promise<{ status: number; data: string
3737
});
3838
}
3939

40+
function postTo(
41+
url: string,
42+
path: string,
43+
body: object,
44+
): Promise<{ status: number; data: string }> {
45+
return new Promise((resolve, reject) => {
46+
const parsed = new URL(url);
47+
const payload = JSON.stringify(body);
48+
const req = http.request(
49+
{
50+
hostname: parsed.hostname,
51+
port: parsed.port,
52+
path,
53+
method: "POST",
54+
headers: {
55+
"Content-Type": "application/json",
56+
"Content-Length": Buffer.byteLength(payload),
57+
},
58+
},
59+
(res) => {
60+
let data = "";
61+
res.on("data", (chunk) => (data += chunk));
62+
res.on("end", () => resolve({ status: res.statusCode!, data }));
63+
},
64+
);
65+
req.on("error", reject);
66+
req.write(payload);
67+
req.end();
68+
});
69+
}
70+
4071
function chatBody(userMessage: string, stream = true) {
4172
return {
4273
model: "gpt-4",
@@ -109,6 +140,11 @@ describe("LLMock", () => {
109140
expect(result).toBe(mock);
110141
});
111142

143+
it("addFixturesFromJSON throws a contextful error on malformed JSON", () => {
144+
mock = new LLMock();
145+
expect(() => mock.addFixturesFromJSON("{ not valid json")).toThrow(/addFixturesFromJSON/);
146+
});
147+
112148
it("chaining API works across multiple calls", () => {
113149
mock = new LLMock();
114150
const result = mock
@@ -819,6 +855,34 @@ describe("LLMock", () => {
819855
expect(res3.status).toBe(200);
820856
expect(res3.data).toContain("Normal response");
821857
});
858+
859+
it("does not let an incompatible (multimedia) endpoint consume a chat-bound one-shot error", async () => {
860+
mock = new LLMock();
861+
mock.onMessage("hello", { content: "Hi!" });
862+
mock.onImage("draw a cat", { image: { b64Json: "aW1n" } });
863+
await mock.start();
864+
865+
// Queue a one-shot error. Conceptually intended for the chat endpoint;
866+
// an error response is incompatible with the image endpoint (mirrors the
867+
// router's endpoint-compat table, where only `fal` accepts errors).
868+
mock.nextRequestError(503, { message: "Overloaded", type: "server_error" });
869+
870+
// Issue an INCOMPATIBLE image request FIRST. It must NOT consume the
871+
// error: the image fixture should serve normally and the error stays
872+
// pending for a compatible endpoint.
873+
const img = await postTo(mock.url, "/v1/images/generations", {
874+
model: "dall-e-3",
875+
prompt: "draw a cat",
876+
});
877+
expect(img.status).toBe(200);
878+
expect(img.data).toContain("aW1n");
879+
880+
// The one-shot error must STILL be pending → the next chat request gets it.
881+
const chat = await post(mock.url, chatBody("hello"));
882+
expect(chat.status).toBe(503);
883+
const body = JSON.parse(chat.data);
884+
expect(body.error.message).toBe("Overloaded");
885+
});
822886
});
823887

824888
describe("journal proxies", () => {

src/__tests__/multimedia-types.test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isAudioResponse,
55
isTranscriptionResponse,
66
isVideoResponse,
7+
matchesPattern,
78
} from "../helpers.js";
89
import { matchFixture } from "../router.js";
910
import type { Fixture, ChatCompletionRequest, FixtureResponse } from "../types.js";
@@ -26,6 +27,11 @@ describe("multimedia type guards", () => {
2627
expect(isImageResponse(r)).toBe(false);
2728
});
2829

30+
test("isImageResponse rejects non-object image value", () => {
31+
const r = { image: "not-an-object" } as unknown as FixtureResponse;
32+
expect(isImageResponse(r)).toBe(false);
33+
});
34+
2935
test("isAudioResponse detects audio (string form)", () => {
3036
const r: FixtureResponse = { audio: "AAAA", format: "mp3" };
3137
expect(isAudioResponse(r)).toBe(true);
@@ -149,7 +155,54 @@ describe("endpoint filtering in matchFixture", () => {
149155
_endpointType: "image",
150156
};
151157

152-
const first = matchFixture(fixtures, imageReq, counts);
153-
expect(first).toBe(fixtures[0]);
158+
// Pin the FULL sequence ordering this test claims to verify. matchFixture
159+
// gates a sequenced fixture on its match count equalling sequenceIndex but
160+
// does not itself mutate the count — the caller (journal) increments after
161+
// consuming a match, and crucially advances ALL sequenced siblings sharing
162+
// the same match criteria so the group shares one logical counter. Mimic
163+
// that here so each call advances to the next sequenceIndex, proving the
164+
// sequence resolves 0 → 1 in order and then exhausts.
165+
const advanceSequence = (matched: Fixture): void => {
166+
for (const f of fixtures) {
167+
if (f.match.sequenceIndex !== undefined) {
168+
counts.set(f, (counts.get(f) ?? 0) + 1);
169+
}
170+
}
171+
// (matched is part of the group; the loop above already advanced it)
172+
void matched;
173+
};
174+
const resolve = (): Fixture | null => {
175+
const f = matchFixture(fixtures, imageReq, counts);
176+
if (f) advanceSequence(f);
177+
return f;
178+
};
179+
180+
expect(resolve()).toBe(fixtures[0]);
181+
expect(resolve()).toBe(fixtures[1]);
182+
// The sequence is exhausted: no fixture has a sequenceIndex matching the
183+
// next shared count, so further requests no longer match.
184+
expect(resolve()).toBeNull();
185+
});
186+
});
187+
188+
describe("matchesPattern", () => {
189+
test("does not mutate the caller's RegExp lastIndex", () => {
190+
// A global regex carries mutable `lastIndex` state. matchesPattern must
191+
// not leave that state mutated, or callers reusing the same regex object
192+
// (e.g. the search/rerank/moderation filter loops) get inconsistent
193+
// results on subsequent uses.
194+
const re = /guitar/g;
195+
expect(matchesPattern("guitar", re)).toBe(true);
196+
// After the call, the caller's own use of the same regex must behave as if
197+
// matchesPattern never touched it.
198+
expect(re.lastIndex).toBe(0);
199+
expect(re.test("guitar")).toBe(true);
200+
});
201+
202+
test("is consistent across repeated calls with the same global regex", () => {
203+
const re = /g/g;
204+
expect(matchesPattern("guitar", re)).toBe(true);
205+
expect(matchesPattern("guitar", re)).toBe(true);
206+
expect(matchesPattern("guitar", re)).toBe(true);
154207
});
155208
});

0 commit comments

Comments
 (0)