Skip to content

Commit 71b1c95

Browse files
authored
Codebase hardening: bucket (c) sweep from dual-context header CR (#179)
## Summary Addresses 14 of 16 pre-existing issues from the dual-context header CR bucket (c) list, plus 12 additional issues discovered during CR fix rounds (26 total fixes across 23 files). **Core hardening** — readBody size limit (10MB) + double-settlement guard, matchesPattern lastIndex reset for global/sticky regex, tightened isErrorResponse type guard, deep-copy toolCalls in normalizeFactoryResponse, deduplicated DEFAULT_TEST_ID constant **WebSocket handlers** — ContentWithToolCallsResponse support for ws-responses + ws-realtime (was 500 error), isClosed guards on post-streaming sends, interruption timer cleanup, per-response instructions passthrough, consistent journal header capture **Bedrock** — forward max_tokens in request conversion, use || null for empty-string content (tool-call-only messages), type-safe event removal replacing fragile pop() **Double-journal prevention** — added handled_by_hook guard to all 14 proxy handler sites across 11 files (ollama, cohere, speech, messages, images, embeddings, gemini, responses, transcription, bedrock, bedrock-converse) **Endpoint hardening** — journal headers captured in all 47 ElevenLabs/fal/WS sites, proactive TTL sweep for video + fal job maps with proper lifecycle (clear preserves timer, destroy stops it), atomic video getEntry() for consistent TTL check, persistent created_at across status polls, fal queue 409 journal entry, boundary-based multipart parsing, cli help text ## Test plan - [x] 2891 tests pass (80 files, 37 skipped) - [x] TypeScript typecheck clean - [x] Prettier + ESLint clean - [x] 3 CR rounds with 7 agents each (21 agent-rounds)
2 parents eb432d7 + 0893967 commit 71b1c95

30 files changed

Lines changed: 970 additions & 444 deletions

CHANGELOG.md

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

33
## [Unreleased]
44

5+
### Fixed
6+
7+
- **Helper utilities and error serialization** — hardened helper functions and error serialization paths for correctness and robustness
8+
- **Journal and fixture-loader correctness** — fixed journal entry handling and fixture-loader edge cases
9+
- **WebSocket handler consistency and strict-mode journal** — aligned WebSocket handler behavior and ensured strict-mode journal entries are recorded correctly
10+
- **Provider handler consistency and proxy outcomes** — unified provider handler error paths and proxy outcome reporting
11+
- **Media handler hardening and chaos injection** — strengthened media handler validation and chaos injection reliability
12+
13+
### Tests
14+
15+
- **Bedrock mock consistency and CLI help text** — corrected Bedrock mock test assertions and CLI `--help` output coverage
16+
517
## [1.22.0] - 2026-05-11
618

719
### Added

src/__tests__/bedrock-stream.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,6 @@ function postPartialBinary(
187187
res.on("error", () => {
188188
aborted = true;
189189
});
190-
res.on("aborted", () => {
191-
aborted = true;
192-
});
193190
res.on("close", () => {
194191
safeResolve({ body: Buffer.concat(chunks), aborted });
195192
});
@@ -1747,7 +1744,7 @@ describe("converseToCompletionRequest (edge cases)", () => {
17471744
},
17481745
"model",
17491746
);
1750-
expect(result.messages[0]).toEqual({ role: "assistant", content: "" });
1747+
expect(result.messages[0]).toEqual({ role: "assistant", content: null });
17511748
});
17521749

17531750
it("handles user tool result with missing text in content items (text ?? '' fallback)", () => {
@@ -1859,8 +1856,8 @@ describe("converseToCompletionRequest (edge cases)", () => {
18591856
"model",
18601857
);
18611858
expect(result.messages[0].tool_calls).toHaveLength(1);
1862-
// Empty text → content is "" (nullish coalescing preserves empty string)
1863-
expect(result.messages[0].content).toBe("");
1859+
// Empty text → content is null (|| coerces empty string to null)
1860+
expect(result.messages[0].content).toBe(null);
18641861
});
18651862
});
18661863

src/__tests__/bedrock.test.ts

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,8 @@ describe("bedrockToCompletionRequest (edge cases)", () => {
751751
},
752752
"model",
753753
);
754-
// Empty array → no tool_use blocks, textContent is "" → preserved as "" (not coerced to null via ??)
755-
expect(result.messages[0]).toEqual({ role: "assistant", content: "" });
754+
// Empty array → no tool_use blocks, textContent is "" → coerced to null via ||
755+
expect(result.messages[0]).toEqual({ role: "assistant", content: null });
756756
});
757757

