fix(ai): Fix sendMessageStream bug that sends duplicate user turns (#9956)

This commit is contained in:
Christina Holland
2026-05-13 10:58:28 -07:00
committed by GitHub
parent 5a181cff29
commit 1c185fb195
3 changed files with 76 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@firebase/ai': patch
---
Fix a bug that caused `ChatSession.sendMessageStream()` and `TemplateChatSession.sendMessageStream()` to send duplicate user turns in the request.
+7 -8
View File
@@ -163,10 +163,9 @@ export abstract class ChatSessionBase<
} else {
formattedContent = formatNewContent(request);
}
const formattedRequest = this._formatRequest(
formattedContent,
tempHistory
);
const formattedRequest = this._formatRequest(formattedContent, [
...tempHistory
]);
tempHistory.push(formattedContent);
@@ -260,11 +259,11 @@ export abstract class ChatSessionBase<
formattedContent = formatNewContent(request);
}
const formattedRequest = this._formatRequest(formattedContent, [
...tempHistory
]);
tempHistory.push(formattedContent);
const formattedRequest = this._formatRequest(
formattedContent,
tempHistory
);
result = await this._callGenerateContentStream(
formattedRequest,
@@ -60,6 +60,36 @@ describe('ChatSession', () => {
restore();
});
describe('sendMessage()', () => {
it('sends the correct params to generateContent()', async () => {
const generateContentStub = stub(
generateContentMethods,
'generateContent'
).resolves();
const chatSession = new ChatSession(
fakeApiSettings,
'a-model',
fakeChromeAdapter,
{
history: [
{ role: 'user', parts: [{ text: 'user turn 1' }] },
{ role: 'model', parts: [{ text: 'model turn 1' }] }
]
}
);
// The result isn't important.
await chatSession.sendMessage('user turn 2');
expect(generateContentStub).to.be.calledWith(
fakeApiSettings,
'a-model',
match.any,
match.any
);
expect(generateContentStub.args[0][2].contents).to.deep.equal([
{ role: 'user', parts: [{ text: 'user turn 1' }] },
{ role: 'model', parts: [{ text: 'model turn 1' }] },
{ role: 'user', parts: [{ text: 'user turn 2' }] }
]);
});
it('generateContent errors should be catchable', async () => {
const generateContentStub = stub(
generateContentMethods,
@@ -185,9 +215,43 @@ describe('ChatSession', () => {
expect(generateContentStub.args[1][2].contents[2].parts[0].text).to.equal(
'hello 2'
);
expect(generateContentStub.args[1][2].contents.length).to.equal(3);
});
});
describe('sendMessageStream()', () => {
it('sends the correct params to generateContentStream()', async () => {
const clock = useFakeTimers();
const generateContentStreamStub = stub(
generateContentMethods,
'generateContentStream'
).resolves();
const chatSession = new ChatSession(
fakeApiSettings,
'a-model',
fakeChromeAdapter,
{
history: [
{ role: 'user', parts: [{ text: 'user turn 1' }] },
{ role: 'model', parts: [{ text: 'model turn 1' }] }
]
}
);
// Expected as the stub resolved undefined, the result isn't important.
await expect(chatSession.sendMessageStream('user turn 2')).to.be.rejected;
expect(generateContentStreamStub).to.be.calledWith(
fakeApiSettings,
'a-model',
match.any,
match.any
);
expect(generateContentStreamStub.args[0][2].contents).to.deep.equal([
{ role: 'user', parts: [{ text: 'user turn 1' }] },
{ role: 'model', parts: [{ text: 'model turn 1' }] },
{ role: 'user', parts: [{ text: 'user turn 2' }] }
]);
await clock.runAllAsync();
clock.restore();
});
it('generateContentStream errors should be catchable', async () => {
const clock = useFakeTimers();
const consoleStub = stub(console, 'error');