247 lines
6.9 KiB
Go
247 lines
6.9 KiB
Go
package harness
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bytedance/sonic"
|
|
)
|
|
|
|
func TestClaudePreLaunchPinsSelectedModelAcrossClaudeTiers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env, cleanup, err := claudePreLaunch("https://example.com/anthropic", "test-key", "openai/gpt-5")
|
|
if err != nil {
|
|
t.Fatalf("claudePreLaunch() error = %v", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
for _, want := range []string{
|
|
"CLAUDE_CODE_SIMPLE=1",
|
|
"ANTHROPIC_DEFAULT_SONNET_MODEL=openai/gpt-5",
|
|
"ANTHROPIC_DEFAULT_OPUS_MODEL=openai/gpt-5",
|
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-5",
|
|
} {
|
|
parts := strings.SplitN(want, "=", 2)
|
|
if got := envValue(env, parts[0]); got != parts[1] {
|
|
t.Fatalf("env[%q] = %q, want %q", parts[0], got, parts[1])
|
|
}
|
|
}
|
|
|
|
if got := envValue(env, "ANTHROPIC_MODEL"); got != "" {
|
|
t.Fatalf("did not expect ANTHROPIC_MODEL in env, got %#v", env)
|
|
}
|
|
}
|
|
|
|
func TestClaudeWriteNativeConfigPinsTierDefaults(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
|
|
settingsDir := filepath.Join(home, ".claude")
|
|
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir settings dir: %v", err)
|
|
}
|
|
settingsPath := filepath.Join(settingsDir, "settings.json")
|
|
initial := `{"env":{"EXISTING":"keep","ANTHROPIC_MODEL":"stale-model"}}`
|
|
if err := os.WriteFile(settingsPath, []byte(initial), 0o600); err != nil {
|
|
t.Fatalf("write initial settings: %v", err)
|
|
}
|
|
|
|
if err := claudeWriteNativeConfig("https://example.com/anthropic", "test-key", "openai/gpt-5"); err != nil {
|
|
t.Fatalf("claudeWriteNativeConfig() error = %v", err)
|
|
}
|
|
|
|
b, err := os.ReadFile(settingsPath)
|
|
if err != nil {
|
|
t.Fatalf("read settings: %v", err)
|
|
}
|
|
|
|
var settings map[string]any
|
|
if err := sonic.Unmarshal(b, &settings); err != nil {
|
|
t.Fatalf("unmarshal settings: %v", err)
|
|
}
|
|
|
|
envRaw, ok := settings["env"]
|
|
if !ok {
|
|
t.Fatalf("expected env map in settings, got %#v", settings)
|
|
}
|
|
envMap, ok := envRaw.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("env map type = %T, want map[string]any", envRaw)
|
|
}
|
|
|
|
for key, want := range map[string]string{
|
|
"EXISTING": "keep",
|
|
"ANTHROPIC_BASE_URL": "https://example.com/anthropic",
|
|
"ANTHROPIC_API_KEY": "test-key",
|
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "openai/gpt-5",
|
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "openai/gpt-5",
|
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "openai/gpt-5",
|
|
} {
|
|
if got, _ := envMap[key].(string); got != want {
|
|
t.Fatalf("env[%q] = %q, want %q", key, got, want)
|
|
}
|
|
}
|
|
|
|
if _, ok := envMap["ANTHROPIC_MODEL"]; ok {
|
|
t.Fatalf("did not expect legacy ANTHROPIC_MODEL in settings env: %#v", envMap)
|
|
}
|
|
}
|
|
|
|
func TestOpencodeModelRef(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if got := opencodeModelRef("gpt-4.1"); got != "bifrost/gpt-4.1" {
|
|
t.Fatalf("opencodeModelRef() = %q, want %q", got, "bifrost/gpt-4.1")
|
|
}
|
|
}
|
|
|
|
func TestOpencodePreLaunchWritesCustomProviderConfig(t *testing.T) {
|
|
xdg := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
|
|
|
env, cleanup, err := opencodePreLaunch("https://example.com/openai", "test-key", "gpt-4.1")
|
|
if err != nil {
|
|
t.Fatalf("opencodePreLaunch() error = %v", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
if len(env) != 2 {
|
|
t.Fatalf("unexpected env returned: %#v", env)
|
|
}
|
|
|
|
configPath := envValue(env, "OPENCODE_CONFIG")
|
|
if configPath == "" {
|
|
t.Fatalf("expected OPENCODE_CONFIG in env, got %#v", env)
|
|
}
|
|
b, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("read generated config: %v", err)
|
|
}
|
|
|
|
cfg := string(b)
|
|
for _, want := range []string{
|
|
`"model": "bifrost/gpt-4.1"`,
|
|
`"bifrost": {`,
|
|
`"npm": "@ai-sdk/openai-compatible"`,
|
|
`"baseURL": "https://example.com/openai"`,
|
|
`"apiKey": "test-key"`,
|
|
`"gpt-4.1": {`,
|
|
} {
|
|
if !strings.Contains(cfg, want) {
|
|
t.Fatalf("expected generated config to contain %q, got %s", want, cfg)
|
|
}
|
|
}
|
|
|
|
tuiPath := envValue(env, "OPENCODE_TUI_CONFIG")
|
|
if tuiPath == "" {
|
|
t.Fatalf("expected OPENCODE_TUI_CONFIG in env, got %#v", env)
|
|
}
|
|
tuiCfg, err := os.ReadFile(tuiPath)
|
|
if err != nil {
|
|
t.Fatalf("read generated tui config: %v", err)
|
|
}
|
|
if !strings.Contains(string(tuiCfg), `"theme": "system"`) {
|
|
t.Fatalf("expected generated tui config to set system theme, got %s", string(tuiCfg))
|
|
}
|
|
}
|
|
|
|
func TestOpencodePreLaunchPreservesExistingTheme(t *testing.T) {
|
|
xdg := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
|
|
|
tuiPath := filepath.Join(xdg, "opencode", "tui.json")
|
|
if err := os.MkdirAll(filepath.Dir(tuiPath), 0o755); err != nil {
|
|
t.Fatalf("mkdir tui dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(tuiPath, []byte("{\n \"theme\": \"light\"\n}\n"), 0o600); err != nil {
|
|
t.Fatalf("write tui config: %v", err)
|
|
}
|
|
|
|
env, cleanup, err := opencodePreLaunch("https://example.com/openai", "test-key", "gpt-4.1")
|
|
if err != nil {
|
|
t.Fatalf("opencodePreLaunch() error = %v", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
if got := envValue(env, "OPENCODE_TUI_CONFIG"); got != "" {
|
|
t.Fatalf("did not expect OPENCODE_TUI_CONFIG override when user theme exists, got %#v", env)
|
|
}
|
|
if got := envValue(env, "OPENCODE_CONFIG"); got == "" {
|
|
t.Fatalf("expected OPENCODE_CONFIG to remain present, got %#v", env)
|
|
}
|
|
}
|
|
|
|
func TestOpencodePreLaunchAddsSystemThemeWithoutModel(t *testing.T) {
|
|
xdg := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
|
|
|
env, cleanup, err := opencodePreLaunch("https://example.com/openai", "test-key", "")
|
|
if err != nil {
|
|
t.Fatalf("opencodePreLaunch() error = %v", err)
|
|
}
|
|
|
|
tuiPath := envValue(env, "OPENCODE_TUI_CONFIG")
|
|
if tuiPath == "" {
|
|
t.Fatalf("expected OPENCODE_TUI_CONFIG in env, got %#v", env)
|
|
}
|
|
if got := envValue(env, "OPENCODE_CONFIG"); got != "" {
|
|
t.Fatalf("did not expect OPENCODE_CONFIG without a model, got %#v", env)
|
|
}
|
|
if _, err := os.Stat(tuiPath); err != nil {
|
|
t.Fatalf("expected generated tui config to exist: %v", err)
|
|
}
|
|
|
|
cleanup()
|
|
if _, err := os.Stat(tuiPath); !os.IsNotExist(err) {
|
|
t.Fatalf("expected generated tui config to be removed after cleanup, stat err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadOpencodeTUIConfigSupportsJSONC(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "tui.json")
|
|
content := "{\n // keep my choice\n \"theme\": \"light\",\n \"foo\": true,\n}\n"
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("write tui config: %v", err)
|
|
}
|
|
|
|
cfg, hasTheme, err := loadOpencodeTUIConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("loadOpencodeTUIConfig() error = %v", err)
|
|
}
|
|
if !hasTheme {
|
|
t.Fatal("expected theme to be detected")
|
|
}
|
|
if cfg["theme"] != "light" {
|
|
t.Fatalf("cfg[theme] = %#v, want %q", cfg["theme"], "light")
|
|
}
|
|
}
|
|
|
|
func TestOpencodeTUIConfigPathPrefersXDG(t *testing.T) {
|
|
xdg := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
|
|
|
got, err := opencodeTUIConfigPath()
|
|
if err != nil {
|
|
t.Fatalf("opencodeTUIConfigPath() error = %v", err)
|
|
}
|
|
want := filepath.Join(xdg, "opencode", "tui.json")
|
|
if got != want {
|
|
t.Fatalf("opencodeTUIConfigPath() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func envValue(env []string, key string) string {
|
|
prefix := key + "="
|
|
for _, entry := range env {
|
|
if strings.HasPrefix(entry, prefix) {
|
|
return strings.TrimPrefix(entry, prefix)
|
|
}
|
|
}
|
|
return ""
|
|
}
|