mirror of
https://github.com/ollama/ollama.git
synced 2026-03-27 02:58:43 +07:00
552 lines
15 KiB
Go
552 lines
15 KiB
Go
package launch
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
func TestVSCodeIntegration(t *testing.T) {
|
|
v := &VSCode{}
|
|
|
|
t.Run("String", func(t *testing.T) {
|
|
if got := v.String(); got != "Visual Studio Code" {
|
|
t.Errorf("String() = %q, want %q", got, "Visual Studio Code")
|
|
}
|
|
})
|
|
|
|
t.Run("implements Runner", func(t *testing.T) {
|
|
var _ Runner = v
|
|
})
|
|
|
|
t.Run("implements Editor", func(t *testing.T) {
|
|
var _ Editor = v
|
|
})
|
|
}
|
|
|
|
func TestVSCodeEdit(t *testing.T) {
|
|
v := &VSCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
clmPath := testVSCodePath(t, tmpDir, "chatLanguageModels.json")
|
|
|
|
tests := []struct {
|
|
name string
|
|
setup string // initial chatLanguageModels.json content, empty means no file
|
|
models []string
|
|
validate func(t *testing.T, data []byte)
|
|
}{
|
|
{
|
|
name: "fresh install",
|
|
models: []string{"llama3.2"},
|
|
validate: func(t *testing.T, data []byte) {
|
|
assertOllamaVendorConfigured(t, data)
|
|
},
|
|
},
|
|
{
|
|
name: "preserve other vendor entries",
|
|
setup: `[{"vendor": "azure", "name": "Azure", "url": "https://example.com"}]`,
|
|
models: []string{"llama3.2"},
|
|
validate: func(t *testing.T, data []byte) {
|
|
var entries []map[string]any
|
|
json.Unmarshal(data, &entries)
|
|
if len(entries) != 2 {
|
|
t.Errorf("expected 2 entries, got %d", len(entries))
|
|
}
|
|
// Check Azure entry preserved
|
|
found := false
|
|
for _, e := range entries {
|
|
if v, _ := e["vendor"].(string); v == "azure" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("azure vendor entry was not preserved")
|
|
}
|
|
assertOllamaVendorConfigured(t, data)
|
|
},
|
|
},
|
|
{
|
|
name: "update existing ollama entry",
|
|
setup: `[{"vendor": "ollama", "name": "Ollama", "url": "http://old:11434"}]`,
|
|
models: []string{"llama3.2"},
|
|
validate: func(t *testing.T, data []byte) {
|
|
assertOllamaVendorConfigured(t, data)
|
|
},
|
|
},
|
|
{
|
|
name: "empty models is no-op",
|
|
setup: `[{"vendor": "azure", "name": "Azure"}]`,
|
|
models: []string{},
|
|
validate: func(t *testing.T, data []byte) {
|
|
if string(data) != `[{"vendor": "azure", "name": "Azure"}]` {
|
|
t.Error("empty models should not modify file")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "corrupted JSON treated as empty",
|
|
setup: `{corrupted json`,
|
|
models: []string{"llama3.2"},
|
|
validate: func(t *testing.T, data []byte) {
|
|
var entries []map[string]any
|
|
if err := json.Unmarshal(data, &entries); err != nil {
|
|
t.Errorf("result is not valid JSON: %v", err)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
os.RemoveAll(filepath.Dir(clmPath))
|
|
|
|
if tt.setup != "" {
|
|
os.MkdirAll(filepath.Dir(clmPath), 0o755)
|
|
os.WriteFile(clmPath, []byte(tt.setup), 0o644)
|
|
}
|
|
|
|
if err := v.Edit(tt.models); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ := os.ReadFile(clmPath)
|
|
tt.validate(t, data)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVSCodeEditCleansUpOldSettings(t *testing.T) {
|
|
v := &VSCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
settingsPath := testVSCodePath(t, tmpDir, "settings.json")
|
|
|
|
// Create settings.json with old byok setting
|
|
os.MkdirAll(filepath.Dir(settingsPath), 0o755)
|
|
os.WriteFile(settingsPath, []byte(`{"github.copilot.chat.byok.ollamaEndpoint": "http://old:11434", "ollama.launch.configured": true, "editor.fontSize": 14}`), 0o644)
|
|
|
|
if err := v.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify old settings were removed
|
|
data, err := os.ReadFile(settingsPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var settings map[string]any
|
|
json.Unmarshal(data, &settings)
|
|
if _, ok := settings["github.copilot.chat.byok.ollamaEndpoint"]; ok {
|
|
t.Error("github.copilot.chat.byok.ollamaEndpoint should have been removed")
|
|
}
|
|
if _, ok := settings["ollama.launch.configured"]; ok {
|
|
t.Error("ollama.launch.configured should have been removed")
|
|
}
|
|
if settings["editor.fontSize"] != float64(14) {
|
|
t.Error("editor.fontSize should have been preserved")
|
|
}
|
|
}
|
|
|
|
func TestVSCodePaths(t *testing.T) {
|
|
v := &VSCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
clmPath := testVSCodePath(t, tmpDir, "chatLanguageModels.json")
|
|
|
|
t.Run("no file returns nil", func(t *testing.T) {
|
|
os.Remove(clmPath)
|
|
if paths := v.Paths(); paths != nil {
|
|
t.Errorf("expected nil, got %v", paths)
|
|
}
|
|
})
|
|
|
|
t.Run("existing file returns path", func(t *testing.T) {
|
|
os.MkdirAll(filepath.Dir(clmPath), 0o755)
|
|
os.WriteFile(clmPath, []byte(`[]`), 0o644)
|
|
|
|
if paths := v.Paths(); len(paths) != 1 {
|
|
t.Errorf("expected 1 path, got %d", len(paths))
|
|
}
|
|
})
|
|
}
|
|
|
|
// testVSCodePath returns the expected VS Code config path for the given file in tests.
|
|
func testVSCodePath(t *testing.T, tmpDir, filename string) string {
|
|
t.Helper()
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return filepath.Join(tmpDir, "Library", "Application Support", "Code", "User", filename)
|
|
case "windows":
|
|
t.Setenv("APPDATA", tmpDir)
|
|
return filepath.Join(tmpDir, "Code", "User", filename)
|
|
default:
|
|
return filepath.Join(tmpDir, ".config", "Code", "User", filename)
|
|
}
|
|
}
|
|
|
|
func assertOllamaVendorConfigured(t *testing.T, data []byte) {
|
|
t.Helper()
|
|
var entries []map[string]any
|
|
if err := json.Unmarshal(data, &entries); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if vendor, _ := entry["vendor"].(string); vendor == "ollama" {
|
|
if name, _ := entry["name"].(string); name != "Ollama" {
|
|
t.Errorf("expected name \"Ollama\", got %q", name)
|
|
}
|
|
if url, _ := entry["url"].(string); url == "" {
|
|
t.Error("url not set")
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("no ollama vendor entry found")
|
|
}
|
|
|
|
func TestShowInModelPicker(t *testing.T) {
|
|
v := &VSCode{}
|
|
|
|
// helper to create a state DB with optional seed data
|
|
setupDB := func(t *testing.T, tmpDir string, seedPrefs map[string]bool, seedCache []map[string]any) string {
|
|
t.Helper()
|
|
dbDir := filepath.Join(tmpDir, "globalStorage")
|
|
os.MkdirAll(dbDir, 0o755)
|
|
dbPath := filepath.Join(dbDir, "state.vscdb")
|
|
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if _, err := db.Exec("CREATE TABLE ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seedPrefs != nil {
|
|
data, _ := json.Marshal(seedPrefs)
|
|
db.Exec("INSERT INTO ItemTable (key, value) VALUES ('chatModelPickerPreferences', ?)", string(data))
|
|
}
|
|
if seedCache != nil {
|
|
data, _ := json.Marshal(seedCache)
|
|
db.Exec("INSERT INTO ItemTable (key, value) VALUES ('chat.cachedLanguageModels.v2', ?)", string(data))
|
|
}
|
|
return dbPath
|
|
}
|
|
|
|
// helper to read prefs back from DB
|
|
readPrefs := func(t *testing.T, dbPath string) map[string]bool {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
var raw string
|
|
if err := db.QueryRow("SELECT value FROM ItemTable WHERE key = 'chatModelPickerPreferences'").Scan(&raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prefs := make(map[string]bool)
|
|
json.Unmarshal([]byte(raw), &prefs)
|
|
return prefs
|
|
}
|
|
|
|
t.Run("fresh DB creates table and shows models", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
if runtime.GOOS == "windows" {
|
|
t.Setenv("APPDATA", tmpDir)
|
|
}
|
|
|
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dbPath := testVSCodePath(t, tmpDir, filepath.Join("globalStorage", "state.vscdb"))
|
|
prefs := readPrefs(t, dbPath)
|
|
if !prefs["ollama/Ollama/llama3.2"] {
|
|
t.Error("expected llama3.2 to be shown")
|
|
}
|
|
if !prefs["ollama/Ollama/llama3.2:latest"] {
|
|
t.Error("expected llama3.2:latest to be shown")
|
|
}
|
|
})
|
|
|
|
t.Run("configured models are shown", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), nil, nil)
|
|
|
|
err := v.ShowInModelPicker([]string{"llama3.2", "qwen3:8b"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prefs := readPrefs(t, dbPath)
|
|
if !prefs["ollama/Ollama/llama3.2"] {
|
|
t.Error("expected llama3.2 to be shown")
|
|
}
|
|
if !prefs["ollama/Ollama/qwen3:8b"] {
|
|
t.Error("expected qwen3:8b to be shown")
|
|
}
|
|
})
|
|
|
|
t.Run("removed models are hidden", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), map[string]bool{
|
|
"ollama/Ollama/llama3.2": true,
|
|
"ollama/Ollama/llama3.2:latest": true,
|
|
"ollama/Ollama/mistral": true,
|
|
"ollama/Ollama/mistral:latest": true,
|
|
}, nil)
|
|
|
|
// Only configure llama3.2 — mistral should get hidden
|
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prefs := readPrefs(t, dbPath)
|
|
if !prefs["ollama/Ollama/llama3.2"] {
|
|
t.Error("expected llama3.2 to stay shown")
|
|
}
|
|
if prefs["ollama/Ollama/mistral"] {
|
|
t.Error("expected mistral to be hidden")
|
|
}
|
|
if prefs["ollama/Ollama/mistral:latest"] {
|
|
t.Error("expected mistral:latest to be hidden")
|
|
}
|
|
})
|
|
|
|
t.Run("non-ollama prefs are preserved", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), map[string]bool{
|
|
"copilot/gpt-4o": true,
|
|
}, nil)
|
|
|
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prefs := readPrefs(t, dbPath)
|
|
if !prefs["copilot/gpt-4o"] {
|
|
t.Error("expected copilot/gpt-4o to stay shown")
|
|
}
|
|
})
|
|
|
|
t.Run("uses cached numeric IDs when available", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
cache := []map[string]any{
|
|
{
|
|
"identifier": "ollama/Ollama/4",
|
|
"metadata": map[string]any{"vendor": "ollama", "name": "llama3.2"},
|
|
},
|
|
}
|
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), nil, cache)
|
|
|
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prefs := readPrefs(t, dbPath)
|
|
if !prefs["ollama/Ollama/4"] {
|
|
t.Error("expected numeric ID ollama/Ollama/4 to be shown")
|
|
}
|
|
// Name-based fallback should also be set
|
|
if !prefs["ollama/Ollama/llama3.2"] {
|
|
t.Error("expected name-based ID to also be shown")
|
|
}
|
|
})
|
|
|
|
t.Run("empty models is no-op", func(t *testing.T) {
|
|
err := v.ShowInModelPicker([]string{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
// helper to read a string value from the state DB
|
|
readValue := func(t *testing.T, dbPath, key string) string {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
var val string
|
|
if err := db.QueryRow("SELECT value FROM ItemTable WHERE key = ?", key).Scan(&val); err != nil {
|
|
return ""
|
|
}
|
|
return val
|
|
}
|
|
|
|
t.Run("sets primary model as active selection", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
setupDB(t, testVSCodePath(t, tmpDir, ""), nil, nil)
|
|
|
|
err := v.ShowInModelPicker([]string{"llama3.2", "qwen3:8b"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dbPath := testVSCodePath(t, tmpDir, filepath.Join("globalStorage", "state.vscdb"))
|
|
panelModel := readValue(t, dbPath, "chat.currentLanguageModel.panel")
|
|
if panelModel != "ollama/Ollama/llama3.2" {
|
|
t.Errorf("expected panel model ollama/Ollama/llama3.2, got %q", panelModel)
|
|
}
|
|
editorModel := readValue(t, dbPath, "chat.currentLanguageModel.editor")
|
|
if editorModel != "ollama/Ollama/llama3.2" {
|
|
t.Errorf("expected editor model ollama/Ollama/llama3.2, got %q", editorModel)
|
|
}
|
|
panelDefault := readValue(t, dbPath, "chat.currentLanguageModel.panel.isDefault")
|
|
if panelDefault != "false" {
|
|
t.Errorf("expected panel isDefault false, got %q", panelDefault)
|
|
}
|
|
})
|
|
|
|
t.Run("sets cached numeric ID as active selection", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
cache := []map[string]any{
|
|
{
|
|
"identifier": "ollama/Ollama/4",
|
|
"metadata": map[string]any{"vendor": "ollama", "name": "llama3.2"},
|
|
},
|
|
}
|
|
setupDB(t, testVSCodePath(t, tmpDir, ""), nil, cache)
|
|
|
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dbPath := testVSCodePath(t, tmpDir, filepath.Join("globalStorage", "state.vscdb"))
|
|
panelModel := readValue(t, dbPath, "chat.currentLanguageModel.panel")
|
|
if panelModel != "ollama/Ollama/4" {
|
|
t.Errorf("expected panel model to use cached numeric ID ollama/Ollama/4, got %q", panelModel)
|
|
}
|
|
})
|
|
|
|
t.Run("previously hidden model is re-shown when configured", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("XDG_CONFIG_HOME", "")
|
|
dbPath := setupDB(t, testVSCodePath(t, tmpDir, ""), map[string]bool{
|
|
"ollama/Ollama/llama3.2": false,
|
|
"ollama/Ollama/llama3.2:latest": false,
|
|
}, nil)
|
|
|
|
// Ollama config is authoritative — should override the hidden state
|
|
err := v.ShowInModelPicker([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prefs := readPrefs(t, dbPath)
|
|
if !prefs["ollama/Ollama/llama3.2"] {
|
|
t.Error("expected llama3.2 to be re-shown")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseCopilotChatVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
output string
|
|
wantInstalled bool
|
|
wantVersion string
|
|
}{
|
|
{
|
|
name: "found among other extensions",
|
|
output: "ms-python.python@2024.1.1\ngithub.copilot-chat@0.40.1\ngithub.copilot@1.200.0\n",
|
|
wantInstalled: true,
|
|
wantVersion: "0.40.1",
|
|
},
|
|
{
|
|
name: "only extension",
|
|
output: "GitHub.copilot-chat@0.41.0\n",
|
|
wantInstalled: true,
|
|
wantVersion: "0.41.0",
|
|
},
|
|
{
|
|
name: "not installed",
|
|
output: "ms-python.python@2024.1.1\ngithub.copilot@1.200.0\n",
|
|
wantInstalled: false,
|
|
},
|
|
{
|
|
name: "empty output",
|
|
output: "",
|
|
wantInstalled: false,
|
|
},
|
|
{
|
|
name: "case insensitive match",
|
|
output: "GitHub.Copilot-Chat@0.39.0\n",
|
|
wantInstalled: true,
|
|
wantVersion: "0.39.0",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
installed, version := parseCopilotChatVersion(tt.output)
|
|
if installed != tt.wantInstalled {
|
|
t.Errorf("installed = %v, want %v", installed, tt.wantInstalled)
|
|
}
|
|
if installed && version != tt.wantVersion {
|
|
t.Errorf("version = %q, want %q", version, tt.wantVersion)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareVersions(t *testing.T) {
|
|
tests := []struct {
|
|
a, b string
|
|
want int
|
|
}{
|
|
{"0.40.1", "0.40.1", 0},
|
|
{"0.40.2", "0.40.1", 1},
|
|
{"0.40.0", "0.40.1", -1},
|
|
{"0.41.0", "0.40.1", 1},
|
|
{"0.39.9", "0.40.1", -1},
|
|
{"1.0.0", "0.40.1", 1},
|
|
{"0.40", "0.40.1", -1},
|
|
{"0.40.1.1", "0.40.1", 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.a+"_vs_"+tt.b, func(t *testing.T) {
|
|
got := compareVersions(tt.a, tt.b)
|
|
if got != tt.want {
|
|
t.Errorf("compareVersions(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|