From 570f89b7b547906961c59e91219e5dcce1274917 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:42:01 +0000 Subject: [PATCH 1/3] Initial plan From 054bfcfec1874cdf8531fc53709b4a5d8301513f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:48:06 +0000 Subject: [PATCH 2/3] refactor: extract duplicate WS frame helpers into shared test-helper --- .../test-helpers/websocket-frame-helpers.js | 49 +++++++++++++++ .../api-proxy/token-tracker.schema.test.js | 61 ++----------------- 2 files changed, 53 insertions(+), 57 deletions(-) create mode 100644 containers/api-proxy/test-helpers/websocket-frame-helpers.js diff --git a/containers/api-proxy/test-helpers/websocket-frame-helpers.js b/containers/api-proxy/test-helpers/websocket-frame-helpers.js new file mode 100644 index 000000000..ec4ea104e --- /dev/null +++ b/containers/api-proxy/test-helpers/websocket-frame-helpers.js @@ -0,0 +1,49 @@ +'use strict'; + +/** + * Build a minimal unmasked WebSocket text frame for a given UTF-8 string. + * Only supports payloads up to 125 bytes (single-byte length field). + * + * @param {string} text JSON (or any string) to encode as a WS text frame + * @returns {Buffer} + */ +function buildFrame(text) { + const payload = Buffer.from(text, 'utf8'); + const header = Buffer.alloc(2); + header[0] = 0x81; // FIN + opcode 1 (text) + header[1] = payload.length; + return Buffer.concat([header, payload]); +} + +/** + * Return the standard HTTP/1.1 101 Switching Protocols header buffer used in + * all Anthropic WebSocket upgrade tests. + * + * @returns {Buffer} + */ +function buildHttpUpgradeHeader() { + return Buffer.from('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n'); +} + +/** + * Build the canonical Anthropic streaming WebSocket payload consisting of: + * - HTTP 101 upgrade header + * - `message_start` frame (input_tokens: 20, output_tokens: 0) + * - `message_delta` frame (output_tokens: 8) + * + * @returns {Buffer} A single buffer ready to emit on the socket 'data' event + */ +function buildAnthropicUsageFrames() { + const httpHeader = buildHttpUpgradeHeader(); + const frame1 = buildFrame(JSON.stringify({ + type: 'message_start', + message: { model: 'claude-sonnet-4-20250514', usage: { input_tokens: 20, output_tokens: 0 } }, + })); + const frame2 = buildFrame(JSON.stringify({ + type: 'message_delta', + usage: { output_tokens: 8 }, + })); + return Buffer.concat([httpHeader, frame1, frame2]); +} + +module.exports = { buildFrame, buildHttpUpgradeHeader, buildAnthropicUsageFrames }; diff --git a/containers/api-proxy/token-tracker.schema.test.js b/containers/api-proxy/token-tracker.schema.test.js index 5d635ee3b..646d09d12 100644 --- a/containers/api-proxy/token-tracker.schema.test.js +++ b/containers/api-proxy/token-tracker.schema.test.js @@ -21,6 +21,7 @@ const { TOKEN_DIAG_SCHEMA, } = require('./token-persistence'); const { EventEmitter } = require('events'); +const { buildAnthropicUsageFrames } = require('./test-helpers/websocket-frame-helpers'); afterAll(async () => { await closeLogStream(); @@ -276,24 +277,6 @@ describe('token-usage JSONL record schema field', () => { test('trackWebSocketTokenUsage path writes versioned _schema to the stream', (done) => { const socket = new EventEmitter(); - function buildFrame(text) { - const payload = Buffer.from(text, 'utf8'); - const header = Buffer.alloc(2); - header[0] = 0x81; - header[1] = payload.length; - return Buffer.concat([header, payload]); - } - - const httpHeader = Buffer.from('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n'); - const frame1 = buildFrame(JSON.stringify({ - type: 'message_start', - message: { model: 'claude-sonnet-4-20250514', usage: { input_tokens: 20, output_tokens: 0 } }, - })); - const frame2 = buildFrame(JSON.stringify({ - type: 'message_delta', - usage: { output_tokens: 8 }, - })); - trackWebSocketTokenUsage(socket, { requestId: 'schema-field-ws', provider: 'anthropic', @@ -302,7 +285,7 @@ describe('token-usage JSONL record schema field', () => { metrics: null, }); - socket.emit('data', Buffer.concat([httpHeader, frame1, frame2])); + socket.emit('data', buildAnthropicUsageFrames()); socket.emit('close'); setTimeout(() => { @@ -390,24 +373,6 @@ describe('token-usage JSONL record schema field', () => { test('trackWebSocketTokenUsage path persists optional budget fields returned by onUsage', (done) => { const socket = new EventEmitter(); - function buildFrame(text) { - const payload = Buffer.from(text, 'utf8'); - const header = Buffer.alloc(2); - header[0] = 0x81; - header[1] = payload.length; - return Buffer.concat([header, payload]); - } - - const httpHeader = Buffer.from('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n'); - const frame1 = buildFrame(JSON.stringify({ - type: 'message_start', - message: { model: 'claude-sonnet-4-20250514', usage: { input_tokens: 20, output_tokens: 0 } }, - })); - const frame2 = buildFrame(JSON.stringify({ - type: 'message_delta', - usage: { output_tokens: 8 }, - })); - trackWebSocketTokenUsage(socket, { requestId: 'budget-fields-ws', provider: 'anthropic', @@ -423,7 +388,7 @@ describe('token-usage JSONL record schema field', () => { }), }); - socket.emit('data', Buffer.concat([httpHeader, frame1, frame2])); + socket.emit('data', buildAnthropicUsageFrames()); socket.emit('close'); setTimeout(() => { @@ -444,24 +409,6 @@ describe('token-usage JSONL record schema field', () => { test('trackWebSocketTokenUsage path omits optional budget fields when onUsage returns undefined', (done) => { const socket = new EventEmitter(); - function buildFrame(text) { - const payload = Buffer.from(text, 'utf8'); - const header = Buffer.alloc(2); - header[0] = 0x81; - header[1] = payload.length; - return Buffer.concat([header, payload]); - } - - const httpHeader = Buffer.from('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n'); - const frame1 = buildFrame(JSON.stringify({ - type: 'message_start', - message: { model: 'claude-sonnet-4-20250514', usage: { input_tokens: 20, output_tokens: 0 } }, - })); - const frame2 = buildFrame(JSON.stringify({ - type: 'message_delta', - usage: { output_tokens: 8 }, - })); - trackWebSocketTokenUsage(socket, { requestId: 'budget-fields-ws-none', provider: 'anthropic', @@ -471,7 +418,7 @@ describe('token-usage JSONL record schema field', () => { onUsage: () => undefined, }); - socket.emit('data', Buffer.concat([httpHeader, frame1, frame2])); + socket.emit('data', buildAnthropicUsageFrames()); socket.emit('close'); setTimeout(() => { From f341c501aac78cb7fe278f030d3fecd4a8f551d3 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Mon, 15 Jun 2026 08:01:46 -0700 Subject: [PATCH 3/3] fix: add payload length guard to buildFrame helper Throws a clear error if payload exceeds 125 bytes (the single-byte length limit), preventing silent production of invalid WebSocket frames. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- containers/api-proxy/test-helpers/websocket-frame-helpers.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/containers/api-proxy/test-helpers/websocket-frame-helpers.js b/containers/api-proxy/test-helpers/websocket-frame-helpers.js index ec4ea104e..3c1aafed2 100644 --- a/containers/api-proxy/test-helpers/websocket-frame-helpers.js +++ b/containers/api-proxy/test-helpers/websocket-frame-helpers.js @@ -9,6 +9,9 @@ */ function buildFrame(text) { const payload = Buffer.from(text, 'utf8'); + if (payload.length > 125) { + throw new Error(`buildFrame only supports payloads up to 125 bytes, got ${payload.length}. Use a shorter payload or extend the helper to support extended length fields.`); + } const header = Buffer.alloc(2); header[0] = 0x81; // FIN + opcode 1 (text) header[1] = payload.length;