Files
multica/server/pkg/agent/gemini_test.go
Sanjay Ramadugu f99f50eb0c feat(daemon): add Google Gemini CLI backend
Registers `gemini` as a sixth supported agent provider alongside claude,
codex, opencode, openclaw, and hermes.

- Daemon config probes for `gemini` on PATH (MULTICA_GEMINI_PATH /
  MULTICA_GEMINI_MODEL env overrides mirror the other providers).
- New agent.geminiBackend in pkg/agent/gemini.go: spawns
  `gemini -p <prompt> --yolo -o text [-m <model>] [-r <session>]`,
  reads stdout to completion, and returns a single MessageText plus
  the standard Result struct (Status / Output / DurationMs).
- Execution environment writes a GEMINI.md file into the task workdir
  (mirroring the existing CLAUDE.md / AGENTS.md injection for other
  providers) so Gemini discovers the Multica runtime meta-skill
  through its native mechanism.

Tests:

- pkg/agent/gemini_test.go — unit coverage for buildGeminiArgs
  (baseline, model override, resume session, omit-when-empty).
- internal/daemon/execenv/TestInjectRuntimeConfigGemini — verifies
  GEMINI.md is written and that CLAUDE.md/AGENTS.md are NOT.

Scope (intentional for v1):

- Text output only (`-o text`). Streaming tool events via
  `--output-format stream-json` is a follow-up once we have a
  reliable reproduction of Gemini's event schema.
- No MCP config plumbing. Gemini's `--allowed-mcp-server-names`
  filter pairs well with the per-agent MCP work on feat/per-agent-mcp;
  stacking the two can land as a follow-up.
- No token usage scraping (Gemini's accounting lives on the Google
  Cloud side, not a local JSONL log like claude/codex).
- No session resume wiring beyond accepting the ExecOptions field —
  the daemon does not yet persist Gemini session IDs because the text
  output mode does not expose them.

Migration / env changes:

- New optional environment variables MULTICA_GEMINI_PATH and
  MULTICA_GEMINI_MODEL. Default path is the string "gemini" (resolved
  via PATH at daemon startup). If no Gemini install is detected, the
  provider is simply absent from the runtime — no behavior change for
  existing deployments.
2026-04-11 22:58:49 -04:00

80 lines
1.7 KiB
Go

package agent
import (
"testing"
)
func TestBuildGeminiArgsBaseline(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("write a haiku", ExecOptions{})
expected := []string{
"-p", "write a haiku",
"--yolo",
"-o", "text",
}
if len(args) != len(expected) {
t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
}
for i, want := range expected {
if args[i] != want {
t.Fatalf("expected args[%d] = %q, got %q", i, want, args[i])
}
}
}
func TestBuildGeminiArgsWithModel(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{Model: "gemini-2.5-pro"})
var foundModel bool
for i, a := range args {
if a == "-m" {
if i+1 >= len(args) || args[i+1] != "gemini-2.5-pro" {
t.Fatalf("expected -m followed by gemini-2.5-pro, got %v", args)
}
foundModel = true
break
}
}
if !foundModel {
t.Fatalf("expected -m flag when Model is set, got args=%v", args)
}
}
func TestBuildGeminiArgsWithResume(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{ResumeSessionID: "3"})
var foundResume bool
for i, a := range args {
if a == "-r" {
if i+1 >= len(args) || args[i+1] != "3" {
t.Fatalf("expected -r followed by session id, got %v", args)
}
foundResume = true
break
}
}
if !foundResume {
t.Fatalf("expected -r flag when ResumeSessionID is set, got args=%v", args)
}
}
func TestBuildGeminiArgsOmitsModelWhenEmpty(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{})
for _, a := range args {
if a == "-m" {
t.Fatalf("expected no -m flag when Model is empty, got args=%v", args)
}
if a == "-r" {
t.Fatalf("expected no -r flag when ResumeSessionID is empty, got args=%v", args)
}
}
}