openspec/lightweight-tasks/task1-2-4-1-1.md

Add user envelope handling in runNonInteractive function
This commit is contained in:
x22x22
2025-10-30 13:35:32 +08:00
parent f5f378f262
commit ae19d05e63
2 changed files with 63 additions and 1 deletions

View File

@@ -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);

View File

@@ -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);
}