Skip to content

Commit f73e401

Browse files
committed
add unit tests for chat internal errors
1 parent 60f5c56 commit f73e401

File tree

2 files changed

+69
-2
lines changed

2 files changed

+69
-2
lines changed

packages/ai/src/methods/chat-session.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ChatSession } from './chat-session';
2525
import { ApiSettings } from '../types/internal';
2626
import { VertexAIBackend } from '../backend';
2727
import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services';
28+
import { logger } from '../logger';
2829

2930
use(sinonChai);
3031
use(chaiAsPromised);
@@ -220,6 +221,70 @@ describe('ChatSession', () => {
220221
);
221222
clock.restore();
222223
});
224+
it('logs error and rejects user promise when response aggregation fails', async () => {
225+
const loggerStub = stub(logger, 'error');
226+
const error = new Error('Aggregation failed');
227+
228+
// Simulate stream returning, but the response promise failing (e.g. parsing error)
229+
stub(generateContentMethods, 'generateContentStream').resolves({
230+
stream: (async function* () {})(),
231+
response: Promise.reject(error)
232+
} as unknown as GenerateContentStreamResult);
233+
234+
const chatSession = new ChatSession(fakeApiSettings, 'a-model');
235+
const initialHistoryLength = (await chatSession.getHistory()).length;
236+
237+
// Immediate call resolves with the stream object
238+
const result = await chatSession.sendMessageStream('hello');
239+
240+
// User's response promise should reject
241+
await expect(result.response).to.be.rejectedWith(error);
242+
243+
// Wait for the internal _sendPromise chain to settle
244+
await new Promise(resolve => setTimeout(resolve, 0));
245+
246+
expect(loggerStub).to.have.been.calledWith(error);
247+
248+
// History should NOT have been updated (no response appended)
249+
const finalHistory = await chatSession.getHistory();
250+
expect(finalHistory.length).to.equal(initialHistoryLength);
251+
});
252+
it('logs error but resolves user promise when history appending logic fails', async () => {
253+
const loggerStub = stub(logger, 'error');
254+
255+
// Simulate a response that is technically valid enough to resolve aggregation,
256+
// but malformed in a way that causes the history update logic to throw.
257+
// Passing `null` as a candidate causes `{ ...response.candidates[0].content }` to throw.
258+
const malformedResponse = {
259+
candidates: [null]
260+
};
261+
262+
stub(generateContentMethods, 'generateContentStream').resolves({
263+
stream: (async function* () {})(),
264+
response: Promise.resolve(malformedResponse)
265+
} as unknown as GenerateContentStreamResult);
266+
267+
const chatSession = new ChatSession(fakeApiSettings, 'a-model');
268+
const initialHistoryLength = (await chatSession.getHistory()).length;
269+
270+
const result = await chatSession.sendMessageStream('hello');
271+
272+
// The user's response promise SHOULD resolve, because aggregation succeeded.
273+
// The error is purely internal side-effect (history update).
274+
await expect(result.response).to.eventually.equal(malformedResponse);
275+
276+
// Wait for internal chain
277+
await new Promise(resolve => setTimeout(resolve, 0));
278+
279+
expect(loggerStub).to.have.been.called;
280+
const errorArg = loggerStub.firstCall.args[0];
281+
expect(errorArg).to.be.instanceOf(TypeError);
282+
283+
// The user message WAS added before the crash, but the response wasn't.
284+
const finalHistory = await chatSession.getHistory();
285+
expect(finalHistory.length).to.equal(initialHistoryLength + 1);
286+
expect(finalHistory[finalHistory.length - 1].role).to.equal('user');
287+
});
223288
it('error from stream promise should not be logged', async () => {
224289
const consoleStub = stub(console, 'error');
225290
stub(generateContentMethods, 'generateContentStream').rejects('foo');
@@ -313,4 +378,4 @@ describe('ChatSession', () => {
313378
);
314379
});
315380
});
316-
});
381+
});

packages/ai/src/requests/request.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ describe('request methods', () => {
308308
const signal = options!.signal;
309309
return new Promise((_resolve, reject): void => {
310310
const abortListener = (): void => {
311-
reject(new DOMException(signal?.reason || 'Aborted', ABORT_ERROR_NAME));
311+
reject(
312+
new DOMException(signal?.reason || 'Aborted', ABORT_ERROR_NAME)
313+
);
312314
};
313315

314316
signal?.addEventListener('abort', abortListener, { once: true });

0 commit comments

Comments
 (0)