mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-18 13:37:23 +02:00
58e9881d6e
GeminiAdapter was reading message.text and result.usage, so current CLI content/stats events produced empty $0 success rows. Accept content with an assistant role guard, stats token fallbacks, init model, and treat empty exit-0 output as an error (#2159). Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
3.8 KiB
TypeScript
96 lines
3.8 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { parseGeminiStreamJson } from './gemini';
|
|
|
|
// Current CLI shape (gemini ≥ ~1.58): content + role, tokens under result.stats
|
|
const CURRENT_CLI_FIXTURE = [
|
|
'{"type":"init","timestamp":"2026-03-20T15:14:46.455Z","session_id":"test-session-123","model":"gemini-2.5-pro"}',
|
|
'{"type":"message","role":"user","content":"Reply with exactly the word: PONG"}',
|
|
'{"type":"message","role":"assistant","content":"PONG","delta":true}',
|
|
'{"type":"result","status":"success","stats":{"input_tokens":24946,"output_tokens":32,"total_tokens":24978}}',
|
|
].join('\n');
|
|
|
|
// Legacy shape: text field, tokens under result.usage
|
|
const LEGACY_FIXTURE = [
|
|
'{"type":"message","text":"hello"}',
|
|
'{"type":"tool_use","name":"run_shell_command"}',
|
|
'{"type":"result","usage":{"input_token_count":100,"output_token_count":5},"model":"gemini-2.5-flash"}',
|
|
].join('\n');
|
|
|
|
describe('parseGeminiStreamJson', () => {
|
|
test('current CLI: reads assistant content, ignores user echo, stats tokens, init model', () => {
|
|
const parsed = parseGeminiStreamJson(CURRENT_CLI_FIXTURE);
|
|
expect(parsed.output).toBe('PONG');
|
|
expect(parsed.tokens.input).toBe(24946);
|
|
expect(parsed.tokens.output).toBe(32);
|
|
expect(parsed.modelUsed).toBe('gemini-2.5-pro');
|
|
expect(parsed.toolCalls).toBe(0);
|
|
});
|
|
|
|
test('current CLI: user role content must not concat into output', () => {
|
|
const raw = [
|
|
'{"type":"message","role":"user","content":"PROMPT ECHO"}',
|
|
'{"type":"message","role":"assistant","content":"ok","delta":true}',
|
|
].join('\n');
|
|
const parsed = parseGeminiStreamJson(raw);
|
|
expect(parsed.output).toBe('ok');
|
|
expect(parsed.output).not.toContain('PROMPT ECHO');
|
|
});
|
|
|
|
test('legacy: still accepts text + usage.input_token_count', () => {
|
|
const parsed = parseGeminiStreamJson(LEGACY_FIXTURE);
|
|
expect(parsed.output).toBe('hello');
|
|
expect(parsed.tokens.input).toBe(100);
|
|
expect(parsed.tokens.output).toBe(5);
|
|
expect(parsed.toolCalls).toBe(1);
|
|
expect(parsed.modelUsed).toBe('gemini-2.5-flash');
|
|
});
|
|
|
|
test('legacy text with role:user is ignored', () => {
|
|
const raw = [
|
|
'{"type":"message","role":"user","text":"echo"}',
|
|
'{"type":"message","role":"assistant","text":"kept"}',
|
|
].join('\n');
|
|
const parsed = parseGeminiStreamJson(raw);
|
|
expect(parsed.output).toBe('kept');
|
|
});
|
|
|
|
test('concatenates multiple assistant content deltas', () => {
|
|
const raw = [
|
|
'{"type":"message","role":"assistant","content":"A","delta":true}',
|
|
'{"type":"message","role":"assistant","content":"B","delta":true}',
|
|
].join('\n');
|
|
expect(parseGeminiStreamJson(raw).output).toBe('AB');
|
|
});
|
|
|
|
test('skips malformed lines without throwing', () => {
|
|
const raw = [
|
|
'{"type":"init","model":"m1"}',
|
|
'not json',
|
|
'{"type":"message","role":"assistant","content":"x","delta":true}',
|
|
'{incomplete',
|
|
'{"type":"result","stats":{"input_tokens":1,"output_tokens":2}}',
|
|
].join('\n');
|
|
const parsed = parseGeminiStreamJson(raw);
|
|
expect(parsed.output).toBe('x');
|
|
expect(parsed.tokens).toEqual({ input: 1, output: 2 });
|
|
expect(parsed.modelUsed).toBe('m1');
|
|
});
|
|
|
|
test('empty / whitespace-only input yields empty parse (no throw)', () => {
|
|
const parsed = parseGeminiStreamJson('');
|
|
expect(parsed.output).toBe('');
|
|
expect(parsed.tokens).toEqual({ input: 0, output: 0 });
|
|
expect(parsed.toolCalls).toBe(0);
|
|
expect(parsed.modelUsed).toBeUndefined();
|
|
});
|
|
|
|
test('result.model overrides init.model when both present', () => {
|
|
const raw = [
|
|
'{"type":"init","model":"from-init"}',
|
|
'{"type":"message","role":"assistant","content":"hi"}',
|
|
'{"type":"result","model":"from-result","stats":{"input_tokens":1,"output_tokens":1}}',
|
|
].join('\n');
|
|
expect(parseGeminiStreamJson(raw).modelUsed).toBe('from-result');
|
|
});
|
|
});
|