Files
ollama/cmd/launch/kimi_code_test.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)
}
}