From 23ccb13373983bf20160e5937121404a40759f4c Mon Sep 17 00:00:00 2001 From: Aria Zhao Date: Sun, 21 Jun 2026 03:45:58 +0000 Subject: [PATCH] fix(core): guard message inspectors against empty parts arrays `isFunctionCall()` and `isFunctionResponse()` in `messageInspectors.ts` returned `true` for messages with an empty `parts` array, because `[].every(...)` is vacuously `true` in JavaScript. This misclassified any `role: "model"` message with `parts: []` as a function call, and any `role: "user"` message with `parts: []` as a function response, affecting the routing, loop-detection, and chat subsystems that rely on these checks. Add a `parts.length > 0` guard to both functions so empty-parts messages are correctly treated as neither a function call nor a function response. Also add the previously missing test coverage for `messageInspectors.ts`. Fixes #23195 Fixes #22912 --- .../core/src/utils/messageInspectors.test.ts | 111 ++++++++++++++++++ packages/core/src/utils/messageInspectors.ts | 2 + 2 files changed, 113 insertions(+) create mode 100644 packages/core/src/utils/messageInspectors.test.ts diff --git a/packages/core/src/utils/messageInspectors.test.ts b/packages/core/src/utils/messageInspectors.test.ts new file mode 100644 index 00000000000..487cc5f8c29 --- /dev/null +++ b/packages/core/src/utils/messageInspectors.test.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import type { Content } from '@google/genai'; +import { isFunctionCall, isFunctionResponse } from './messageInspectors.js'; + +describe('messageInspectors', () => { + describe('isFunctionResponse', () => { + it('returns true for a user message whose parts are all function responses', () => { + const content: Content = { + role: 'user', + parts: [ + { functionResponse: { name: 'tool', response: { ok: true } } }, + { functionResponse: { name: 'tool2', response: { ok: true } } }, + ], + }; + expect(isFunctionResponse(content)).toBe(true); + }); + + it('returns false when the role is not "user"', () => { + const content: Content = { + role: 'model', + parts: [{ functionResponse: { name: 'tool', response: { ok: true } } }], + }; + expect(isFunctionResponse(content)).toBe(false); + }); + + it('returns false when a part is not a function response', () => { + const content: Content = { + role: 'user', + parts: [{ text: 'hello' }], + }; + expect(isFunctionResponse(content)).toBe(false); + }); + + it('returns false when parts are mixed', () => { + const content: Content = { + role: 'user', + parts: [ + { functionResponse: { name: 'tool', response: { ok: true } } }, + { text: 'hello' }, + ], + }; + expect(isFunctionResponse(content)).toBe(false); + }); + + it('returns false when parts is undefined', () => { + const content: Content = { role: 'user' }; + expect(isFunctionResponse(content)).toBe(false); + }); + + it('returns false when parts is an empty array', () => { + const content: Content = { role: 'user', parts: [] }; + expect(isFunctionResponse(content)).toBe(false); + }); + }); + + describe('isFunctionCall', () => { + it('returns true for a model message whose parts are all function calls', () => { + const content: Content = { + role: 'model', + parts: [ + { functionCall: { name: 'tool', args: {} } }, + { functionCall: { name: 'tool2', args: {} } }, + ], + }; + expect(isFunctionCall(content)).toBe(true); + }); + + it('returns false when the role is not "model"', () => { + const content: Content = { + role: 'user', + parts: [{ functionCall: { name: 'tool', args: {} } }], + }; + expect(isFunctionCall(content)).toBe(false); + }); + + it('returns false when a part is not a function call', () => { + const content: Content = { + role: 'model', + parts: [{ text: 'hello' }], + }; + expect(isFunctionCall(content)).toBe(false); + }); + + it('returns false when parts are mixed', () => { + const content: Content = { + role: 'model', + parts: [ + { functionCall: { name: 'tool', args: {} } }, + { text: 'hello' }, + ], + }; + expect(isFunctionCall(content)).toBe(false); + }); + + it('returns false when parts is undefined', () => { + const content: Content = { role: 'model' }; + expect(isFunctionCall(content)).toBe(false); + }); + + it('returns false when parts is an empty array', () => { + const content: Content = { role: 'model', parts: [] }; + expect(isFunctionCall(content)).toBe(false); + }); + }); +}); diff --git a/packages/core/src/utils/messageInspectors.ts b/packages/core/src/utils/messageInspectors.ts index 9a77094694c..a695a938fcf 100644 --- a/packages/core/src/utils/messageInspectors.ts +++ b/packages/core/src/utils/messageInspectors.ts @@ -10,6 +10,7 @@ export function isFunctionResponse(content: Content): boolean { return ( content.role === 'user' && !!content.parts && + content.parts.length > 0 && content.parts.every((part) => !!part.functionResponse) ); } @@ -18,6 +19,7 @@ export function isFunctionCall(content: Content): boolean { return ( content.role === 'model' && !!content.parts && + content.parts.length > 0 && content.parts.every((part) => !!part.functionCall) ); }