758758
it("handles user message with content blocks but no tool_results (text extraction)", () => {
@@ -1650,31 +1650,8 @@ describe("Bedrock webSearches warning", () => {
16501650
response: { content: "Result.", webSearches: ["test"] },
16511651
};
16521652
const journal = new Journal();
1653-
const req = {
1654-
method: undefined,
1655-
url: undefined,
1656-
headers: {},
1657-
} as unknown as http.IncomingMessage;
1658-
const res = {
1659-
_written: "",
1660-
writableEnded: false,
1661-
statusCode: 0,
1662-
writeHead(s: number) {
1663-
this.statusCode = s;
1664-
},
1665-
setHeader() {},
1666-
write(d: string) {
1667-
this._written += d;
1668-
return true;
1669-
},
1670-
end(d?: string) {
1671-
if (d) this._written += d;
1672-
this.writableEnded = true;
1673-
},
1674-
destroy() {
1675-
this.writableEnded = true;
1676-
},
1677-
} as unknown as http.ServerResponse;
1653+
const req = createMockReq();
1654+
const res = createMockRes();
16781655

16791656
await handleBedrock(
16801657
req,

src/bedrock-converse.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,12 @@ function buildBedrockStreamContentWithToolCallsEvents(
180180
): Array<{ eventType: string; payload: object }> {
181181
const events = buildBedrockStreamTextEvents(content, chunkSize, reasoning, overrides);
182182
// Remove trailing metadata + messageStop events — we re-emit them after tool blocks
183-
events.pop(); // metadata
184-
events.pop(); // messageStop
183+
for (let i = events.length - 1; i >= 0; i--) {
184+
const et = (events[i] as { eventType: string }).eventType;
185+
if (et === "metadata" || et === "messageStop") {
186+
events.splice(i, 1);
187+
}
188+
}
185189
let blockIndex = reasoning ? 2 : 1;
186190

187191
for (const tc of toolCalls) {
@@ -336,7 +340,7 @@ export function converseToCompletionRequest(
336340
if (toolUseBlocks.length > 0) {
337341
messages.push({
338342
role: "assistant",
339-
content: textContent ?? null,
343+
content: textContent || null,
340344
tool_calls: toolUseBlocks.map((b) => ({
341345
id: b.toolUse!.toolUseId,
342346
type: "function" as const,
@@ -347,7 +351,7 @@ export function converseToCompletionRequest(
347351
})),
348352
});
349353
} else {
350-
messages.push({ role: "assistant", content: textContent ?? null });
354+
messages.push({ role: "assistant", content: textContent || null });
351355
}
352356
} else {
353357
const warnMsg = `Unexpected message role "${msg.role}" in Converse request — skipping`;
@@ -607,6 +611,7 @@ export async function handleConverse(
607611
defaults,
608612
raw,
609613
);
614+
if (outcome === "handled_by_hook") return;
610615
if (outcome !== "not_configured") {
611616
journal.add({
612617
method: req.method ?? "POST",
@@ -723,7 +728,7 @@ export async function handleConverse(
723728

724729
// Tool call response
725730
if (isToolCallResponse(response)) {
726-
if ("webSearches" in response) {
731+
if (response.webSearches?.length) {
727732
logger.warn(
728733
"webSearches in fixture response are not supported for Bedrock Converse API — ignoring",
729734
);
@@ -877,6 +882,7 @@ export async function handleConverseStream(
877882
defaults,
878883
raw,
879884
);
885+
if (outcome === "handled_by_hook") return;
880886
if (outcome !== "not_configured") {
881887
journal.add({
882888
method: req.method ?? "POST",
@@ -1023,7 +1029,7 @@ export async function handleConverseStream(
10231029

10241030
// Tool call response — stream as Event Stream
10251031
if (isToolCallResponse(response)) {
1026-
if ("webSearches" in response) {
1032+
if (response.webSearches?.length) {
10271033
logger.warn(
10281034
"webSearches in fixture response are not supported for Bedrock Converse API — ignoring",
10291035
);

src/bedrock.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export function bedrockToCompletionRequest(
207207
if (toolUseBlocks.length > 0) {
208208
messages.push({
209209
role: "assistant",
210-
content: textContent ?? null,
210+
content: textContent || null,
211211
tool_calls: toolUseBlocks.map((b, index) => {
212212
if (!b.id && logger) {
213213
logger.warn(
@@ -225,7 +225,7 @@ export function bedrockToCompletionRequest(
225225
}),
226226
});
227227
} else {
228-
messages.push({ role: "assistant", content: textContent ?? null });
228+
messages.push({ role: "assistant", content: textContent || null });
229229
}
230230
} else {
231231
messages.push({ role: "assistant", content: null });
@@ -257,6 +257,7 @@ export function bedrockToCompletionRequest(
257257
messages,
258258
stream: false,
259259
temperature: req.temperature,
260+
max_tokens: req.max_tokens,
260261
tools,
261262
};
262263
}
@@ -438,6 +439,7 @@ export async function handleBedrock(
438439
defaults,
439440
raw,
440441
);
442+
if (outcome === "handled_by_hook") return;
441443
if (outcome !== "not_configured") {
442444
journal.add({
443445
method: req.method ?? "POST",
@@ -571,7 +573,7 @@ export async function handleBedrock(
571573

572574
// Tool call response
573575
if (isToolCallResponse(response)) {
574-
if ("webSearches" in response) {
576+
if (response.webSearches?.length) {
575577
logger.warn("webSearches in fixture response are not supported for Bedrock API — ignoring");
576578
}
577579
const overrides = extractOverrides(response);
@@ -1058,6 +1060,7 @@ export async function handleBedrockStream(
10581060
defaults,
10591061
raw,
10601062
);
1063+
if (outcome === "handled_by_hook") return;
10611064
if (outcome !== "not_configured") {
10621065
journal.add({
10631066
method: req.method ?? "POST",
@@ -1204,7 +1207,7 @@ export async function handleBedrockStream(
12041207

12051208
// Tool call response — stream as Event Stream
12061209
if (isToolCallResponse(response)) {
1207-
if ("webSearches" in response) {
1210+
if (response.webSearches?.length) {
12081211
logger.warn("webSearches in fixture response are not supported for Bedrock API — ignoring");
12091212
}
12101213
const overrides = extractOverrides(response);

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Options:
2222
-l, --latency <ms> Latency in ms between SSE chunks (default: 0)
2323
-c, --chunk-size <chars> Chunk size in characters (default: 20)
2424
-w, --watch Watch fixture path for changes and reload
25-
--log-level <level> Log verbosity: silent, info, debug (default: info)
25+
--log-level <level> Log verbosity: silent, warn, info, debug (default: info)
2626
--validate-on-load Validate fixture schemas at startup
2727
--metrics Enable Prometheus metrics at GET /metrics
2828
--record Record mode: proxy unmatched requests and save fixtures

src/cohere.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
isToolCallResponse,
2929
isContentWithToolCallsResponse,
3030
isErrorResponse,
31+
serializeErrorResponse,
3132
flattenHeaders,
3233
getTestId,
3334
resolveResponse,
@@ -874,6 +875,7 @@ export async function handleCohere(
874875
defaults,
875876
raw,
876877
);
878+
if (outcome === "handled_by_hook") return;
877879
if (outcome !== "not_configured") {
878880
journal.add({
879881
method: req.method ?? "POST",
@@ -933,7 +935,7 @@ export async function handleCohere(
933935
body: completionReq,
934936
response: { status, fixture },
935937
});
936-
writeErrorResponse(res, status, JSON.stringify(response));
938+
writeErrorResponse(res, status, serializeErrorResponse(response));
937939
return;
938940
}
939941

@@ -1033,6 +1035,11 @@ export async function handleCohere(
10331035

10341036
// Tool call response
10351037
if (isToolCallResponse(response)) {
1038+
if (response.webSearches?.length) {
1039+
logger.warn(
1040+
"webSearches in fixture response are not supported for Cohere v2 Chat API — ignoring",
1041+
);
1042+
}
10361043
const overrides = extractOverrides(response);
10371044
const journalEntry = journal.add({
10381045
method: req.method ?? "POST",

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** Sentinel testId used when no explicit test scope is provided. */
2+
export const DEFAULT_TEST_ID = "__default__";

0 commit comments

Comments
 (0)