@@ -3,6 +3,7 @@ package tools
33import (
44 "context"
55 "os/exec"
6+ "strings"
67 "testing"
78 "time"
89)
@@ -45,6 +46,68 @@ func TestParseStreamLine_InvalidJSON(t *testing.T) {
4546 }
4647}
4748
49+ func TestParseStreamLine_GeminiMessage (t * testing.T ) {
50+ line := []byte (`{"type":"message","role":"assistant","content":"Hello","delta":true}` )
51+ evt := parseStreamLine (line )
52+ if evt .Type != "text" {
53+ t .Errorf ("Type = %q, want text" , evt .Type )
54+ }
55+ if evt .Content != "Hello" {
56+ t .Errorf ("Content = %q" , evt .Content )
57+ }
58+ }
59+
60+ func TestParseStreamLine_GeminiResult (t * testing.T ) {
61+ line := []byte (`{"type":"result","status":"success","stats":{"total_tokens":100}}` )
62+ evt := parseStreamLine (line )
63+ if evt .Type != "result" {
64+ t .Errorf ("Type = %q, want result" , evt .Type )
65+ }
66+ }
67+
68+ func TestParseStreamLine_GeminiUserMessage (t * testing.T ) {
69+ line := []byte (`{"type":"message","role":"user","content":"prompt"}` )
70+ evt := parseStreamLine (line )
71+ if evt .Type != "" {
72+ t .Errorf ("user messages should be dropped, got Type=%q" , evt .Type )
73+ }
74+ }
75+
76+ func TestParseStreamLine_CodexItemCompleted (t * testing.T ) {
77+ line := []byte (`{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"Hello"}}` )
78+ evt := parseStreamLine (line )
79+ if evt .Type != "text" {
80+ t .Errorf ("Type = %q, want text" , evt .Type )
81+ }
82+ if evt .Content != "Hello" {
83+ t .Errorf ("Content = %q" , evt .Content )
84+ }
85+ }
86+
87+ func TestParseStreamLine_CodexReasoningIgnored (t * testing.T ) {
88+ line := []byte (`{"type":"item.completed","item":{"type":"reasoning","text":"thinking..."}}` )
89+ evt := parseStreamLine (line )
90+ if evt .Type != "" {
91+ t .Errorf ("reasoning items should be dropped, got Type=%q" , evt .Type )
92+ }
93+ }
94+
95+ func TestParseStreamLine_CodexTurnCompleted (t * testing.T ) {
96+ line := []byte (`{"type":"turn.completed","usage":{"input_tokens":100}}` )
97+ evt := parseStreamLine (line )
98+ if evt .Type != "" {
99+ t .Errorf ("turn.completed should be dropped, got Type=%q" , evt .Type )
100+ }
101+ }
102+
103+ func TestParseStreamLine_GeminiInit (t * testing.T ) {
104+ line := []byte (`{"type":"init","session_id":"abc"}` )
105+ evt := parseStreamLine (line )
106+ if evt .Type != "" {
107+ t .Errorf ("init should be dropped, got Type=%q" , evt .Type )
108+ }
109+ }
110+
48111func TestParseStreamLine_EmptyType (t * testing.T ) {
49112 line := []byte (`{"content":"orphan"}` )
50113 evt := parseStreamLine (line )
@@ -54,7 +117,7 @@ func TestParseStreamLine_EmptyType(t *testing.T) {
54117}
55118
56119func TestStreamRunner_BuildsClaudeStreamArgs (t * testing.T ) {
57- r := NewStreamRunner (AgentInfo {Kind : AgentClaude , Binary : "/usr/local/bin/ claude" })
120+ r := NewStreamRunner (AgentInfo {Kind : AgentClaude , Binary : "claude" })
58121 args := r .buildStreamArgs (runOptions {})
59122 want := []string {"--print" , "--verbose" , "--output-format" , "stream-json" }
60123 if len (args ) != len (want ) {
@@ -68,7 +131,7 @@ func TestStreamRunner_BuildsClaudeStreamArgs(t *testing.T) {
68131}
69132
70133func TestStreamRunner_BuildsGeminiStreamArgs (t * testing.T ) {
71- r := NewStreamRunner (AgentInfo {Kind : AgentGemini , Binary : "/usr/local/bin/ gemini" })
134+ r := NewStreamRunner (AgentInfo {Kind : AgentGemini , Binary : "gemini" })
72135 args := r .buildStreamArgs (runOptions {})
73136 want := []string {"-o" , "stream-json" }
74137 if len (args ) != len (want ) {
@@ -81,8 +144,22 @@ func TestStreamRunner_BuildsGeminiStreamArgs(t *testing.T) {
81144 }
82145}
83146
147+ func TestStreamRunner_BuildsCodexStreamArgs (t * testing.T ) {
148+ r := NewStreamRunner (AgentInfo {Kind : AgentCodex , Binary : "codex" })
149+ args := r .buildStreamArgs (runOptions {})
150+ want := []string {"exec" , "--json" }
151+ if len (args ) != len (want ) {
152+ t .Fatalf ("args = %v, want %v" , args , want )
153+ }
154+ for i , a := range args {
155+ if a != want [i ] {
156+ t .Errorf ("args[%d] = %q, want %q" , i , a , want [i ])
157+ }
158+ }
159+ }
160+
84161func TestStreamRunner_BuildsClaudeStreamArgsWithBudget (t * testing.T ) {
85- r := NewStreamRunner (AgentInfo {Kind : AgentClaude , Binary : "/usr/local/bin/ claude" })
162+ r := NewStreamRunner (AgentInfo {Kind : AgentClaude , Binary : "claude" })
86163 args := r .buildStreamArgs (runOptions {maxBudgetUSD : 0.50 })
87164 found := false
88165 for i , a := range args {
@@ -154,3 +231,59 @@ func TestStreamRunner_RealClaude(t *testing.T) {
154231 t .Error ("expected at least one event" )
155232 }
156233}
234+
235+ func TestStreamRunner_RealGemini (t * testing.T ) {
236+ if _ , err := exec .LookPath ("gemini" ); err != nil {
237+ t .Skip ("gemini not on PATH" )
238+ }
239+ agents := DetectAgents ()
240+ var info AgentInfo
241+ for _ , a := range agents {
242+ if a .Kind == AgentGemini {
243+ info = a
244+ break
245+ }
246+ }
247+ r := NewStreamRunner (info )
248+ var eventCount int
249+ out , err := r .RunStream (context .Background (), "Say hello in exactly one word." , 30 * time .Second , func (evt StreamEvent ) {
250+ eventCount ++
251+ })
252+ if err != nil {
253+ if strings .Contains (err .Error (), "Permission" ) || strings .Contains (err .Error (), "denied" ) || strings .Contains (err .Error (), "auth" ) {
254+ t .Skipf ("gemini auth not configured: %v" , err )
255+ }
256+ t .Fatalf ("RunStream: %v" , err )
257+ }
258+ if out == "" {
259+ t .Error ("expected non-empty output" )
260+ }
261+ }
262+
263+ func TestStreamRunner_RealCodex (t * testing.T ) {
264+ if _ , err := exec .LookPath ("codex" ); err != nil {
265+ t .Skip ("codex not on PATH" )
266+ }
267+ agents := DetectAgents ()
268+ var info AgentInfo
269+ for _ , a := range agents {
270+ if a .Kind == AgentCodex {
271+ info = a
272+ break
273+ }
274+ }
275+ r := NewStreamRunner (info )
276+ var eventCount int
277+ out , err := r .RunStream (context .Background (), "Say hello in exactly one word." , 30 * time .Second , func (evt StreamEvent ) {
278+ eventCount ++
279+ })
280+ if err != nil {
281+ if strings .Contains (err .Error (), "auth" ) || strings .Contains (err .Error (), "API key" ) || strings .Contains (err .Error (), "login" ) {
282+ t .Skipf ("codex auth not configured: %v" , err )
283+ }
284+ t .Fatalf ("RunStream: %v" , err )
285+ }
286+ if out == "" {
287+ t .Error ("expected non-empty output" )
288+ }
289+ }
0 commit comments