Files
obsidian_ollama/tests/ollama-client.test.js
fegger 2d78882594 chore: move compiled output to dist
Update the plugin entrypoint and TypeScript output directory to use dist instead of writing generated JavaScript into src.

Remove previously checked-in compiled source files and add coverage for the Ollama client and tool executor.
2026-05-19 17:30:05 +02:00

1513 lines
82 KiB
JavaScript

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
var ollama_client_1 = require("../src/ollama-client");
describe('OllamaClient', function () {
var client;
var mockFetch;
function createMockReader(data) {
var encoder = new TextEncoder();
var encoded = encoder.encode(data);
var called = false;
return {
read: function () {
if (!called) {
called = true;
return Promise.resolve({ done: false, value: encoded });
}
return Promise.resolve({ done: true, value: new Uint8Array(0) });
},
releaseLock: jest.fn(),
};
}
var mockMessages = [
{ role: 'system', content: 'You are helpful.' },
{ role: 'user', content: 'Hello' },
];
var mockTools = [
{
type: 'function',
function: {
name: 'test_tool',
description: 'A test tool',
parameters: {
type: 'object',
properties: { input: { type: 'string' } },
required: ['input'],
},
},
},
];
beforeEach(function () {
mockFetch = jest.fn();
client = new ollama_client_1.OllamaClient('http://localhost:11434', 'llama3', mockFetch);
});
afterEach(function () {
jest.clearAllMocks();
// Removed cancelStream call as we now use local controllers
});
describe('chat (non-streaming)', function () {
it('should send a non-streaming request and return the response', function () { return __awaiter(void 0, void 0, void 0, function () {
var mockResponse, result;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockResponse = {
ok: true,
json: function () { return Promise.resolve({ message: { content: 'Hello back!' } }); },
};
mockFetch.mockResolvedValue(mockResponse);
return [4 /*yield*/, client.chat(mockMessages, mockTools)];
case 1:
result = _a.sent();
expect(result.content).toBe('Hello back!');
expect(mockFetch).toHaveBeenCalledWith('http://localhost:11434/api/chat', expect.objectContaining({
method: 'POST',
body: JSON.stringify({
model: 'llama3',
messages: mockMessages,
tools: mockTools,
stream: false,
}),
}));
return [2 /*return*/];
}
});
}); });
it('should throw on non-OK response', function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({ ok: false, status: 500 });
return [4 /*yield*/, expect(client.chat(mockMessages, mockTools)).rejects.toThrow('Ollama API error: 500')];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should handle missing message content gracefully', function () { return __awaiter(void 0, void 0, void 0, function () {
var result;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({
ok: true,
json: function () { return Promise.resolve({}); },
});
return [4 /*yield*/, client.chat(mockMessages, mockTools)];
case 1:
result = _a.sent();
expect(result.content).toBe('');
expect(result.tool_calls).toEqual([]);
return [2 /*return*/];
}
});
}); });
it('should include abort signal in fetch options', function () { return __awaiter(void 0, void 0, void 0, function () {
var fetchOptions;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({
ok: true,
json: function () { return Promise.resolve({ message: { content: 'ok' } }); },
});
return [4 /*yield*/, client.chat(mockMessages, mockTools)];
case 1:
_a.sent();
fetchOptions = mockFetch.mock.calls[0][1];
expect(fetchOptions.signal).toBeInstanceOf(AbortSignal);
return [2 /*return*/];
}
});
}); });
it('should forward tool_calls from response when present', function () { return __awaiter(void 0, void 0, void 0, function () {
var result;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({
ok: true,
json: function () {
return Promise.resolve({
message: {
content: 'result',
tool_calls: [{ function: { name: 'create_file', arguments: '{}' } }],
},
});
},
});
return [4 /*yield*/, client.chat(mockMessages, mockTools)];
case 1:
result = _a.sent();
expect(result.tool_calls).toEqual([{ function: { name: 'create_file', arguments: '{}' } }]);
return [2 /*return*/];
}
});
}); });
});
describe('streamChat', function () {
it('should send a streaming request and yield chunks', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream, chunks, _a, stream_1, stream_1_1, chunk, e_1_1;
var _b, e_1, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = [
JSON.stringify({ message: { content: 'He' } }),
JSON.stringify({ message: { content: 'llo' } }),
JSON.stringify({ message: { content: '!' } }),
'',
].join('\n');
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
chunks = [];
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_1 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_1.next()];
case 4:
if (!(stream_1_1 = _e.sent(), _b = stream_1_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_1_1.value;
_a = false;
chunk = _d;
chunks.push(chunk.content);
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_1_1 = _e.sent();
e_1 = { error: e_1_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_1.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_1)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_1) throw e_1.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(chunks).toEqual(['He', 'llo', '!']);
expect(mockReader.releaseLock).toHaveBeenCalled();
return [2 /*return*/];
}
});
}); });
it('should skip malformed JSON chunks and log a warning', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, consoleWarnSpy, stream, chunks, _a, stream_2, stream_2_1, chunk, e_2_1;
var _b, e_2, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = [
JSON.stringify({ message: { content: 'valid' } }),
'this is not json',
JSON.stringify({ message: { content: 'also valid' } }),
'',
].join('\n');
mockReader = createMockReader(streamData);
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
chunks = [];
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_2 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_2.next()];
case 4:
if (!(stream_2_1 = _e.sent(), _b = stream_2_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_2_1.value;
_a = false;
chunk = _d;
chunks.push(chunk.content);
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_2_1 = _e.sent();
e_2 = { error: e_2_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_2.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_2)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_2) throw e_2.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(chunks).toEqual(['valid', 'also valid']);
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[ollama-client] WARN: Skipped malformed chunk: this is not json... - Unexpected token \'h\', "this is not json" is not valid JSON'));
consoleWarnSpy.mockRestore();
return [2 /*return*/];
}
});
}); });
it('should throw when too many chunks are malformed', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
streamData = Array(51).fill('invalid json').join('\n') + '\n';
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _a.sent();
return [4 /*yield*/, expect((function () { return __awaiter(void 0, void 0, void 0, function () {
var _a, stream_3, stream_3_1, _, e_3_1;
var _b, e_3, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 5, 6, 11]);
_a = true, stream_3 = __asyncValues(stream);
_e.label = 1;
case 1: return [4 /*yield*/, stream_3.next()];
case 2:
if (!(stream_3_1 = _e.sent(), _b = stream_3_1.done, !_b)) return [3 /*break*/, 4];
_d = stream_3_1.value;
_a = false;
_ = _d;
_e.label = 3;
case 3:
_a = true;
return [3 /*break*/, 1];
case 4: return [3 /*break*/, 11];
case 5:
e_3_1 = _e.sent();
e_3 = { error: e_3_1 };
return [3 /*break*/, 11];
case 6:
_e.trys.push([6, , 9, 10]);
if (!(!_a && !_b && (_c = stream_3.return))) return [3 /*break*/, 8];
return [4 /*yield*/, _c.call(stream_3)];
case 7:
_e.sent();
_e.label = 8;
case 8: return [3 /*break*/, 10];
case 9:
if (e_3) throw e_3.error;
return [7 /*endfinally*/];
case 10: return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
}); })()).rejects.toThrow(/malformed/)];
case 2:
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should throw on non-OK response', function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({ ok: false, status: 404 });
return [4 /*yield*/, expect(client.streamChatAsPromise(mockMessages, mockTools)).rejects.toThrow('Ollama API error: 404')];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should throw when response has no body', function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({ ok: true, body: undefined });
return [4 /*yield*/, expect(client.streamChatAsPromise(mockMessages, mockTools)).rejects.toThrow('No response body')];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should throw on invalid content type', function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({
ok: true,
body: {
getReader: function () { return ({
read: function () { return Promise.resolve({ done: true, value: new Uint8Array(0) }); },
}); },
},
headers: {
get: function (name) { return (name === 'content-type' ? 'text/html' : null); },
},
});
return [4 /*yield*/, expect(client.streamChatAsPromise(mockMessages, mockTools)).rejects.toThrow('Invalid response format')];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should propagate Ollama error messages from the stream', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
streamData = JSON.stringify({ error: 'model not found' }) + '\n';
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _a.sent();
return [4 /*yield*/, expect((function () { return __awaiter(void 0, void 0, void 0, function () {
var _a, stream_4, stream_4_1, _, e_4_1;
var _b, e_4, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 5, 6, 11]);
_a = true, stream_4 = __asyncValues(stream);
_e.label = 1;
case 1: return [4 /*yield*/, stream_4.next()];
case 2:
if (!(stream_4_1 = _e.sent(), _b = stream_4_1.done, !_b)) return [3 /*break*/, 4];
_d = stream_4_1.value;
_a = false;
_ = _d;
_e.label = 3;
case 3:
_a = true;
return [3 /*break*/, 1];
case 4: return [3 /*break*/, 11];
case 5:
e_4_1 = _e.sent();
e_4 = { error: e_4_1 };
return [3 /*break*/, 11];
case 6:
_e.trys.push([6, , 9, 10]);
if (!(!_a && !_b && (_c = stream_4.return))) return [3 /*break*/, 8];
return [4 /*yield*/, _c.call(stream_4)];
case 7:
_e.sent();
_e.label = 8;
case 8: return [3 /*break*/, 10];
case 9:
if (e_4) throw e_4.error;
return [7 /*endfinally*/];
case 10: return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
}); })()).rejects.toThrow('Ollama error: model not found')];
case 2:
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should yield tool_calls when present in streamed response', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream, lastChunk, _a, stream_5, stream_5_1, chunk, e_5_1;
var _b, e_5, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = [
JSON.stringify({
message: {
content: '',
tool_calls: [{ function: { name: 'create_file', arguments: '{"path":"a.md"}' } }],
},
}),
'',
].join('\n');
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_5 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_5.next()];
case 4:
if (!(stream_5_1 = _e.sent(), _b = stream_5_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_5_1.value;
_a = false;
chunk = _d;
lastChunk = chunk;
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_5_1 = _e.sent();
e_5 = { error: e_5_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_5.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_5)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_5) throw e_5.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(lastChunk.tool_calls).toEqual([
{ function: { name: 'create_file', arguments: '{"path":"a.md"}' } },
]);
return [2 /*return*/];
}
});
}); });
it('should default tool_calls to empty array when not present', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream, lastChunk, _a, stream_6, stream_6_1, chunk, e_6_1;
var _b, e_6, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = JSON.stringify({ message: { content: 'hello' } }) + '\n';
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_6 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_6.next()];
case 4:
if (!(stream_6_1 = _e.sent(), _b = stream_6_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_6_1.value;
_a = false;
chunk = _d;
lastChunk = chunk;
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_6_1 = _e.sent();
e_6 = { error: e_6_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_6.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_6)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_6) throw e_6.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(lastChunk.tool_calls).toEqual([]);
return [2 /*return*/];
}
});
}); });
it('should send correct request body with stream:true', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream, _a, stream_7, stream_7_1, _, e_7_1;
var _b, e_7, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = JSON.stringify({ message: { content: 'ok' } }) + '\n';
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_7 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_7.next()];
case 4:
if (!(stream_7_1 = _e.sent(), _b = stream_7_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_7_1.value;
_a = false;
_ = _d;
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_7_1 = _e.sent();
e_7 = { error: e_7_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_7.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_7)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_7) throw e_7.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(mockFetch).toHaveBeenCalledWith('http://localhost:11434/api/chat', expect.objectContaining({
method: 'POST',
body: JSON.stringify({
model: 'llama3',
messages: mockMessages,
tools: mockTools,
stream: true,
}),
signal: expect.any(AbortSignal),
}));
return [2 /*return*/];
}
});
}); });
});
describe('streamChat with retry logic', function () {
it('should retry on 5xx errors and eventually succeed', function () { return __awaiter(void 0, void 0, void 0, function () {
var callCount, stream, chunks, _a, stream_8, stream_8_1, _, e_8_1;
var _b, e_8, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
callCount = 0;
mockFetch.mockImplementation(function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
callCount++;
if (callCount === 1) {
return [2 /*return*/, { ok: false, status: 500 }];
}
if (callCount === 2) {
return [2 /*return*/, { ok: false, status: 502 }];
}
return [2 /*return*/, {
ok: true,
body: {
getReader: function () { return ({
read: function () { return Promise.resolve({ done: true, value: new Uint8Array(0) }); },
releaseLock: function () { },
}); },
},
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
}];
});
}); });
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
chunks = [];
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_8 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_8.next()];
case 4:
if (!(stream_8_1 = _e.sent(), _b = stream_8_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_8_1.value;
_a = false;
_ = _d;
chunks.push('chunk');
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_8_1 = _e.sent();
e_8 = { error: e_8_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_8.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_8)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_8) throw e_8.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(callCount).toBe(3);
expect(chunks.length).toBe(0);
return [2 /*return*/];
}
});
}); });
it('should give up after maxRetries attempts', function () { return __awaiter(void 0, void 0, void 0, function () {
var stream;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({ ok: false, status: 500 });
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _a.sent();
return [4 /*yield*/, expect((function () { return __awaiter(void 0, void 0, void 0, function () {
var _a, stream_9, stream_9_1, _, e_9_1;
var _b, e_9, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 5, 6, 11]);
_a = true, stream_9 = __asyncValues(stream);
_e.label = 1;
case 1: return [4 /*yield*/, stream_9.next()];
case 2:
if (!(stream_9_1 = _e.sent(), _b = stream_9_1.done, !_b)) return [3 /*break*/, 4];
_d = stream_9_1.value;
_a = false;
_ = _d;
_e.label = 3;
case 3:
_a = true;
return [3 /*break*/, 1];
case 4: return [3 /*break*/, 11];
case 5:
e_9_1 = _e.sent();
e_9 = { error: e_9_1 };
return [3 /*break*/, 11];
case 6:
_e.trys.push([6, , 9, 10]);
if (!(!_a && !_b && (_c = stream_9.return))) return [3 /*break*/, 8];
return [4 /*yield*/, _c.call(stream_9)];
case 7:
_e.sent();
_e.label = 8;
case 8: return [3 /*break*/, 10];
case 9:
if (e_9) throw e_9.error;
return [7 /*endfinally*/];
case 10: return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
}); })()).rejects.toThrow('Ollama API error: 500')];
case 2:
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should not retry on 4xx errors', function () { return __awaiter(void 0, void 0, void 0, function () {
var stream;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockResolvedValue({ ok: false, status: 404 });
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _a.sent();
return [4 /*yield*/, expect((function () { return __awaiter(void 0, void 0, void 0, function () {
var _a, stream_10, stream_10_1, _, e_10_1;
var _b, e_10, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 5, 6, 11]);
_a = true, stream_10 = __asyncValues(stream);
_e.label = 1;
case 1: return [4 /*yield*/, stream_10.next()];
case 2:
if (!(stream_10_1 = _e.sent(), _b = stream_10_1.done, !_b)) return [3 /*break*/, 4];
_d = stream_10_1.value;
_a = false;
_ = _d;
_e.label = 3;
case 3:
_a = true;
return [3 /*break*/, 1];
case 4: return [3 /*break*/, 11];
case 5:
e_10_1 = _e.sent();
e_10 = { error: e_10_1 };
return [3 /*break*/, 11];
case 6:
_e.trys.push([6, , 9, 10]);
if (!(!_a && !_b && (_c = stream_10.return))) return [3 /*break*/, 8];
return [4 /*yield*/, _c.call(stream_10)];
case 7:
_e.sent();
_e.label = 8;
case 8: return [3 /*break*/, 10];
case 9:
if (e_10) throw e_10.error;
return [7 /*endfinally*/];
case 10: return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
}); })()).rejects.toThrow('Ollama API error: 404')];
case 2:
_a.sent();
return [2 /*return*/];
}
});
}); });
});
describe('cancelStream', function () {
it('should abort an active streaming request when cancelled before fetch resolves', function () { return __awaiter(void 0, void 0, void 0, function () {
var capturedSignal, consumeStream, consumePromise;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockImplementation(function (_url, options) {
capturedSignal = options === null || options === void 0 ? void 0 : options.signal;
return Promise.race([
// Simulate slow network response
new Promise(function () {
// Never resolves on its own - relies on abort
}),
// Reject when signal is aborted (like real fetch does)
new Promise(function (_, reject) {
if (capturedSignal === null || capturedSignal === void 0 ? void 0 : capturedSignal.aborted) {
reject(new DOMException('The operation was aborted.', 'AbortError'));
return;
}
capturedSignal === null || capturedSignal === void 0 ? void 0 : capturedSignal.addEventListener('abort', function () {
reject(new DOMException('The operation was aborted.', 'AbortError'));
});
}),
]);
});
consumeStream = function () { return __awaiter(void 0, void 0, void 0, function () {
var stream, _a, stream_11, stream_11_1, _, e_11_1;
var _b, e_11, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_11 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_11.next()];
case 4:
if (!(stream_11_1 = _e.sent(), _b = stream_11_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_11_1.value;
_a = false;
_ = _d;
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_11_1 = _e.sent();
e_11 = { error: e_11_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_11.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_11)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_11) throw e_11.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13: return [2 /*return*/];
}
});
}); };
consumePromise = consumeStream();
// Wait a tick for fetch to be invoked
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })];
case 1:
// Wait a tick for fetch to be invoked
_a.sent();
// Verify controller is set after fetch starts
expect(client['currentStreamController']).not.toBeNull();
// Cancel the stream - this aborts the controller and triggers fetch rejection
client.cancelStream();
// Verify signal was aborted
expect(capturedSignal === null || capturedSignal === void 0 ? void 0 : capturedSignal.aborted).toBe(true);
// Verify controller was cleared by cancelStream
expect(client['currentStreamController']).toBeNull();
// The stream consumption must reject with an abort error
return [4 /*yield*/, expect(consumePromise).rejects.toThrow('The operation was aborted.')];
case 2:
// The stream consumption must reject with an abort error
_a.sent();
return [2 /*return*/];
}
});
}); });
it('should handle cancel when no active stream', function () {
// Calling cancelStream with no active stream must not throw
expect(function () { return client.cancelStream(); }).not.toThrow();
expect(client['currentStreamController']).toBeNull();
});
it('should clear the controller after stream completes normally', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream, _a, stream_12, stream_12_1, _, e_12_1;
var _b, e_12, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = JSON.stringify({ message: { content: 'done' } }) + '\n';
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_12 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_12.next()];
case 4:
if (!(stream_12_1 = _e.sent(), _b = stream_12_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_12_1.value;
_a = false;
_ = _d;
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_12_1 = _e.sent();
e_12 = { error: e_12_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_12.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_12)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_12) throw e_12.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
// Controller should be cleared after normal completion
expect(client['currentStreamController']).toBeNull();
return [2 /*return*/];
}
});
}); });
it('should allow a new stream after cancelling a previous one', function () { return __awaiter(void 0, void 0, void 0, function () {
var consumeFirst, firstStreamPromise, stream2, _a, stream2_1, stream2_1_1, _, e_13_1;
var _b, e_13, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
// First fetch: pending and abortable
mockFetch.mockImplementationOnce(function (_url, options) {
var signal = options === null || options === void 0 ? void 0 : options.signal;
return Promise.race([
new Promise(function () {
// Never resolves on its own
}),
new Promise(function (_, reject) {
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
reject(new DOMException('The operation was aborted.', 'AbortError'));
return;
}
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', function () {
reject(new DOMException('The operation was aborted.', 'AbortError'));
});
}),
]);
});
// Second fetch: resolves immediately with valid stream
mockFetch.mockResolvedValueOnce({
ok: true,
body: {
getReader: function () { return ({
read: function () { return Promise.resolve({ done: true, value: new Uint8Array(0) }); },
releaseLock: function () { },
}); },
},
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
consumeFirst = function () { return __awaiter(void 0, void 0, void 0, function () {
var stream, _a, stream_13, stream_13_1, _, e_14_1;
var _b, e_14, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_13 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_13.next()];
case 4:
if (!(stream_13_1 = _e.sent(), _b = stream_13_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_13_1.value;
_a = false;
_ = _d;
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_14_1 = _e.sent();
e_14 = { error: e_14_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_13.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_13)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_14) throw e_14.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13: return [2 /*return*/];
}
});
}); };
firstStreamPromise = consumeFirst();
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })];
case 1:
_e.sent();
// Cancel first stream
client.cancelStream();
return [4 /*yield*/, expect(firstStreamPromise).rejects.toThrow('The operation was aborted.')];
case 2:
_e.sent();
// Controller is cleared, can start a new stream
expect(client['currentStreamController']).toBeNull();
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 3:
stream2 = _e.sent();
_e.label = 4;
case 4:
_e.trys.push([4, 9, 10, 15]);
_a = true, stream2_1 = __asyncValues(stream2);
_e.label = 5;
case 5: return [4 /*yield*/, stream2_1.next()];
case 6:
if (!(stream2_1_1 = _e.sent(), _b = stream2_1_1.done, !_b)) return [3 /*break*/, 8];
_d = stream2_1_1.value;
_a = false;
_ = _d;
_e.label = 7;
case 7:
_a = true;
return [3 /*break*/, 5];
case 8: return [3 /*break*/, 15];
case 9:
e_13_1 = _e.sent();
e_13 = { error: e_13_1 };
return [3 /*break*/, 15];
case 10:
_e.trys.push([10, , 13, 14]);
if (!(!_a && !_b && (_c = stream2_1.return))) return [3 /*break*/, 12];
return [4 /*yield*/, _c.call(stream2_1)];
case 11:
_e.sent();
_e.label = 12;
case 12: return [3 /*break*/, 14];
case 13:
if (e_13) throw e_13.error;
return [7 /*endfinally*/];
case 14: return [7 /*endfinally*/];
case 15:
// Second stream completes and clears controller
expect(client['currentStreamController']).toBeNull();
return [2 /*return*/];
}
});
}); });
});
describe('cancelStream race condition', function () {
it('should not clear new stream controller when old stream finally block executes', function () { return __awaiter(void 0, void 0, void 0, function () {
var fireFirstAbort, stream1, consumeFirst, firstPromise, firstController, stream2, consumeSecond, secondPromise, secondController;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
mockFetch.mockImplementationOnce(function (_url, options) {
var signal = options === null || options === void 0 ? void 0 : options.signal;
return new Promise(function (_, reject) {
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
reject(new DOMException('The operation was aborted.', 'AbortError'));
return;
}
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', function () {
reject(new DOMException('The operation was aborted.', 'AbortError'));
}, { once: true });
// Capture abort trigger for manual control
fireFirstAbort = function () {
signal === null || signal === void 0 ? void 0 : signal.dispatchEvent(new CustomEvent('abort'));
};
});
});
// Second fetch call: also pending, so controller stays assigned
// We don't need it to complete - just need to verify controller survives abort
mockFetch.mockImplementationOnce(function (_url, options) {
var signal = options === null || options === void 0 ? void 0 : options.signal;
return new Promise(function (_, reject) {
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
reject(new DOMException('The operation was aborted.', 'AbortError'));
return;
}
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', function () {
reject(new DOMException('The operation was aborted.', 'AbortError'));
}, { once: true });
});
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream1 = _a.sent();
consumeFirst = function () { return __awaiter(void 0, void 0, void 0, function () {
var _a, stream1_1, stream1_1_1, _, e_15_1;
var _b, e_15, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 5, 6, 11]);
_a = true, stream1_1 = __asyncValues(stream1);
_e.label = 1;
case 1: return [4 /*yield*/, stream1_1.next()];
case 2:
if (!(stream1_1_1 = _e.sent(), _b = stream1_1_1.done, !_b)) return [3 /*break*/, 4];
_d = stream1_1_1.value;
_a = false;
_ = _d;
_e.label = 3;
case 3:
_a = true;
return [3 /*break*/, 1];
case 4: return [3 /*break*/, 11];
case 5:
e_15_1 = _e.sent();
e_15 = { error: e_15_1 };
return [3 /*break*/, 11];
case 6:
_e.trys.push([6, , 9, 10]);
if (!(!_a && !_b && (_c = stream1_1.return))) return [3 /*break*/, 8];
return [4 /*yield*/, _c.call(stream1_1)];
case 7:
_e.sent();
_e.label = 8;
case 8: return [3 /*break*/, 10];
case 9:
if (e_15) throw e_15.error;
return [7 /*endfinally*/];
case 10: return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
}); };
firstPromise = consumeFirst();
// Wait for fetch to be triggered (controller should be set)
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })];
case 2:
// Wait for fetch to be triggered (controller should be set)
_a.sent();
firstController = client['currentStreamController'];
expect(firstController).not.toBeNull();
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 3:
stream2 = _a.sent();
consumeSecond = function () { return __awaiter(void 0, void 0, void 0, function () {
var _a, stream2_2, stream2_2_1, _, e_16_1;
var _b, e_16, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 5, 6, 11]);
_a = true, stream2_2 = __asyncValues(stream2);
_e.label = 1;
case 1: return [4 /*yield*/, stream2_2.next()];
case 2:
if (!(stream2_2_1 = _e.sent(), _b = stream2_2_1.done, !_b)) return [3 /*break*/, 4];
_d = stream2_2_1.value;
_a = false;
_ = _d;
_e.label = 3;
case 3:
_a = true;
return [3 /*break*/, 1];
case 4: return [3 /*break*/, 11];
case 5:
e_16_1 = _e.sent();
e_16 = { error: e_16_1 };
return [3 /*break*/, 11];
case 6:
_e.trys.push([6, , 9, 10]);
if (!(!_a && !_b && (_c = stream2_2.return))) return [3 /*break*/, 8];
return [4 /*yield*/, _c.call(stream2_2)];
case 7:
_e.sent();
_e.label = 8;
case 8: return [3 /*break*/, 10];
case 9:
if (e_16) throw e_16.error;
return [7 /*endfinally*/];
case 10: return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
}); };
secondPromise = consumeSecond();
// Wait for second stream fetch to trigger and assign its controller
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })];
case 4:
// Wait for second stream fetch to trigger and assign its controller
_a.sent();
secondController = client['currentStreamController'];
expect(secondController).not.toBeNull();
expect(secondController).not.toBe(firstController);
// Defer abort to next microtask so Jest associates rejection with expectation
// Then immediately await the rejection
queueMicrotask(function () { return fireFirstAbort === null || fireFirstAbort === void 0 ? void 0 : fireFirstAbort(); });
return [4 /*yield*/, expect(firstPromise).rejects.toThrow('The operation was aborted.')];
case 5:
_a.sent();
// Second stream's controller should still be intact
// (not cleared by first stream's finally block due to guard)
expect(client['currentStreamController']).toBe(secondController);
// Clean up second stream so it doesn't leak
secondController === null || secondController === void 0 ? void 0 : secondController.abort();
return [4 /*yield*/, secondPromise.catch(function () { })];
case 6:
_a.sent();
return [2 /*return*/];
}
});
}); });
});
describe('streamChat final buffer parsing', function () {
it('should parse final buffer content when stream ends with partial line', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream, chunks, _a, stream_14, stream_14_1, chunk, e_17_1;
var _b, e_17, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = [
JSON.stringify({ message: { content: 'First' } }),
JSON.stringify({ message: { content: 'Second' } }),
JSON.stringify({ message: { content: 'Third' } }),
'', // Final line should be empty to signal end
].join('\n');
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
chunks = [];
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_14 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_14.next()];
case 4:
if (!(stream_14_1 = _e.sent(), _b = stream_14_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_14_1.value;
_a = false;
chunk = _d;
chunks.push(chunk.content);
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_17_1 = _e.sent();
e_17 = { error: e_17_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_14.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_14)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_17) throw e_17.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(chunks).toEqual(['First', 'Second', 'Third']);
expect(mockReader.releaseLock).toHaveBeenCalled();
return [2 /*return*/];
}
});
}); });
it('should handle malformed final buffer content gracefully', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, consoleWarnSpy, stream, chunks, _a, stream_15, stream_15_1, chunk, e_18_1;
var _b, e_18, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = [
JSON.stringify({ message: { content: 'Valid' } }),
'malformed json',
'', // Final line should be empty to signal end
].join('\n');
mockReader = createMockReader(streamData);
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
chunks = [];
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_15 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_15.next()];
case 4:
if (!(stream_15_1 = _e.sent(), _b = stream_15_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_15_1.value;
_a = false;
chunk = _d;
chunks.push(chunk.content);
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_18_1 = _e.sent();
e_18 = { error: e_18_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_15.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_15)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_18) throw e_18.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(chunks).toEqual(['Valid']);
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Skipped malformed chunk'));
consoleWarnSpy.mockRestore();
return [2 /*return*/];
}
});
}); });
it('should parse final buffer content even when it contains message data', function () { return __awaiter(void 0, void 0, void 0, function () {
var streamData, mockReader, stream, chunks, _a, stream_16, stream_16_1, chunk, e_19_1;
var _b, e_19, _c, _d;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
streamData = [
JSON.stringify({ message: { content: 'First' } }),
'', // Final line should be empty to signal end
JSON.stringify({ message: { content: 'Final' } }),
].join('\n');
mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: function () { return mockReader; } },
headers: {
get: function (name) { return (name === 'content-type' ? 'application/x-ndjson' : null); },
},
});
return [4 /*yield*/, client.streamChat(mockMessages, mockTools)];
case 1:
stream = _e.sent();
chunks = [];
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 13]);
_a = true, stream_16 = __asyncValues(stream);
_e.label = 3;
case 3: return [4 /*yield*/, stream_16.next()];
case 4:
if (!(stream_16_1 = _e.sent(), _b = stream_16_1.done, !_b)) return [3 /*break*/, 6];
_d = stream_16_1.value;
_a = false;
chunk = _d;
chunks.push(chunk.content);
_e.label = 5;
case 5:
_a = true;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 13];
case 7:
e_19_1 = _e.sent();
e_19 = { error: e_19_1 };
return [3 /*break*/, 13];
case 8:
_e.trys.push([8, , 11, 12]);
if (!(!_a && !_b && (_c = stream_16.return))) return [3 /*break*/, 10];
return [4 /*yield*/, _c.call(stream_16)];
case 9:
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 12];
case 11:
if (e_19) throw e_19.error;
return [7 /*endfinally*/];
case 12: return [7 /*endfinally*/];
case 13:
expect(chunks).toEqual(['First', 'Final']);
expect(mockReader.releaseLock).toHaveBeenCalled();
return [2 /*return*/];
}
});
}); });
});
});