diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts index d92ef0f8..53cc9139 100644 --- a/packages/cli/src/nonInteractiveCli.test.ts +++ b/packages/cli/src/nonInteractiveCli.test.ts @@ -22,6 +22,7 @@ import { import type { Part } from '@google/genai'; import { runNonInteractive } from './nonInteractiveCli.js'; import { vi } from 'vitest'; +import type { StreamJsonUserEnvelope } from './streamJson/types.js'; import type { LoadedSettings } from './config/settings.js'; // Mock core modules @@ -943,6 +944,63 @@ describe('runNonInteractive', () => { }); }); + it('should emit a single user envelope when userEnvelope is provided', async () => { + (mockConfig.getOutputFormat as vi.Mock).mockReturnValue('stream-json'); + (mockConfig.getIncludePartialMessages as vi.Mock).mockReturnValue(false); + + const writes: string[] = []; + processStdoutSpy.mockImplementation((chunk: string | Uint8Array) => { + if (typeof chunk === 'string') { + writes.push(chunk); + } else { + writes.push(Buffer.from(chunk).toString('utf8')); + } + return true; + }); + + mockGeminiClient.sendMessageStream.mockReturnValue( + createStreamFromEvents([ + { type: GeminiEventType.Content, value: 'Handled once' }, + { + type: GeminiEventType.Finished, + value: { reason: undefined, usageMetadata: { totalTokenCount: 2 } }, + }, + ]), + ); + + const userEnvelope = { + type: 'user', + message: { + role: 'user', + content: [ + { + type: 'text', + text: '来自 envelope 的消息', + }, + ], + }, + } as unknown as StreamJsonUserEnvelope; + + await runNonInteractive( + mockConfig, + mockSettings, + 'ignored input', + 'prompt-envelope', + { + userEnvelope, + }, + ); + + const envelopes = writes + .join('') + .split('\n') + .filter((line) => line.trim().length > 0) + .map((line) => JSON.parse(line)); + + const userEnvelopes = envelopes.filter((env) => env.type === 'user'); + expect(userEnvelopes).toHaveLength(0); + }); + it('should include usage metadata and API duration in stream-json result', async () => { (mockConfig.getOutputFormat as vi.Mock).mockReturnValue('stream-json'); (mockConfig.getIncludePartialMessages as vi.Mock).mockReturnValue(false); diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index ffd7b9bd..a71e5bba 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -201,6 +201,7 @@ export async function runNonInteractive( let initialPartList: PartListUnion | null = extractPartsFromEnvelope( options.userEnvelope, ); + let usedEnvelopeInput = initialPartList !== null; if (!initialPartList) { let slashHandled = false; @@ -215,6 +216,7 @@ export async function runNonInteractive( // A slash command can replace the prompt entirely; fall back to @-command processing otherwise. initialPartList = slashCommandResult as PartListUnion; slashHandled = true; + usedEnvelopeInput = false; } } @@ -236,17 +238,19 @@ export async function runNonInteractive( ); } initialPartList = processedQuery as PartListUnion; + usedEnvelopeInput = false; } } if (!initialPartList) { initialPartList = [{ text: input }]; + usedEnvelopeInput = false; } const initialParts = normalizePartList(initialPartList); let currentMessages: Content[] = [{ role: 'user', parts: initialParts }]; - if (streamJsonWriter) { + if (streamJsonWriter && !usedEnvelopeInput) { streamJsonWriter.emitUserMessageFromParts(initialParts); }