mirror of
https://github.com/ollama/ollama.git
synced 2026-11-10 15:02:13 -06:00
1348 lines
41 KiB
Go
1348 lines
41 KiB
Go
package launch
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/pelletier/go-toml/v2"
|
|
|
|
modelpkg "github.com/ollama/ollama/types/model"
|
|
)
|
|
|
|
func writeFakeKimiCode(t *testing.T, dir, logPath string) {
|
|
t.Helper()
|
|
script := fmt.Sprintf(`#!/bin/sh
|
|
if [ "$1" = "--version" ]; then
|
|
echo "2.3.3"
|
|
exit 0
|
|
fi
|
|
for arg in "$@"; do
|
|
printf "%%s\n" "$arg" >> %q
|
|
done
|
|
exit 0
|
|
`, logPath)
|
|
if err := os.WriteFile(filepath.Join(dir, "kimi"), []byte(script), 0o755); err != nil {
|
|
t.Fatalf("failed to write fake kimi: %v", err)
|
|
}
|
|
}
|
|
|
|
func readKimiCodeConfig(t *testing.T, path string) map[string]any {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("failed to read kimi config: %v", err)
|
|
}
|
|
cfg := map[string]any{}
|
|
if err := toml.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatalf("kimi config is not valid TOML: %v\n%s", err, data)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func kimiCodeConfigModel(t *testing.T, cfg map[string]any, alias string) map[string]any {
|
|
t.Helper()
|
|
models, _ := cfg["models"].(map[string]any)
|
|
entry, ok := models[alias].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("models[%q] missing from config: %v", alias, models)
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func stringList(v any) []string {
|
|
items, _ := v.([]any)
|
|
out := make([]string, 0, len(items))
|
|
for _, item := range items {
|
|
s, _ := item.(string)
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestKimiRun_KimiCodeWritesConfigAndSelectsModel(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX shell fake binary")
|
|
}
|
|
|
|
homeDir := t.TempDir()
|
|
setTestHome(t, homeDir)
|
|
binDir := t.TempDir()
|
|
logPath := filepath.Join(binDir, "kimi-args.log")
|
|
writeFakeKimiCode(t, binDir, logPath)
|
|
t.Setenv("PATH", binDir)
|
|
t.Setenv("KIMI_CODE_HOME", "")
|
|
|
|
srv := httptest.NewServer(http.NotFoundHandler())
|
|
defer srv.Close()
|
|
t.Setenv("OLLAMA_HOST", srv.URL)
|
|
|
|
configPath := filepath.Join(homeDir, ".kimi-code", "config.toml")
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
existing := `default_model = "kimi-code/k3"
|
|
telemetry = false
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1"
|
|
|
|
[models."kimi-code/k3"]
|
|
provider = "managed:kimi-code"
|
|
model = "k3"
|
|
max_context_size = 1048576
|
|
|
|
[models."ollama/stale:cloud"]
|
|
provider = "ollama"
|
|
model = "stale:cloud"
|
|
max_context_size = 4096
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
models := []LaunchModel{
|
|
{
|
|
Name: "kimi-k3:cloud",
|
|
ContextLength: 262144,
|
|
ToolCapable: true,
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityCompletion, modelpkg.CapabilityTools, modelpkg.CapabilityVision, modelpkg.CapabilityThinking},
|
|
Thinking: &modelpkg.Thinking{Values: []any{false, "low", "high", "max"}, Default: "max"},
|
|
},
|
|
{
|
|
Name: "qwen3-coder:30b",
|
|
ContextLength: 32768,
|
|
ToolCapable: true,
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityCompletion, modelpkg.CapabilityTools},
|
|
},
|
|
}
|
|
|
|
var gotPrompt string
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
gotPrompt = prompt
|
|
return true, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
k := &Kimi{}
|
|
if err := k.Run("kimi-k3:cloud", models, []string{"-p", "hello"}); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
if !strings.Contains(gotPrompt, configPath) {
|
|
t.Fatalf("prompt = %q, want existing config confirmation for %s", gotPrompt, configPath)
|
|
}
|
|
|
|
data, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read args log: %v", err)
|
|
}
|
|
gotArgs := strings.Split(strings.TrimSpace(string(data)), "\n")
|
|
wantArgs := []string{"-m", "ollama/kimi-k3:cloud", "-p", "hello"}
|
|
if !slices.Equal(gotArgs, wantArgs) {
|
|
t.Fatalf("args = %v, want %v", gotArgs, wantArgs)
|
|
}
|
|
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
if cfg["default_model"] != "kimi-code/k3" || cfg["telemetry"] != false {
|
|
t.Fatalf("user settings not preserved: %v", cfg)
|
|
}
|
|
|
|
providers := cfg["providers"].(map[string]any)
|
|
if _, ok := providers["managed:kimi-code"]; !ok {
|
|
t.Fatalf("user provider removed: %v", providers)
|
|
}
|
|
ollama := providers["ollama"].(map[string]any)
|
|
if ollama["type"] != "openai" || ollama["base_url"] != srv.URL+"/v1" || ollama["api_key"] != "ollama" {
|
|
t.Fatalf("ollama provider = %v", ollama)
|
|
}
|
|
|
|
allModels := cfg["models"].(map[string]any)
|
|
if _, ok := allModels["kimi-code/k3"]; !ok {
|
|
t.Fatalf("user model removed: %v", allModels)
|
|
}
|
|
if _, ok := allModels["ollama/stale:cloud"]; !ok {
|
|
t.Fatalf("previously written ollama model was removed: %v", allModels)
|
|
}
|
|
|
|
kimi := kimiCodeConfigModel(t, cfg, "ollama/kimi-k3:cloud")
|
|
if kimi["model"] != "kimi-k3:cloud" || kimi["provider"] != "ollama" || kimi["max_context_size"] != int64(262144) {
|
|
t.Fatalf("kimi entry = %v", kimi)
|
|
}
|
|
if got, want := stringList(kimi["capabilities"]), []string{"tool_use", "image_in", "thinking"}; !slices.Equal(got, want) {
|
|
t.Fatalf("capabilities = %v, want %v", got, want)
|
|
}
|
|
if got, want := stringList(kimi["support_efforts"]), []string{"low", "high", "max"}; !slices.Equal(got, want) {
|
|
t.Fatalf("support_efforts = %v, want %v", got, want)
|
|
}
|
|
if kimi["default_effort"] != "max" || kimi["off_effort"] != "none" {
|
|
t.Fatalf("efforts = default %v off %v", kimi["default_effort"], kimi["off_effort"])
|
|
}
|
|
|
|
qwen := kimiCodeConfigModel(t, cfg, "ollama/qwen3-coder:30b")
|
|
if got, want := stringList(qwen["capabilities"]), []string{"tool_use"}; !slices.Equal(got, want) {
|
|
t.Fatalf("capabilities = %v, want %v", got, want)
|
|
}
|
|
if len(qwen) != 4 {
|
|
t.Fatalf("non-thinking model has unexpected keys: %v", qwen)
|
|
}
|
|
}
|
|
|
|
func TestKimiCodeModelEntry_ThinkingContracts(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
model LaunchModel
|
|
wantCaps []string
|
|
wantEfforts []string
|
|
wantDefault any
|
|
wantOffEffort any
|
|
}{
|
|
{
|
|
name: "always thinking",
|
|
model: LaunchModel{
|
|
Name: "minimax-m3:cloud",
|
|
ContextLength: 200000,
|
|
ToolCapable: true,
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityTools, modelpkg.CapabilityThinking},
|
|
Thinking: &modelpkg.Thinking{Values: []any{true}, Default: true},
|
|
},
|
|
wantCaps: []string{"tool_use", "thinking", "always_thinking"},
|
|
wantEfforts: []string{"high"},
|
|
wantDefault: "high",
|
|
},
|
|
{
|
|
name: "on off thinking",
|
|
model: LaunchModel{
|
|
Name: "qwen3.5:cloud",
|
|
ContextLength: 262144,
|
|
ToolCapable: true,
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityTools, modelpkg.CapabilityThinking},
|
|
Thinking: &modelpkg.Thinking{Values: []any{false, true}, Default: true},
|
|
},
|
|
wantCaps: []string{"tool_use", "thinking"},
|
|
wantEfforts: []string{"high"},
|
|
wantDefault: "high",
|
|
wantOffEffort: "none",
|
|
},
|
|
{
|
|
name: "thinking without metadata",
|
|
model: LaunchModel{
|
|
Name: "local-thinker",
|
|
ContextLength: 8192,
|
|
ToolCapable: true,
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityTools, modelpkg.CapabilityThinking},
|
|
},
|
|
wantCaps: []string{"tool_use", "thinking"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
entry := kimiCodeModelEntry(tt.model)
|
|
if got := stringList(entry["capabilities"]); !slices.Equal(got, tt.wantCaps) {
|
|
t.Fatalf("capabilities = %v, want %v", got, tt.wantCaps)
|
|
}
|
|
if got := stringList(entry["support_efforts"]); !slices.Equal(got, tt.wantEfforts) {
|
|
t.Fatalf("support_efforts = %v, want %v", got, tt.wantEfforts)
|
|
}
|
|
if entry["default_effort"] != tt.wantDefault {
|
|
t.Fatalf("default_effort = %v, want %v", entry["default_effort"], tt.wantDefault)
|
|
}
|
|
if entry["off_effort"] != tt.wantOffEffort {
|
|
t.Fatalf("off_effort = %v, want %v", entry["off_effort"], tt.wantOffEffort)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_UsesKimiCodeHome(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", []LaunchModel{{Name: "glm-5.3:cloud", ContextLength: 200000}}); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
cfg := readKimiCodeConfig(t, filepath.Join(kimiHome, "config.toml"))
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
if cfg["default_model"] != "ollama/glm-5.3:cloud" {
|
|
t.Fatalf("default_model = %v, want ollama/glm-5.3:cloud", cfg["default_model"])
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_DefaultModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
existing string
|
|
want string
|
|
}{
|
|
{
|
|
name: "replaces previous ollama default",
|
|
existing: `default_model = "ollama/old:cloud"
|
|
|
|
[models."ollama/old:cloud"]
|
|
provider = "ollama"
|
|
model = "old:cloud"
|
|
`,
|
|
want: "ollama/glm-5.3:cloud",
|
|
},
|
|
{
|
|
name: "replaces dangling default",
|
|
existing: `default_model = "gone"
|
|
`,
|
|
want: "ollama/glm-5.3:cloud",
|
|
},
|
|
{
|
|
name: "keeps user default",
|
|
existing: `default_model = "kimi-code/k3"
|
|
|
|
[models."kimi-code/k3"]
|
|
provider = "managed:kimi-code"
|
|
model = "k3"
|
|
`,
|
|
want: "kimi-code/k3",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
if err := os.WriteFile(configPath, []byte(tt.existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
return true, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", []LaunchModel{{Name: "glm-5.3:cloud", ContextLength: 200000}}); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
if cfg["default_model"] != tt.want {
|
|
t.Fatalf("default_model = %v, want %v", cfg["default_model"], tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKimiCodeModelEntry_FillsThinkingFromShow(t *testing.T) {
|
|
old := lookupServerModelMetadata
|
|
lookupServerModelMetadata = func(name string) (serverModelMetadata, bool) {
|
|
if name != "beam:cloud" {
|
|
t.Fatalf("unexpected show lookup for %q", name)
|
|
}
|
|
return serverModelMetadata{Thinking: &modelpkg.Thinking{Values: []any{"low", "medium", "high", "xhigh", "max"}, Default: "max"}}, true
|
|
}
|
|
t.Cleanup(func() { lookupServerModelMetadata = old })
|
|
|
|
entry := kimiCodeModelEntry(LaunchModel{
|
|
Name: "beam:cloud",
|
|
ContextLength: 1048576,
|
|
ToolCapable: true,
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityTools, modelpkg.CapabilityThinking},
|
|
})
|
|
if got, want := stringList(entry["support_efforts"]), []string{"low", "medium", "high", "xhigh", "max"}; !slices.Equal(got, want) {
|
|
t.Fatalf("support_efforts = %v, want %v", got, want)
|
|
}
|
|
if entry["default_effort"] != "max" {
|
|
t.Fatalf("default_effort = %v, want max", entry["default_effort"])
|
|
}
|
|
if got, want := stringList(entry["capabilities"]), []string{"tool_use", "thinking", "always_thinking"}; !slices.Equal(got, want) {
|
|
t.Fatalf("capabilities = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_AddsPrimaryWhenMissingFromModels(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
cfg := readKimiCodeConfig(t, filepath.Join(kimiHome, "config.toml"))
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_RejectsInvalidExistingConfig(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
if err := os.WriteFile(configPath, []byte("not = [valid"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err == nil {
|
|
t.Fatal("expected parse error")
|
|
}
|
|
data, _ := os.ReadFile(configPath)
|
|
if string(data) != "not = [valid" {
|
|
t.Fatalf("invalid config was overwritten: %q", data)
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_DeclinedUpdateLeavesConfigUntouched(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `default_model = "kimi-code/k3"
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
err := writeKimiCodeConfig("glm-5.3:cloud", []LaunchModel{{Name: "glm-5.3:cloud", ContextLength: 200000}})
|
|
if err == nil {
|
|
t.Fatal("expected error when update is declined")
|
|
}
|
|
if !strings.Contains(err.Error(), "--yes") {
|
|
t.Fatalf("error = %v, want --yes hint", err)
|
|
}
|
|
data, readErr := os.ReadFile(configPath)
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if string(data) != existing {
|
|
t.Fatalf("config changed after decline: %q", data)
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PromptsBeforeUpdatingUserSettings(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
existing string
|
|
wantPrompt bool
|
|
}{
|
|
{
|
|
name: "fresh config",
|
|
},
|
|
{
|
|
name: "ollama entries only",
|
|
existing: `default_model = "ollama/old:cloud"
|
|
|
|
[providers.ollama]
|
|
type = "openai"
|
|
base_url = "http://gpu-box:11434/v1"
|
|
api_key = "ollama"
|
|
|
|
[models."ollama/old:cloud"]
|
|
provider = "ollama"
|
|
model = "old:cloud"
|
|
`,
|
|
},
|
|
{
|
|
name: "user provider",
|
|
existing: `[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
`,
|
|
wantPrompt: true,
|
|
},
|
|
{
|
|
name: "user model",
|
|
existing: `[models."kimi-code/k3"]
|
|
provider = "managed:kimi-code"
|
|
model = "k3"
|
|
`,
|
|
wantPrompt: true,
|
|
},
|
|
{
|
|
name: "user setting",
|
|
existing: "telemetry = false\n",
|
|
wantPrompt: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
if tt.existing != "" {
|
|
if err := os.WriteFile(configPath, []byte(tt.existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
var gotPrompt string
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
gotPrompt = prompt
|
|
return true, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
if (gotPrompt != "") != tt.wantPrompt {
|
|
t.Fatalf("prompt = %q, wantPrompt %v", gotPrompt, tt.wantPrompt)
|
|
}
|
|
if tt.wantPrompt && !strings.Contains(gotPrompt, configPath) {
|
|
t.Fatalf("prompt = %q, want confirmation for %s", gotPrompt, configPath)
|
|
}
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
if providers := cfg["providers"].(map[string]any); providers["ollama"].(map[string]any)["base_url"] != "http://127.0.0.1:11434/v1" {
|
|
t.Fatalf("base_url = %v, want local host", providers["ollama"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_SkipsUnchangedRewrite(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prompts := 0
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
prompts++
|
|
return true, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
models := []LaunchModel{{Name: "glm-5.3:cloud", ContextLength: 200000}, {Name: "qwen3-coder:30b", ContextLength: 32768}}
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", models); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
if prompts != 1 {
|
|
t.Fatalf("prompts after first write = %d, want 1", prompts)
|
|
}
|
|
first, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", models); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
if prompts != 1 {
|
|
t.Fatalf("prompts after identical write = %d, want 1", prompts)
|
|
}
|
|
second, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(second) != string(first) {
|
|
t.Fatal("config was rewritten even though nothing changed")
|
|
}
|
|
kimiCodeConfigModel(t, readKimiCodeConfig(t, configPath), "ollama/qwen3-coder:30b")
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_UsesResolvedPrimaryMetadata(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
|
|
srv := httptest.NewServer(http.NotFoundHandler())
|
|
defer srv.Close()
|
|
t.Setenv("OLLAMA_HOST", srv.URL)
|
|
|
|
models := []LaunchModel{{
|
|
Name: "llama3.2:latest",
|
|
ContextLength: 131072,
|
|
ToolCapable: true,
|
|
Capabilities: []modelpkg.Capability{modelpkg.CapabilityTools},
|
|
}}
|
|
if err := writeKimiCodeConfig("llama3.2", models); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
if cfg["default_model"] != "ollama/llama3.2" {
|
|
t.Fatalf("default_model = %v, want ollama/llama3.2", cfg["default_model"])
|
|
}
|
|
entry := kimiCodeConfigModel(t, cfg, "ollama/llama3.2")
|
|
if entry["max_context_size"] != int64(131072) {
|
|
t.Fatalf("max_context_size = %v, want 131072", entry["max_context_size"])
|
|
}
|
|
if got := stringList(entry["capabilities"]); !slices.Equal(got, []string{"tool_use"}) {
|
|
t.Fatalf("capabilities = %v, want [tool_use]", got)
|
|
}
|
|
entries := cfg["models"].(map[string]any)
|
|
if len(entries) != 1 {
|
|
t.Fatalf("models = %v, want only ollama/llama3.2", entries)
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PreservesUnrelatedContent(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `# hand-tuned, keep this
|
|
telemetry = false
|
|
|
|
[providers."managed:kimi-code"]
|
|
type = "kimi"
|
|
base_url = "https://api.kimi.com/coding/v1"
|
|
|
|
[models."kimi-code/k3"]
|
|
provider = "managed:kimi-code"
|
|
model = "k3"
|
|
max_context_size = 1048576
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
return true, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
raw, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"# hand-tuned, keep this",
|
|
"telemetry = false",
|
|
`[providers."managed:kimi-code"]`,
|
|
`type = "kimi"`,
|
|
`[models."kimi-code/k3"]`,
|
|
} {
|
|
if !strings.Contains(string(raw), want) {
|
|
t.Fatalf("config lost %q:\n%s", want, raw)
|
|
}
|
|
}
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
if cfg["default_model"] != "ollama/glm-5.3:cloud" {
|
|
t.Fatalf("default_model = %v, want ollama/glm-5.3:cloud", cfg["default_model"])
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PreservesMultilineHook(t *testing.T) {
|
|
const command = `cat <<'CONFIG'
|
|
providers.ollama.api_key = "literal hook text"
|
|
[providers.ollama]
|
|
type = "openai"
|
|
[models."ollama/local".overrides]
|
|
max_context_size = 8192
|
|
CONFIG
|
|
`
|
|
for _, delimiter := range []string{"'''", `"""`} {
|
|
t.Run(delimiter, func(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
path := filepath.Join(kimiHome, "config.toml")
|
|
existing := fmt.Sprintf("[[hooks]]\nevent = 'SessionStart'\ncommand = %s\n%s%s\n", delimiter, command, delimiter)
|
|
if err := os.WriteFile(path, []byte(existing), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(string, ConfirmOptions) (bool, error) { return true, nil }
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
models := []LaunchModel{{Name: "local", ContextLength: 32768}}
|
|
if err := writeKimiCodeConfig("local", models); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := readKimiCodeConfig(t, path)
|
|
hooks := cfg["hooks"].([]any)
|
|
if got := hooks[0].(map[string]any)["command"]; got != command {
|
|
t.Fatalf("hook command = %q, want %q", got, command)
|
|
}
|
|
first, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeKimiCodeConfig("local", models); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(first) != string(second) {
|
|
t.Fatal("unchanged configuration was rewritten")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKimiLegacyCLIArchived(t *testing.T) {
|
|
tests := map[string]bool{
|
|
"1.50.0": false,
|
|
"1.51.0": false,
|
|
"1.52.0": true,
|
|
"1.53.1": true,
|
|
"2.0.0": true,
|
|
"bogus": false,
|
|
}
|
|
for version, want := range tests {
|
|
if got := kimiLegacyCLIArchived(version); got != want {
|
|
t.Errorf("kimiLegacyCLIArchived(%q) = %v, want %v", version, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnsureKimiInstalled_RejectsArchivedCLIWhenNotInteractive(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX shell fake binaries")
|
|
}
|
|
oldGOOS := kimiGOOS
|
|
kimiGOOS = "linux"
|
|
t.Cleanup(func() { kimiGOOS = oldGOOS })
|
|
|
|
setTestHome(t, t.TempDir())
|
|
binDir := t.TempDir()
|
|
t.Setenv("PATH", binDir)
|
|
writeFakeBinary(t, binDir, "curl")
|
|
writeFakeBinary(t, binDir, "bash")
|
|
legacyPath := filepath.Join(binDir, "kimi")
|
|
if err := os.WriteFile(legacyPath, []byte("#!/bin/sh\necho 'kimi, version 1.52.0'\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldInteractive := isInteractiveSession
|
|
isInteractiveSession = func() bool { return false }
|
|
t.Cleanup(func() { isInteractiveSession = oldInteractive })
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
t.Fatalf("unexpected prompt: %q", prompt)
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
_, err := ensureKimiInstalled()
|
|
if err == nil || !strings.Contains(err.Error(), "no longer runs commands") {
|
|
t.Fatalf("ensureKimiInstalled() error = %v, want archived kimi-cli error", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "https://code.kimi.com/kimi-code/install.sh") {
|
|
t.Fatalf("error = %v, want install command", err)
|
|
}
|
|
}
|
|
|
|
func TestFindKimiBinary_PrefersKimiCodeInstall(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX executable names")
|
|
}
|
|
oldGOOS := kimiGOOS
|
|
kimiGOOS = runtime.GOOS
|
|
t.Cleanup(func() { kimiGOOS = oldGOOS })
|
|
|
|
homeDir := t.TempDir()
|
|
setTestHome(t, homeDir)
|
|
pathDir := t.TempDir()
|
|
writeFakeBinary(t, pathDir, "kimi")
|
|
t.Setenv("PATH", pathDir)
|
|
|
|
kimiCodeBin := filepath.Join(homeDir, ".kimi-code", "bin", "kimi")
|
|
if err := os.MkdirAll(filepath.Dir(kimiCodeBin), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(kimiCodeBin, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := findKimiBinary()
|
|
if err != nil {
|
|
t.Fatalf("findKimiBinary() error = %v", err)
|
|
}
|
|
if got != kimiCodeBin {
|
|
t.Fatalf("findKimiBinary() = %q, want %q", got, kimiCodeBin)
|
|
}
|
|
}
|
|
|
|
func TestEnsureKimiInstalled_FallsBackToLegacyCLIOnDecline(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX shell fake binaries")
|
|
}
|
|
oldGOOS := kimiGOOS
|
|
kimiGOOS = "linux"
|
|
t.Cleanup(func() { kimiGOOS = oldGOOS })
|
|
|
|
setTestHome(t, t.TempDir())
|
|
binDir := t.TempDir()
|
|
t.Setenv("PATH", binDir)
|
|
writeFakeBinary(t, binDir, "curl")
|
|
writeFakeBinary(t, binDir, "bash")
|
|
legacyPath := filepath.Join(binDir, "kimi")
|
|
if err := os.WriteFile(legacyPath, []byte("#!/bin/sh\necho 'kimi, version 1.50.0'\necho 'kimi-cli is no longer maintained.'\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldInteractive := isInteractiveSession
|
|
isInteractiveSession = func() bool { return true }
|
|
t.Cleanup(func() { isInteractiveSession = oldInteractive })
|
|
|
|
var gotPrompt string
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
gotPrompt = prompt
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
got, err := ensureKimiInstalled()
|
|
if err != nil {
|
|
t.Fatalf("ensureKimiInstalled() error = %v", err)
|
|
}
|
|
if got != legacyPath {
|
|
t.Fatalf("ensureKimiInstalled() = %q, want legacy %q", got, legacyPath)
|
|
}
|
|
if !strings.Contains(gotPrompt, "kimi-cli 1.50.0 is no longer maintained") {
|
|
t.Fatalf("prompt = %q, want archived kimi-cli prompt", gotPrompt)
|
|
}
|
|
}
|
|
|
|
func TestKimiRun_RejectsArchivedLegacyCLI(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX shell fake binary")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
logPath := filepath.Join(tmpDir, "kimi-args.log")
|
|
script := fmt.Sprintf(`#!/bin/sh
|
|
if [ "$1" = "--version" ]; then
|
|
echo "kimi, version 1.53.1"
|
|
exit 0
|
|
fi
|
|
for arg in "$@"; do
|
|
printf "%%s\n" "$arg" >> %q
|
|
done
|
|
exit 0
|
|
`, logPath)
|
|
if err := os.WriteFile(filepath.Join(tmpDir, "kimi"), []byte(script), 0o755); err != nil {
|
|
t.Fatalf("failed to write fake kimi: %v", err)
|
|
}
|
|
writeFakeBinary(t, tmpDir, "curl")
|
|
writeFakeBinary(t, tmpDir, "bash")
|
|
t.Setenv("PATH", tmpDir)
|
|
t.Setenv("KIMI_CODE_HOME", "")
|
|
|
|
oldInteractive := isInteractiveSession
|
|
isInteractiveSession = func() bool { return true }
|
|
t.Cleanup(func() { isInteractiveSession = oldInteractive })
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
err := (&Kimi{}).Run("llama3.2", nil, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "no longer runs commands") {
|
|
t.Fatalf("Run() error = %v, want archived kimi-cli error", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "https://code.kimi.com/kimi-code/install.sh") {
|
|
t.Fatalf("error = %v, want install command", err)
|
|
}
|
|
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
|
|
t.Fatalf("archived kimi-cli should not be launched")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(tmpDir, ".kimi-code", "config.toml")); !os.IsNotExist(err) {
|
|
t.Fatalf("kimi code config should not be written for an archived kimi-cli")
|
|
}
|
|
}
|
|
|
|
func TestEnsureKimiInstalled_KeepsLegacyCLIWhenNotInteractive(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX shell fake binaries")
|
|
}
|
|
oldGOOS := kimiGOOS
|
|
kimiGOOS = "linux"
|
|
t.Cleanup(func() { kimiGOOS = oldGOOS })
|
|
|
|
setTestHome(t, t.TempDir())
|
|
binDir := t.TempDir()
|
|
t.Setenv("PATH", binDir)
|
|
writeFakeBinary(t, binDir, "curl")
|
|
writeFakeBinary(t, binDir, "bash")
|
|
legacyPath := filepath.Join(binDir, "kimi")
|
|
if err := os.WriteFile(legacyPath, []byte("#!/bin/sh\necho 'kimi, version 1.50.0'\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldInteractive := isInteractiveSession
|
|
isInteractiveSession = func() bool { return false }
|
|
t.Cleanup(func() { isInteractiveSession = oldInteractive })
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
t.Fatalf("unexpected prompt: %q", prompt)
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
got, err := ensureKimiInstalled()
|
|
if err != nil {
|
|
t.Fatalf("ensureKimiInstalled() error = %v", err)
|
|
}
|
|
if got != legacyPath {
|
|
t.Fatalf("ensureKimiInstalled() = %q, want legacy %q", got, legacyPath)
|
|
}
|
|
}
|
|
|
|
func TestEnsureKimiInstalled_FallsBackWhenInstallerDepsMissing(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX shell fake binaries")
|
|
}
|
|
oldGOOS := kimiGOOS
|
|
kimiGOOS = "linux"
|
|
t.Cleanup(func() { kimiGOOS = oldGOOS })
|
|
|
|
setTestHome(t, t.TempDir())
|
|
binDir := t.TempDir()
|
|
t.Setenv("PATH", binDir)
|
|
legacyPath := filepath.Join(binDir, "kimi")
|
|
if err := os.WriteFile(legacyPath, []byte("#!/bin/sh\necho 'kimi, version 1.50.0'\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldInteractive := isInteractiveSession
|
|
isInteractiveSession = func() bool { return true }
|
|
t.Cleanup(func() { isInteractiveSession = oldInteractive })
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
t.Fatalf("unexpected prompt: %q", prompt)
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
got, err := ensureKimiInstalled()
|
|
if err != nil {
|
|
t.Fatalf("ensureKimiInstalled() error = %v", err)
|
|
}
|
|
if got != legacyPath {
|
|
t.Fatalf("ensureKimiInstalled() = %q, want legacy %q", got, legacyPath)
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PreservesCustomOllamaAlias(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `default_model = "local-fast"
|
|
|
|
[models."local-fast"]
|
|
provider = "ollama"
|
|
model = "local-fast"
|
|
max_context_size = 8192
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
return true, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
if cfg["default_model"] != "local-fast" {
|
|
t.Fatalf("default_model = %v, want user's local-fast", cfg["default_model"])
|
|
}
|
|
entry := kimiCodeConfigModel(t, cfg, "local-fast")
|
|
if entry["max_context_size"] != int64(8192) {
|
|
t.Fatalf("local-fast = %v, want preserved entry", entry)
|
|
}
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PreservesModelOverrides(t *testing.T) {
|
|
tests := map[string]string{
|
|
"inline": `[models."ollama/glm-5.3:cloud"]
|
|
provider = "ollama"
|
|
model = "glm-5.3:cloud"
|
|
max_context_size = 262144
|
|
overrides = { max_context_size = 8192 }
|
|
`,
|
|
"separate table": `[models."ollama/glm-5.3:cloud"]
|
|
provider = "ollama"
|
|
model = "glm-5.3:cloud"
|
|
max_context_size = 262144
|
|
|
|
[models."ollama/glm-5.3:cloud".overrides]
|
|
max_context_size = 8192
|
|
`,
|
|
}
|
|
|
|
for name, existing := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
t.Fatalf("unexpected prompt: %q", prompt)
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
models := []LaunchModel{{Name: "glm-5.3:cloud", ContextLength: 262144}}
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", models); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
entry := kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
overrides, ok := entry["overrides"].(map[string]any)
|
|
if !ok || overrides["max_context_size"] != int64(8192) {
|
|
t.Fatalf("overrides = %v, want max_context_size 8192", entry["overrides"])
|
|
}
|
|
|
|
first, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", models); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
second, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(second) != string(first) {
|
|
t.Fatalf("config was rewritten on an unchanged second pass")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_HandlesDottedProviderKeys(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `providers.ollama.type = "openai"
|
|
providers.ollama.base_url = "http://gpu-box:11434/v1"
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
t.Fatalf("unexpected prompt: %q", prompt)
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
raw, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), "providers.ollama.") {
|
|
t.Fatalf("dotted provider keys survived:\n%s", raw)
|
|
}
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
providers := cfg["providers"].(map[string]any)
|
|
ollama := providers["ollama"].(map[string]any)
|
|
if ollama["base_url"] != "http://127.0.0.1:11434/v1" {
|
|
t.Fatalf("base_url = %v, want local host", ollama["base_url"])
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_HandlesDottedKeysInsideTables(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `[providers]
|
|
ollama.type = "openai"
|
|
ollama.base_url = "http://gpu-box:11434/v1"
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
providers := cfg["providers"].(map[string]any)
|
|
ollama := providers["ollama"].(map[string]any)
|
|
if ollama["base_url"] != "http://127.0.0.1:11434/v1" {
|
|
t.Fatalf("base_url = %v, want local host", ollama["base_url"])
|
|
}
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_HandlesInlineProviderTable(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `providers = { ollama = { type = "openai", base_url = "http://gpu-box:11434/v1", api_key = "ollama" } }
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
providers := cfg["providers"].(map[string]any)
|
|
ollama := providers["ollama"].(map[string]any)
|
|
if ollama["base_url"] != "http://127.0.0.1:11434/v1" {
|
|
t.Fatalf("base_url = %v, want local host", ollama["base_url"])
|
|
}
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PreservesProviderAndModelFields(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
configPath := filepath.Join(kimiHome, "config.toml")
|
|
existing := `[providers.ollama]
|
|
type = "openai"
|
|
base_url = "http://gpu-box:11434/v1"
|
|
api_key = "ollama"
|
|
custom_headers = { X-Proxy = "corp" }
|
|
|
|
[models."ollama/glm-5.3:cloud"]
|
|
provider = "ollama"
|
|
model = "glm-5.3:cloud"
|
|
max_context_size = 262144
|
|
max_output_size = 8192
|
|
display_name = "GLM (work)"
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
models := []LaunchModel{{Name: "glm-5.3:cloud", ContextLength: 262144}}
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", models); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
|
|
cfg := readKimiCodeConfig(t, configPath)
|
|
providers := cfg["providers"].(map[string]any)
|
|
ollama := providers["ollama"].(map[string]any)
|
|
headers, ok := ollama["custom_headers"].(map[string]any)
|
|
if !ok || headers["X-Proxy"] != "corp" {
|
|
t.Fatalf("custom_headers = %v, want preserved", ollama["custom_headers"])
|
|
}
|
|
entry := kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
if entry["max_output_size"] != int64(8192) || entry["display_name"] != "GLM (work)" {
|
|
t.Fatalf("model fields = %v, want max_output_size and display_name preserved", entry)
|
|
}
|
|
if ollama["base_url"] != "http://127.0.0.1:11434/v1" {
|
|
t.Fatalf("base_url = %v, want local host", ollama["base_url"])
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PreservesProviderAuthentication(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
config string
|
|
key string
|
|
want any
|
|
}{
|
|
{"inline key", `api_key = "custom-key"`, "api_key", "custom-key"},
|
|
{"environment key", `api_key_env = "OLLAMA_API_KEY"`, "api_key_env", "OLLAMA_API_KEY"},
|
|
{"OAuth", `oauth = { storage = "keyring", key = "ollama-auth" }`, "oauth", map[string]any{"storage": "keyring", "key": "ollama-auth"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
path := filepath.Join(kimiHome, "config.toml")
|
|
if err := os.WriteFile(path, []byte("[providers.ollama]\ntype = 'openai'\n"+tc.config+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeKimiCodeConfig("local", []LaunchModel{{Name: "local", ContextLength: 32768}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := readKimiCodeConfig(t, path)
|
|
provider := cfg["providers"].(map[string]any)["ollama"].(map[string]any)
|
|
if diff := cmp.Diff(tc.want, provider[tc.key]); diff != "" {
|
|
t.Fatalf("authentication changed (-want +got):\n%s", diff)
|
|
}
|
|
for _, key := range []string{"api_key", "api_key_env", "oauth"} {
|
|
if key != tc.key {
|
|
if _, ok := provider[key]; ok {
|
|
t.Fatalf("conflicting credential %s was added", key)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindKimiBinary_SkipsNonExecutableInstall(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX executable bits")
|
|
}
|
|
oldGOOS := kimiGOOS
|
|
kimiGOOS = runtime.GOOS
|
|
t.Cleanup(func() { kimiGOOS = oldGOOS })
|
|
|
|
homeDir := t.TempDir()
|
|
setTestHome(t, homeDir)
|
|
pathDir := t.TempDir()
|
|
writeFakeBinary(t, pathDir, "kimi")
|
|
t.Setenv("PATH", pathDir)
|
|
|
|
kimiCodeBin := filepath.Join(homeDir, ".kimi-code", "bin", "kimi")
|
|
if err := os.MkdirAll(filepath.Dir(kimiCodeBin), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(kimiCodeBin, []byte("#!/bin/sh\nexit 0\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := findKimiBinary()
|
|
if err != nil {
|
|
t.Fatalf("findKimiBinary() error = %v", err)
|
|
}
|
|
if got != filepath.Join(pathDir, "kimi") {
|
|
t.Fatalf("findKimiBinary() = %q, want the executable on PATH", got)
|
|
}
|
|
}
|
|
|
|
func TestWriteKimiCodeConfig_PrivateConfigDir(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("does not check POSIX permissions")
|
|
}
|
|
homeDir := t.TempDir()
|
|
setTestHome(t, homeDir)
|
|
t.Setenv("KIMI_CODE_HOME", "")
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
|
|
if err := writeKimiCodeConfig("glm-5.3:cloud", nil); err != nil {
|
|
t.Fatalf("writeKimiCodeConfig() error = %v", err)
|
|
}
|
|
info, err := os.Stat(filepath.Join(homeDir, ".kimi-code"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if perm := info.Mode().Perm(); perm != 0o700 {
|
|
t.Fatalf("~/.kimi-code mode = %o, want 700", perm)
|
|
}
|
|
}
|
|
|
|
func TestKimiConfigureOnlyWritesConfig(t *testing.T) {
|
|
setTestHome(t, t.TempDir())
|
|
kimiHome := t.TempDir()
|
|
t.Setenv("KIMI_CODE_HOME", kimiHome)
|
|
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
return false, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
models := []LaunchModel{{Name: "glm-5.3:cloud", ContextLength: 200000}}
|
|
if err := launchAfterConfiguration("kimi", &Kimi{}, "glm-5.3:cloud", models, IntegrationLaunchRequest{ConfigureOnly: true}); err != nil {
|
|
t.Fatalf("launchAfterConfiguration() error = %v", err)
|
|
}
|
|
cfg := readKimiCodeConfig(t, filepath.Join(kimiHome, "config.toml"))
|
|
kimiCodeConfigModel(t, cfg, "ollama/glm-5.3:cloud")
|
|
}
|
|
|
|
func TestEnsureKimiInstalled_RejectsStillArchivedAfterInstall(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses POSIX shell fake binaries")
|
|
}
|
|
oldGOOS := kimiGOOS
|
|
kimiGOOS = "linux"
|
|
t.Cleanup(func() { kimiGOOS = oldGOOS })
|
|
|
|
setTestHome(t, t.TempDir())
|
|
binDir := t.TempDir()
|
|
t.Setenv("PATH", binDir)
|
|
writeFakeBinary(t, binDir, "curl")
|
|
writeFakeBinary(t, binDir, "bash")
|
|
legacyPath := filepath.Join(binDir, "kimi")
|
|
if err := os.WriteFile(legacyPath, []byte("#!/bin/sh\necho 'kimi, version 1.52.0'\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldInteractive := isInteractiveSession
|
|
isInteractiveSession = func() bool { return true }
|
|
t.Cleanup(func() { isInteractiveSession = oldInteractive })
|
|
|
|
oldConfirm := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
return true, nil
|
|
}
|
|
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
|
|
|
_, err := ensureKimiInstalled()
|
|
if err == nil || !strings.Contains(err.Error(), "no longer runs commands") {
|
|
t.Fatalf("ensureKimiInstalled() error = %v, want archived kimi-cli error after install", err)
|
|
}
|
|
}
|