mirror of
https://github.com/ollama/ollama.git
synced 2026-06-05 21:05:00 +08:00
688 lines
19 KiB
Go
688 lines
19 KiB
Go
package launch
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
modelpkg "github.com/ollama/ollama/types/model"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
if os.Getenv("OLLAMA_LAUNCH_OMP_TEST_HELPER") == "1" {
|
|
runOMPTestHelper()
|
|
return
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func runOMPTestHelper() {
|
|
logPath := os.Getenv("OLLAMA_LAUNCH_OMP_TEST_LOG")
|
|
if logPath != "" {
|
|
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
|
if err == nil {
|
|
_, _ = fmt.Fprintln(f, strings.Join(os.Args[1:], " "))
|
|
_ = f.Close()
|
|
}
|
|
}
|
|
|
|
if len(os.Args) >= 3 && os.Args[1] == "plugin" && os.Args[2] == "list" {
|
|
fmt.Print(os.Getenv("OLLAMA_LAUNCH_OMP_TEST_PLUGIN_LIST"))
|
|
os.Exit(0)
|
|
}
|
|
if len(os.Args) >= 4 && os.Args[1] == "plugin" && os.Args[2] == "install" {
|
|
if os.Getenv("OLLAMA_LAUNCH_OMP_TEST_FAIL_INSTALL") == "1" {
|
|
_, _ = fmt.Fprintln(os.Stderr, "install failed")
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func setOMPTestHome(t *testing.T, dir string) {
|
|
t.Helper()
|
|
setTestHome(t, dir)
|
|
t.Setenv("PI_CONFIG_DIR", "")
|
|
t.Setenv("PI_CODING_AGENT_DIR", "")
|
|
}
|
|
|
|
func TestOMPIntegration(t *testing.T) {
|
|
o := &OMP{}
|
|
|
|
t.Run("String", func(t *testing.T) {
|
|
if got := o.String(); got != "OMP" {
|
|
t.Errorf("String() = %q, want %q", got, "OMP")
|
|
}
|
|
})
|
|
|
|
t.Run("implements Runner", func(t *testing.T) {
|
|
var _ Runner = o
|
|
})
|
|
|
|
t.Run("implements ManagedSingleModel", func(t *testing.T) {
|
|
var _ ManagedSingleModel = o
|
|
})
|
|
|
|
t.Run("implements ManagedModelListConfigurer", func(t *testing.T) {
|
|
var _ ManagedModelListConfigurer = o
|
|
})
|
|
|
|
t.Run("does not require interactive onboarding", func(t *testing.T) {
|
|
var _ ManagedInteractiveOnboarding = o
|
|
if o.RequiresInteractiveOnboarding() {
|
|
t.Fatal("OMP onboarding should not require an interactive terminal")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOMPArgs(t *testing.T) {
|
|
o := &OMP{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
model string
|
|
args []string
|
|
want []string
|
|
}{
|
|
{"with model", "gemma4", nil, []string{"--model", "ollama/gemma4"}},
|
|
{"with cloud model", "kimi-k2.6:cloud", nil, []string{"--model", "ollama/kimi-k2.6:cloud"}},
|
|
{"empty model", "", nil, nil},
|
|
{"with model and extra", "gemma4", []string{"--help"}, []string{"--model", "ollama/gemma4", "--help"}},
|
|
{"already qualified", "ollama/gemma4", nil, []string{"--model", "ollama/gemma4"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := o.args(tt.model, tt.args)
|
|
if !slices.Equal(got, tt.want) {
|
|
t.Errorf("args(%q, %v) = %v, want %v", tt.model, tt.args, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOMPRun_WebSearchPluginLifecycle(t *testing.T) {
|
|
seedOMPHelperBinary := func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
src, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dst := filepath.Join(dir, ompExecutableNames()[0])
|
|
if err := os.WriteFile(dst, data, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
setCloudStatus := func(t *testing.T, disabled bool) {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/status" {
|
|
fmt.Fprintf(w, `{"cloud":{"disabled":%t,"source":"config"}}`, disabled)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("OLLAMA_HOST", srv.URL)
|
|
}
|
|
|
|
setup := func(t *testing.T, pluginList string, cloudDisabled bool) (string, *OMP) {
|
|
t.Helper()
|
|
tmpDir := t.TempDir()
|
|
setOMPTestHome(t, tmpDir)
|
|
t.Setenv("PATH", tmpDir)
|
|
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_HELPER", "1")
|
|
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_PLUGIN_LIST", pluginList)
|
|
logPath := filepath.Join(tmpDir, "omp.log")
|
|
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_LOG", logPath)
|
|
setCloudStatus(t, cloudDisabled)
|
|
seedOMPHelperBinary(t, tmpDir)
|
|
return logPath, &OMP{}
|
|
}
|
|
|
|
t.Run("web search missing installs before launch", func(t *testing.T) {
|
|
logPath, o := setup(t, "No plugins installed\n", false)
|
|
|
|
if err := o.Run("kimi-k2.6:cloud", nil, []string{"session"}); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
|
|
calls, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(calls)
|
|
if !strings.Contains(got, "plugin list\n") {
|
|
t.Fatalf("expected plugin list call, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "plugin install "+ompWebSearchPlugin+"\n") {
|
|
t.Fatalf("expected plugin install call, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "--model ollama/kimi-k2.6:cloud session\n") {
|
|
t.Fatalf("expected final omp launch call, got:\n%s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("web search present refreshes before launch", func(t *testing.T) {
|
|
logPath, o := setup(t, "npm Plugins:\n\n● "+ompWebSearchPlugin+"@0.0.5\n", false)
|
|
|
|
if err := o.Run("gemma4", nil, []string{"chat"}); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
|
|
calls, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(calls)
|
|
if !strings.Contains(got, "plugin install "+ompWebSearchPlugin+"\n") {
|
|
t.Fatalf("expected plugin refresh install call, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "--model ollama/gemma4 chat\n") {
|
|
t.Fatalf("expected final omp launch call, got:\n%s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("web search install failure warns and continues", func(t *testing.T) {
|
|
logPath, o := setup(t, "No plugins installed\n", false)
|
|
t.Setenv("OLLAMA_LAUNCH_OMP_TEST_FAIL_INSTALL", "1")
|
|
|
|
stderr := captureStderr(t, func() {
|
|
if err := o.Run("gemma4", nil, []string{"chat"}); err != nil {
|
|
t.Fatalf("Run() should continue after plugin install failure, got %v", err)
|
|
}
|
|
})
|
|
if !strings.Contains(stderr, "Warning: could not install "+ompWebSearchPlugin) {
|
|
t.Fatalf("expected install warning, got:\n%s", stderr)
|
|
}
|
|
|
|
calls, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(calls), "--model ollama/gemma4 chat\n") {
|
|
t.Fatalf("expected final omp launch call, got:\n%s", calls)
|
|
}
|
|
})
|
|
|
|
t.Run("cloud disabled skips web search plugin management", func(t *testing.T) {
|
|
logPath, o := setup(t, "No plugins installed\n", true)
|
|
|
|
stderr := captureStderr(t, func() {
|
|
if err := o.Run("gemma4", nil, []string{"chat"}); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
})
|
|
if !strings.Contains(stderr, "Cloud is disabled; skipping "+ompWebSearchPlugin+" setup.") {
|
|
t.Fatalf("expected cloud-disabled skip message, got:\n%s", stderr)
|
|
}
|
|
|
|
calls, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(calls)
|
|
if strings.Contains(got, "plugin list\n") || strings.Contains(got, "plugin install "+ompWebSearchPlugin+"\n") {
|
|
t.Fatalf("did not expect plugin management calls, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "--model ollama/gemma4 chat\n") {
|
|
t.Fatalf("expected final omp launch call, got:\n%s", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOMPFindPath(t *testing.T) {
|
|
o := &OMP{}
|
|
|
|
t.Run("finds omp in PATH", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
name := "omp"
|
|
if runtime.GOOS == "windows" {
|
|
name = "omp.exe"
|
|
}
|
|
fakeBin := filepath.Join(tmpDir, name)
|
|
os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755)
|
|
t.Setenv("PATH", tmpDir)
|
|
|
|
got, err := o.findPath()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != fakeBin {
|
|
t.Errorf("findPath() = %q, want %q", got, fakeBin)
|
|
}
|
|
})
|
|
|
|
t.Run("falls back to ~/.local/bin/omp", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
fallback := filepath.Join(home, ".local", "bin", ompExecutableNames()[0])
|
|
os.MkdirAll(filepath.Dir(fallback), 0o755)
|
|
os.WriteFile(fallback, []byte("#!/bin/sh\n"), 0o755)
|
|
|
|
got, err := o.findPath()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != fallback {
|
|
t.Errorf("findPath() = %q, want %q", got, fallback)
|
|
}
|
|
})
|
|
|
|
t.Run("falls back to ~/.bun/bin/omp", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
fallback := filepath.Join(home, ".bun", "bin", ompExecutableNames()[0])
|
|
os.MkdirAll(filepath.Dir(fallback), 0o755)
|
|
os.WriteFile(fallback, []byte("#!/bin/sh\n"), 0o755)
|
|
|
|
got, err := o.findPath()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != fallback {
|
|
t.Errorf("findPath() = %q, want %q", got, fallback)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error when not found", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
if _, err := o.findPath(); err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOMPConfigureWithModelsWritesModelsYML(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
t.Setenv("OLLAMA_HOST", "http://0.0.0.0:11434")
|
|
|
|
o := &OMP{}
|
|
models := []LaunchModel{
|
|
{
|
|
Name: "glm-5.1:cloud",
|
|
ContextLength: 202_752,
|
|
MaxOutputTokens: 131_072,
|
|
},
|
|
{
|
|
Name: "qwen3.6",
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityVision},
|
|
},
|
|
}
|
|
if err := o.ConfigureWithModels("glm-5.1:cloud", models); err != nil {
|
|
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
|
}
|
|
|
|
path := filepath.Join(home, ".omp", "agent", "models.yml")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("failed to read models.yml: %v", err)
|
|
}
|
|
|
|
cfg := parseOMPConfigYAML(t, data)
|
|
provider := ompProviderFromYAML(t, cfg)
|
|
if provider["baseUrl"] != "http://127.0.0.1:11434/v1" {
|
|
t.Fatalf("baseUrl = %v, want connectable OpenAI-compatible host", provider["baseUrl"])
|
|
}
|
|
if provider["api"] != "openai-responses" {
|
|
t.Fatalf("api = %v, want openai-responses", provider["api"])
|
|
}
|
|
if provider["auth"] != "none" {
|
|
t.Fatalf("auth = %v, want none", provider["auth"])
|
|
}
|
|
discovery, _ := provider["discovery"].(map[string]any)
|
|
if discovery["type"] != "ollama" {
|
|
t.Fatalf("discovery = %v, want type ollama", discovery)
|
|
}
|
|
|
|
entries := ompModelEntriesFromYAML(t, provider)
|
|
if len(entries) != 2 {
|
|
t.Fatalf("models length = %d, want 2", len(entries))
|
|
}
|
|
if entries[0]["id"] != "glm-5.1:cloud" {
|
|
t.Fatalf("first model id = %v, want primary first", entries[0]["id"])
|
|
}
|
|
if got := numericYAMLValue(entries[0]["contextWindow"]); got != 202_752 {
|
|
t.Fatalf("contextWindow = %d, want 202752", got)
|
|
}
|
|
if got := numericYAMLValue(entries[0]["maxTokens"]); got != 131_072 {
|
|
t.Fatalf("maxTokens = %d, want 131072", got)
|
|
}
|
|
if input := stringSliceYAMLValue(entries[1]["input"]); !slices.Equal(input, []string{"text", "image"}) {
|
|
t.Fatalf("vision input = %v, want [text image]", input)
|
|
}
|
|
if got := o.CurrentModel(); got != "glm-5.1:cloud" {
|
|
t.Fatalf("CurrentModel = %q, want glm-5.1:cloud", got)
|
|
}
|
|
|
|
configPath := filepath.Join(home, ".omp", "agent", "config.yml")
|
|
configData, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read config.yml: %v", err)
|
|
}
|
|
config := parseOMPConfigYAML(t, configData)
|
|
if got := numericYAMLValue(config["setupVersion"]); got != ompSetupVersion {
|
|
t.Fatalf("setupVersion = %d, want %d", got, ompSetupVersion)
|
|
}
|
|
if paths := o.Paths(); !slices.Equal(paths, []string{path, configPath}) {
|
|
t.Fatalf("Paths = %v, want [%s %s]", paths, path, configPath)
|
|
}
|
|
}
|
|
|
|
func TestOMPConfigureWithModelsPreservesExistingConfig(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
|
|
modelsPath := filepath.Join(home, ".omp", "agent", "models.yml")
|
|
if err := os.MkdirAll(filepath.Dir(modelsPath), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
existing := []byte(`
|
|
providers:
|
|
anthropic:
|
|
baseUrl: https://example.com/anthropic
|
|
ollama:
|
|
baseUrl: http://old-host:11434
|
|
api: openai-responses
|
|
auth: none
|
|
models:
|
|
- id: old-model
|
|
name: Old Model
|
|
customField: keep-me
|
|
`)
|
|
if err := os.WriteFile(modelsPath, existing, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
configPath := filepath.Join(home, ".omp", "agent", "config.yml")
|
|
existingConfig := []byte(`
|
|
lastChangelogVersion: 15.7.6
|
|
setupVersion: 0
|
|
theme: monochrome
|
|
`)
|
|
if err := os.WriteFile(configPath, existingConfig, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
o := &OMP{}
|
|
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}, {Name: "old-model"}}); err != nil {
|
|
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(modelsPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := parseOMPConfigYAML(t, data)
|
|
providers, _ := cfg["providers"].(map[string]any)
|
|
if _, ok := providers["anthropic"]; !ok {
|
|
t.Fatalf("expected non-Ollama provider to be preserved: %v", providers)
|
|
}
|
|
|
|
provider := ompProviderFromYAML(t, cfg)
|
|
if provider["baseUrl"] != "http://127.0.0.1:11434/v1" {
|
|
t.Fatalf("baseUrl = %v, want repaired OpenAI-compatible host", provider["baseUrl"])
|
|
}
|
|
|
|
entries := ompModelEntriesFromYAML(t, provider)
|
|
if len(entries) != 2 {
|
|
t.Fatalf("models length = %d, want 2", len(entries))
|
|
}
|
|
if entries[0]["id"] != "new-model" {
|
|
t.Fatalf("first model id = %v, want new-model", entries[0]["id"])
|
|
}
|
|
if entries[1]["id"] != "old-model" {
|
|
t.Fatalf("second model id = %v, want old-model", entries[1]["id"])
|
|
}
|
|
if entries[1]["customField"] != "keep-me" {
|
|
t.Fatalf("custom field was not preserved: %v", entries[1])
|
|
}
|
|
|
|
configData, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config := parseOMPConfigYAML(t, configData)
|
|
if got := numericYAMLValue(config["setupVersion"]); got != ompSetupVersion {
|
|
t.Fatalf("setupVersion = %d, want %d", got, ompSetupVersion)
|
|
}
|
|
if config["theme"] != "monochrome" {
|
|
t.Fatalf("theme was not preserved: %v", config)
|
|
}
|
|
if config["lastChangelogVersion"] != "15.7.6" {
|
|
t.Fatalf("lastChangelogVersion was not preserved: %v", config)
|
|
}
|
|
}
|
|
|
|
func TestOMPConfigureWithModelsAlwaysMarksSetupComplete(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
|
|
configPath := filepath.Join(home, ".omp", "agent", "config.yml")
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(configPath, []byte("setupVersion: 2\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
o := &OMP{}
|
|
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}}); err != nil {
|
|
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
|
}
|
|
|
|
configData, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config := parseOMPConfigYAML(t, configData)
|
|
if got := numericYAMLValue(config["setupVersion"]); got != ompSetupVersion {
|
|
t.Fatalf("setupVersion = %d, want %d", got, ompSetupVersion)
|
|
}
|
|
}
|
|
|
|
func TestOMPConfigureWithModelsRespectsPiConfigDir(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
t.Setenv("PI_CONFIG_DIR", ".custom-omp")
|
|
|
|
o := &OMP{}
|
|
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}}); err != nil {
|
|
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
|
}
|
|
|
|
modelsPath := filepath.Join(home, ".custom-omp", "agent", "models.yml")
|
|
configPath := filepath.Join(home, ".custom-omp", "agent", "config.yml")
|
|
for _, path := range []string{modelsPath, configPath} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected %s to be written: %v", path, err)
|
|
}
|
|
}
|
|
if _, err := os.Stat(filepath.Join(home, ".omp", "agent", "models.yml")); !os.IsNotExist(err) {
|
|
t.Fatalf("expected default OMP models path to be untouched, got err %v", err)
|
|
}
|
|
if paths := o.Paths(); !slices.Equal(paths, []string{modelsPath, configPath}) {
|
|
t.Fatalf("Paths = %v, want [%s %s]", paths, modelsPath, configPath)
|
|
}
|
|
}
|
|
|
|
func TestOMPConfigureWithModelsRespectsPiCodingAgentDir(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
agentDir := filepath.Join(home, "agent-override")
|
|
t.Setenv("PI_CONFIG_DIR", ".ignored-omp")
|
|
t.Setenv("PI_CODING_AGENT_DIR", agentDir)
|
|
|
|
o := &OMP{}
|
|
if err := o.ConfigureWithModels("new-model", []LaunchModel{{Name: "new-model"}}); err != nil {
|
|
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
|
}
|
|
|
|
modelsPath := filepath.Join(agentDir, "models.yml")
|
|
configPath := filepath.Join(agentDir, "config.yml")
|
|
for _, path := range []string{modelsPath, configPath} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected %s to be written: %v", path, err)
|
|
}
|
|
}
|
|
if _, err := os.Stat(filepath.Join(home, ".ignored-omp", "agent", "models.yml")); !os.IsNotExist(err) {
|
|
t.Fatalf("expected PI_CONFIG_DIR path to be ignored when PI_CODING_AGENT_DIR is set, got err %v", err)
|
|
}
|
|
if got := o.CurrentModel(); got != "new-model" {
|
|
t.Fatalf("CurrentModel = %q, want new-model", got)
|
|
}
|
|
}
|
|
|
|
func TestOMPCurrentModelRequiresHealthyProvider(t *testing.T) {
|
|
home := t.TempDir()
|
|
setOMPTestHome(t, home)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
|
|
modelsPath := filepath.Join(home, ".omp", "agent", "models.yml")
|
|
if err := os.MkdirAll(filepath.Dir(modelsPath), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
provider string
|
|
}{
|
|
{
|
|
name: "wrong base url",
|
|
provider: "" +
|
|
" baseUrl: http://127.0.0.1:9999/v1\n" +
|
|
" api: openai-responses\n" +
|
|
" auth: none\n" +
|
|
" discovery:\n" +
|
|
" type: ollama\n",
|
|
},
|
|
{
|
|
name: "wrong api",
|
|
provider: "" +
|
|
" baseUrl: http://127.0.0.1:11434/v1\n" +
|
|
" api: openai-chat\n" +
|
|
" auth: none\n" +
|
|
" discovery:\n" +
|
|
" type: ollama\n",
|
|
},
|
|
{
|
|
name: "wrong auth",
|
|
provider: "" +
|
|
" baseUrl: http://127.0.0.1:11434/v1\n" +
|
|
" api: openai-responses\n" +
|
|
" auth: api-key\n" +
|
|
" discovery:\n" +
|
|
" type: ollama\n",
|
|
},
|
|
{
|
|
name: "wrong discovery",
|
|
provider: "" +
|
|
" baseUrl: http://127.0.0.1:11434/v1\n" +
|
|
" api: openai-responses\n" +
|
|
" auth: none\n" +
|
|
" discovery:\n" +
|
|
" type: static\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := "providers:\n" +
|
|
" ollama:\n" +
|
|
tt.provider +
|
|
" models:\n" +
|
|
" - id: gemma4\n"
|
|
if err := os.WriteFile(modelsPath, []byte(cfg), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := (&OMP{}).CurrentModel(); got != "" {
|
|
t.Fatalf("expected stale config to return empty current model, got %q", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func parseOMPConfigYAML(t *testing.T, data []byte) map[string]any {
|
|
t.Helper()
|
|
var cfg map[string]any
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatalf("generated YAML did not parse: %v\n%s", err, data)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func ompProviderFromYAML(t *testing.T, cfg map[string]any) map[string]any {
|
|
t.Helper()
|
|
providers, ok := cfg["providers"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("providers missing from config: %v", cfg)
|
|
}
|
|
provider, ok := providers["ollama"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("ollama provider missing from config: %v", providers)
|
|
}
|
|
return provider
|
|
}
|
|
|
|
func ompModelEntriesFromYAML(t *testing.T, provider map[string]any) []map[string]any {
|
|
t.Helper()
|
|
rawModels, ok := provider["models"].([]any)
|
|
if !ok {
|
|
t.Fatalf("provider models missing: %v", provider)
|
|
}
|
|
models := make([]map[string]any, 0, len(rawModels))
|
|
for _, raw := range rawModels {
|
|
entry, ok := raw.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("model entry has unexpected type %T: %v", raw, raw)
|
|
}
|
|
models = append(models, entry)
|
|
}
|
|
return models
|
|
}
|
|
|
|
func numericYAMLValue(value any) int {
|
|
switch v := value.(type) {
|
|
case int:
|
|
return v
|
|
case int64:
|
|
return int(v)
|
|
case float64:
|
|
return int(v)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func stringSliceYAMLValue(value any) []string {
|
|
raw, _ := value.([]any)
|
|
out := make([]string, 0, len(raw))
|
|
for _, item := range raw {
|
|
if s, ok := item.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|