mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-13 08:25:10 +08:00
864 lines
33 KiB
Go
864 lines
33 KiB
Go
package aiservice
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"GoNavi-Wails/shared/i18n"
|
|
)
|
|
|
|
func TestResolveLocalMCPCommandUsesMainBinaryWithArgument(t *testing.T) {
|
|
command, args, err := resolveLocalMCPCommand(`C:\Program Files\GoNavi\GoNavi.exe`)
|
|
if err != nil {
|
|
t.Fatalf("resolveLocalMCPCommand returned error: %v", err)
|
|
}
|
|
if command != `C:\Program Files\GoNavi\GoNavi.exe` {
|
|
t.Fatalf("expected command to keep main binary path, got %q", command)
|
|
}
|
|
if !reflect.DeepEqual(args, []string{"mcp-server"}) {
|
|
t.Fatalf("expected main binary args %#v, got %#v", []string{"mcp-server"}, args)
|
|
}
|
|
}
|
|
|
|
func TestResolveLocalMCPCommandKeepsDedicatedServerBinary(t *testing.T) {
|
|
command, args, err := resolveLocalMCPCommand(`D:\Work\CodeRepos\GoNavi\bin\gonavi-mcp-server.exe`)
|
|
if err != nil {
|
|
t.Fatalf("resolveLocalMCPCommand returned error: %v", err)
|
|
}
|
|
if command != `D:\Work\CodeRepos\GoNavi\bin\gonavi-mcp-server.exe` {
|
|
t.Fatalf("expected dedicated server path to be reused, got %q", command)
|
|
}
|
|
if len(args) != 0 {
|
|
t.Fatalf("expected dedicated server args to be empty, got %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestReadClaudeCodeMCPServerConfigReadsExistingInstall(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, ".claude.json")
|
|
initial := map[string]any{
|
|
"mcpServers": map[string]any{
|
|
gonaviMCPServerID: map[string]any{
|
|
"type": "stdio",
|
|
"command": `C:\Program Files\GoNavi\GoNavi.exe`,
|
|
"args": []string{"mcp-server"},
|
|
},
|
|
},
|
|
}
|
|
data, err := json.MarshalIndent(initial, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("MarshalIndent returned error: %v", err)
|
|
}
|
|
if err := os.WriteFile(configPath, append(data, '\n'), 0o644); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
cfg, found, err := readClaudeCodeMCPServerConfig(configPath, gonaviMCPServerID)
|
|
if err != nil {
|
|
t.Fatalf("readClaudeCodeMCPServerConfig returned error: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected gonavi install to be detected")
|
|
}
|
|
if cfg.Command != `C:\Program Files\GoNavi\GoNavi.exe` {
|
|
t.Fatalf("unexpected command: %q", cfg.Command)
|
|
}
|
|
if !reflect.DeepEqual(cfg.Args, []string{"mcp-server"}) {
|
|
t.Fatalf("unexpected args: %#v", cfg.Args)
|
|
}
|
|
}
|
|
|
|
func TestUpsertClaudeCodeMCPServerConfigCreatesAndMergesUserConfig(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, ".claude.json")
|
|
initial := map[string]any{
|
|
"theme": "dark-daltonized",
|
|
"mcpServers": map[string]any{
|
|
"memory": map[string]any{
|
|
"type": "stdio",
|
|
"command": "cmd",
|
|
},
|
|
},
|
|
}
|
|
data, err := json.MarshalIndent(initial, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("MarshalIndent returned error: %v", err)
|
|
}
|
|
if err := os.WriteFile(configPath, append(data, '\n'), 0o644); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
err = upsertClaudeCodeMCPServerConfig(configPath, gonaviMCPServerID, claudeCodeMCPServerConfig{
|
|
Type: "stdio",
|
|
Command: `C:\Program Files\GoNavi\GoNavi.exe`,
|
|
Args: []string{"mcp-server"},
|
|
Env: map[string]string{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upsertClaudeCodeMCPServerConfig returned error: %v", err)
|
|
}
|
|
|
|
updated, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile returned error: %v", err)
|
|
}
|
|
|
|
var root map[string]any
|
|
if err := json.Unmarshal(updated, &root); err != nil {
|
|
t.Fatalf("Unmarshal returned error: %v", err)
|
|
}
|
|
if got := strings.TrimSpace(root["theme"].(string)); got != "dark-daltonized" {
|
|
t.Fatalf("expected theme to be preserved, got %q", got)
|
|
}
|
|
|
|
mcpServers, ok := root["mcpServers"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected mcpServers object, got %#v", root["mcpServers"])
|
|
}
|
|
if _, ok := mcpServers["memory"]; !ok {
|
|
t.Fatalf("expected existing memory server to be preserved, got %#v", mcpServers)
|
|
}
|
|
|
|
gonavi, ok := mcpServers[gonaviMCPServerID].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected gonavi server object, got %#v", mcpServers[gonaviMCPServerID])
|
|
}
|
|
if got := strings.TrimSpace(gonavi["command"].(string)); got != `C:\Program Files\GoNavi\GoNavi.exe` {
|
|
t.Fatalf("expected gonavi command to be written, got %q", got)
|
|
}
|
|
args, ok := gonavi["args"].([]any)
|
|
if !ok || len(args) != 1 || strings.TrimSpace(args[0].(string)) != "mcp-server" {
|
|
t.Fatalf("expected gonavi args to contain mcp-server, got %#v", gonavi["args"])
|
|
}
|
|
}
|
|
|
|
func TestUpsertClaudeCodeMCPServerConfigRejectsInvalidMCPServersShape(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, ".claude.json")
|
|
if err := os.WriteFile(configPath, []byte("{\"mcpServers\":[]}"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
err := upsertClaudeCodeMCPServerConfig(configPath, gonaviMCPServerID, claudeCodeMCPServerConfig{
|
|
Type: "stdio",
|
|
Command: "GoNavi.exe",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected invalid mcpServers shape to return error")
|
|
}
|
|
if !strings.Contains(err.Error(), "mcpServers should be an object") {
|
|
t.Fatalf("expected invalid shape error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseCodexMCPServerConfigDetectsExistingInstall(t *testing.T) {
|
|
content := strings.Join([]string{
|
|
`model = "gpt-5.4"`,
|
|
``,
|
|
`[mcp_servers.gonavi]`,
|
|
`command = 'C:\Program Files\GoNavi\GoNavi.exe'`,
|
|
`args = ['mcp-server']`,
|
|
`startup_timeout_sec = 60`,
|
|
``,
|
|
`[projects.'D:\Work\CodeRepos\GoNavi']`,
|
|
`trust_level = "trusted"`,
|
|
``,
|
|
}, "\n")
|
|
|
|
cfg, found, err := parseCodexMCPServerConfig(content, gonaviMCPServerID)
|
|
if err != nil {
|
|
t.Fatalf("parseCodexMCPServerConfig returned error: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected gonavi install to be detected")
|
|
}
|
|
if cfg.Command != `C:\Program Files\GoNavi\GoNavi.exe` {
|
|
t.Fatalf("unexpected command: %q", cfg.Command)
|
|
}
|
|
if !reflect.DeepEqual(cfg.Args, []string{"mcp-server"}) {
|
|
t.Fatalf("unexpected args: %#v", cfg.Args)
|
|
}
|
|
if cfg.StartupTimeoutSec != 60 {
|
|
t.Fatalf("unexpected startup timeout: %d", cfg.StartupTimeoutSec)
|
|
}
|
|
}
|
|
|
|
func TestCodexMCPConfigParserInternalErrorsUseEnglishText(t *testing.T) {
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
cases := []struct {
|
|
name string
|
|
err error
|
|
want string
|
|
}{
|
|
{
|
|
name: "short string",
|
|
err: func() error { _, err := parseTOMLString("x"); return err }(),
|
|
want: "invalid string format",
|
|
},
|
|
{
|
|
name: "unclosed single quoted string",
|
|
err: func() error { _, err := parseTOMLString("'x"); return err }(),
|
|
want: "single-quoted string is not closed",
|
|
},
|
|
{
|
|
name: "non string",
|
|
err: func() error { _, err := parseTOMLString("[bad]"); return err }(),
|
|
want: "not a string",
|
|
},
|
|
{
|
|
name: "non array",
|
|
err: func() error { _, err := parseTOMLStringArray("'x'"); return err }(),
|
|
want: "not an array",
|
|
},
|
|
{
|
|
name: "invalid array separator",
|
|
err: func() error { _, err := parseTOMLStringArray("['a' 'b']"); return err }(),
|
|
want: "invalid array separator",
|
|
},
|
|
{
|
|
name: "empty array item",
|
|
err: func() error { _, _, err := consumeTOMLQuotedString(""); return err }(),
|
|
want: "string is empty",
|
|
},
|
|
{
|
|
name: "unclosed double quoted string",
|
|
err: func() error { _, _, err := consumeTOMLQuotedString(`"x`); return err }(),
|
|
want: "double-quoted string is not closed",
|
|
},
|
|
{
|
|
name: "non string array element",
|
|
err: func() error { _, err := decodeJSONLikeStringSlice([]any{"ok", 1}); return err }(),
|
|
want: "array element is not a string",
|
|
},
|
|
{
|
|
name: "non string array",
|
|
err: func() error { _, err := decodeJSONLikeStringSlice("x"); return err }(),
|
|
want: "not a string array",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.err == nil {
|
|
t.Fatal("expected parser helper to return an error")
|
|
}
|
|
if hanText.MatchString(tc.err.Error()) {
|
|
t.Fatalf("expected English internal parser error, got %q", tc.err.Error())
|
|
}
|
|
if tc.err.Error() != tc.want {
|
|
t.Fatalf("expected parser helper error %q, got %q", tc.want, tc.err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpsertCodexMCPServerConfigCreatesAndMergesConfig(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.toml")
|
|
initial := strings.Join([]string{
|
|
`model = "gpt-5.4"`,
|
|
``,
|
|
`[mcp_servers.memory]`,
|
|
`command = "cmd"`,
|
|
`args = ["/c", "npx"]`,
|
|
``,
|
|
}, "\n")
|
|
if err := os.WriteFile(configPath, []byte(initial), 0o644); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
err := upsertCodexMCPServerConfig(configPath, gonaviMCPServerID, codexMCPServerConfig{
|
|
Command: `C:\Program Files\GoNavi\GoNavi.exe`,
|
|
Args: []string{"mcp-server"},
|
|
StartupTimeoutSec: defaultCodexMCPStartupTimeoutSecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upsertCodexMCPServerConfig returned error: %v", err)
|
|
}
|
|
|
|
updated, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile returned error: %v", err)
|
|
}
|
|
text := string(updated)
|
|
if !strings.Contains(text, `[mcp_servers.memory]`) {
|
|
t.Fatalf("expected memory server to be preserved, got %s", text)
|
|
}
|
|
if !strings.Contains(text, `[mcp_servers.gonavi]`) {
|
|
t.Fatalf("expected gonavi section to be created, got %s", text)
|
|
}
|
|
if !strings.Contains(text, `command = 'C:\Program Files\GoNavi\GoNavi.exe'`) {
|
|
t.Fatalf("expected gonavi command to be written, got %s", text)
|
|
}
|
|
if !strings.Contains(text, `args = ['mcp-server']`) {
|
|
t.Fatalf("expected gonavi args to be written, got %s", text)
|
|
}
|
|
if !strings.Contains(text, `startup_timeout_sec = 60`) {
|
|
t.Fatalf("expected startup timeout to be written, got %s", text)
|
|
}
|
|
}
|
|
|
|
func TestUpsertCodexMCPServerConfigReplacesExistingBlockAndNestedSections(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.toml")
|
|
initial := strings.Join([]string{
|
|
`model = "gpt-5.4"`,
|
|
``,
|
|
`[mcp_servers.gonavi]`,
|
|
`command = 'old.exe'`,
|
|
`args = ['old']`,
|
|
`startup_timeout_sec = 15`,
|
|
``,
|
|
`[mcp_servers.gonavi.env]`,
|
|
`FOO = "bar"`,
|
|
``,
|
|
`[projects.'D:\Work\CodeRepos\GoNavi']`,
|
|
`trust_level = "trusted"`,
|
|
``,
|
|
}, "\n")
|
|
if err := os.WriteFile(configPath, []byte(initial), 0o644); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
err := upsertCodexMCPServerConfig(configPath, gonaviMCPServerID, codexMCPServerConfig{
|
|
Command: `C:\Program Files\GoNavi\GoNavi.exe`,
|
|
Args: []string{"mcp-server"},
|
|
StartupTimeoutSec: defaultCodexMCPStartupTimeoutSecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upsertCodexMCPServerConfig returned error: %v", err)
|
|
}
|
|
|
|
updated, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile returned error: %v", err)
|
|
}
|
|
text := string(updated)
|
|
if strings.Contains(text, `command = 'old.exe'`) || strings.Contains(text, `[mcp_servers.gonavi.env]`) {
|
|
t.Fatalf("expected old gonavi block to be replaced, got %s", text)
|
|
}
|
|
if !strings.Contains(text, `[projects.'D:\Work\CodeRepos\GoNavi']`) {
|
|
t.Fatalf("expected unrelated project config to be preserved, got %s", text)
|
|
}
|
|
}
|
|
|
|
func TestInspectClaudeCodeMCPInstallStatusIncludesLocalCLIAvailability(t *testing.T) {
|
|
originalConfigPathFunc := claudeCodeConfigPathFunc
|
|
originalCLIPathFunc := localCLICommandPathFunc
|
|
t.Cleanup(func() {
|
|
claudeCodeConfigPathFunc = originalConfigPathFunc
|
|
localCLICommandPathFunc = originalCLIPathFunc
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
claudeCodeConfigPathFunc = func() (string, error) {
|
|
return filepath.Join(tempDir, ".claude.json"), nil
|
|
}
|
|
localCLICommandPathFunc = func(file string) (string, error) {
|
|
if file != claudeCodeClientCommandName {
|
|
t.Fatalf("expected lookup for %q, got %q", claudeCodeClientCommandName, file)
|
|
}
|
|
return `C:\Users\mock\AppData\Roaming\npm\claude.CMD`, nil
|
|
}
|
|
|
|
status := inspectClaudeCodeMCPInstallStatus(`C:\Program Files\GoNavi\GoNavi.exe`, []string{"mcp-server"}, nil)
|
|
if !status.ClientDetected {
|
|
t.Fatal("expected Claude Code command detection to be true")
|
|
}
|
|
if status.ClientCommand != claudeCodeClientCommandName {
|
|
t.Fatalf("expected client command %q, got %q", claudeCodeClientCommandName, status.ClientCommand)
|
|
}
|
|
if status.ClientPath != `C:\Users\mock\AppData\Roaming\npm\claude.CMD` {
|
|
t.Fatalf("unexpected client path: %q", status.ClientPath)
|
|
}
|
|
if status.Installed {
|
|
t.Fatal("expected MCP config to remain uninstalled when config file is absent")
|
|
}
|
|
}
|
|
|
|
func TestInspectCodexMCPInstallStatusKeepsMissingCLISignalSeparateFromConfigState(t *testing.T) {
|
|
originalConfigPathFunc := codexConfigPathFunc
|
|
originalCLIPathFunc := localCLICommandPathFunc
|
|
t.Cleanup(func() {
|
|
codexConfigPathFunc = originalConfigPathFunc
|
|
localCLICommandPathFunc = originalCLIPathFunc
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
codexConfigPathFunc = func() (string, error) {
|
|
return filepath.Join(tempDir, "config.toml"), nil
|
|
}
|
|
localCLICommandPathFunc = func(file string) (string, error) {
|
|
if file != codexClientCommandName {
|
|
t.Fatalf("expected lookup for %q, got %q", codexClientCommandName, file)
|
|
}
|
|
return "", errors.New("not found")
|
|
}
|
|
|
|
status := inspectCodexMCPInstallStatus(`C:\Program Files\GoNavi\GoNavi.exe`, []string{"mcp-server"}, nil)
|
|
if status.ClientDetected {
|
|
t.Fatal("expected codex command detection to be false")
|
|
}
|
|
if status.ClientCommand != codexClientCommandName {
|
|
t.Fatalf("expected client command %q, got %q", codexClientCommandName, status.ClientCommand)
|
|
}
|
|
if status.ClientPath != "" {
|
|
t.Fatalf("expected missing codex command path to be empty, got %q", status.ClientPath)
|
|
}
|
|
if status.Message != "No Codex user-level GoNavi MCP configuration was detected" {
|
|
t.Fatalf("unexpected config message: %q", status.Message)
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallResultMessagesUseServiceLanguage(t *testing.T) {
|
|
originalClaudeConfigPathFunc := claudeCodeConfigPathFunc
|
|
originalCodexConfigPathFunc := codexConfigPathFunc
|
|
originalExecutablePathFunc := localMCPExecutablePathFunc
|
|
t.Cleanup(func() {
|
|
claudeCodeConfigPathFunc = originalClaudeConfigPathFunc
|
|
codexConfigPathFunc = originalCodexConfigPathFunc
|
|
localMCPExecutablePathFunc = originalExecutablePathFunc
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
claudeCodeConfigPathFunc = func() (string, error) {
|
|
return filepath.Join(tempDir, ".claude.json"), nil
|
|
}
|
|
codexConfigPathFunc = func() (string, error) {
|
|
return filepath.Join(tempDir, ".codex", "config.toml"), nil
|
|
}
|
|
localMCPExecutablePathFunc = func() (string, error) {
|
|
return `C:\Program Files\GoNavi\GoNavi.exe`, nil
|
|
}
|
|
|
|
service := NewService()
|
|
service.AISetLanguage(string(i18n.LanguageEnUS))
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
|
|
claudeResult, err := service.AIInstallClaudeCodeMCP()
|
|
if err != nil {
|
|
t.Fatalf("AIInstallClaudeCodeMCP returned error: %v", err)
|
|
}
|
|
if hanText.MatchString(claudeResult.Message) {
|
|
t.Fatalf("Claude Code install message should be localized to English, got %q", claudeResult.Message)
|
|
}
|
|
if !strings.Contains(claudeResult.Message, "Claude Code user-level MCP configuration") {
|
|
t.Fatalf("unexpected Claude Code install message: %q", claudeResult.Message)
|
|
}
|
|
|
|
codexResult, err := service.AIInstallCodexMCP()
|
|
if err != nil {
|
|
t.Fatalf("AIInstallCodexMCP returned error: %v", err)
|
|
}
|
|
if hanText.MatchString(codexResult.Message) {
|
|
t.Fatalf("Codex install message should be localized to English, got %q", codexResult.Message)
|
|
}
|
|
if !strings.Contains(codexResult.Message, "Codex user-level MCP configuration") {
|
|
t.Fatalf("unexpected Codex install message: %q", codexResult.Message)
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallStatusMessagesUseServiceLanguage(t *testing.T) {
|
|
originalClaudeConfigPathFunc := claudeCodeConfigPathFunc
|
|
originalCodexConfigPathFunc := codexConfigPathFunc
|
|
originalExecutablePathFunc := localMCPExecutablePathFunc
|
|
originalCLIPathFunc := localCLICommandPathFunc
|
|
t.Cleanup(func() {
|
|
claudeCodeConfigPathFunc = originalClaudeConfigPathFunc
|
|
codexConfigPathFunc = originalCodexConfigPathFunc
|
|
localMCPExecutablePathFunc = originalExecutablePathFunc
|
|
localCLICommandPathFunc = originalCLIPathFunc
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
claudeCodeConfigPathFunc = func() (string, error) {
|
|
return filepath.Join(tempDir, ".claude.json"), nil
|
|
}
|
|
codexConfigPathFunc = func() (string, error) {
|
|
return filepath.Join(tempDir, ".codex", "config.toml"), nil
|
|
}
|
|
localMCPExecutablePathFunc = func() (string, error) {
|
|
return `C:\Program Files\GoNavi\GoNavi.exe`, nil
|
|
}
|
|
localCLICommandPathFunc = func(string) (string, error) {
|
|
return "", errors.New("not found")
|
|
}
|
|
|
|
service := NewService()
|
|
service.AISetLanguage(string(i18n.LanguageEnUS))
|
|
statuses := service.AIGetMCPClientInstallStatuses()
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
|
|
for _, status := range statuses {
|
|
if hanText.MatchString(status.Message) {
|
|
t.Fatalf("%s status message should be localized to English, got %q", status.Client, status.Message)
|
|
}
|
|
}
|
|
|
|
if len(statuses) != 4 {
|
|
t.Fatalf("expected 4 MCP client statuses, got %d", len(statuses))
|
|
}
|
|
if !strings.Contains(statuses[0].Message, "No Claude Code user-level GoNavi MCP configuration") {
|
|
t.Fatalf("unexpected Claude Code status message: %q", statuses[0].Message)
|
|
}
|
|
if !strings.Contains(statuses[1].Message, "No Codex user-level GoNavi MCP configuration") {
|
|
t.Fatalf("unexpected Codex status message: %q", statuses[1].Message)
|
|
}
|
|
if !strings.Contains(statuses[2].Message, "usually runs in the cloud or a remote environment") {
|
|
t.Fatalf("unexpected remote client status message: %q", statuses[2].Message)
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallConfigPathFailuresUseServiceLanguage(t *testing.T) {
|
|
originalClaudeConfigPathFunc := claudeCodeConfigPathFunc
|
|
originalCodexConfigPathFunc := codexConfigPathFunc
|
|
originalExecutablePathFunc := localMCPExecutablePathFunc
|
|
originalCLIPathFunc := localCLICommandPathFunc
|
|
t.Cleanup(func() {
|
|
claudeCodeConfigPathFunc = originalClaudeConfigPathFunc
|
|
codexConfigPathFunc = originalCodexConfigPathFunc
|
|
localMCPExecutablePathFunc = originalExecutablePathFunc
|
|
localCLICommandPathFunc = originalCLIPathFunc
|
|
})
|
|
|
|
claudeCodeConfigPathFunc = func() (string, error) {
|
|
return "", errors.New("claude config denied")
|
|
}
|
|
codexConfigPathFunc = func() (string, error) {
|
|
return "", errors.New("codex config denied")
|
|
}
|
|
localMCPExecutablePathFunc = func() (string, error) {
|
|
return `C:\Program Files\GoNavi\GoNavi.exe`, nil
|
|
}
|
|
localCLICommandPathFunc = func(string) (string, error) {
|
|
return "", errors.New("not found")
|
|
}
|
|
|
|
service := NewService()
|
|
service.AISetLanguage(string(i18n.LanguageEnUS))
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
|
|
if _, err := service.AIInstallClaudeCodeMCP(); err == nil {
|
|
t.Fatal("expected Claude Code install config path error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to locate Claude Code configuration") || !strings.Contains(err.Error(), "claude config denied") {
|
|
t.Fatalf("Claude Code install error should use English wrapper and keep raw detail, got %q", err.Error())
|
|
}
|
|
|
|
if _, err := service.AIInstallCodexMCP(); err == nil {
|
|
t.Fatal("expected Codex install config path error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to locate Codex configuration") || !strings.Contains(err.Error(), "codex config denied") {
|
|
t.Fatalf("Codex install error should use English wrapper and keep raw detail, got %q", err.Error())
|
|
}
|
|
|
|
statuses := service.AIGetMCPClientInstallStatuses()
|
|
if len(statuses) < 2 {
|
|
t.Fatalf("expected at least Claude Code and Codex statuses, got %d", len(statuses))
|
|
}
|
|
if hanText.MatchString(statuses[0].Message) || !strings.Contains(statuses[0].Message, "Failed to locate Claude Code configuration") || !strings.Contains(statuses[0].Message, "claude config denied") {
|
|
t.Fatalf("Claude Code status should use English wrapper and keep raw detail, got %q", statuses[0].Message)
|
|
}
|
|
if hanText.MatchString(statuses[1].Message) || !strings.Contains(statuses[1].Message, "Failed to locate Codex configuration") || !strings.Contains(statuses[1].Message, "codex config denied") {
|
|
t.Fatalf("Codex status should use English wrapper and keep raw detail, got %q", statuses[1].Message)
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallHomeDirDetailUsesServiceLanguage(t *testing.T) {
|
|
originalClaudeConfigPathFunc := claudeCodeConfigPathFunc
|
|
originalCodexConfigPathFunc := codexConfigPathFunc
|
|
originalExecutablePathFunc := localMCPExecutablePathFunc
|
|
originalCLIPathFunc := localCLICommandPathFunc
|
|
t.Cleanup(func() {
|
|
claudeCodeConfigPathFunc = originalClaudeConfigPathFunc
|
|
codexConfigPathFunc = originalCodexConfigPathFunc
|
|
localMCPExecutablePathFunc = originalExecutablePathFunc
|
|
localCLICommandPathFunc = originalCLIPathFunc
|
|
})
|
|
|
|
claudeCodeConfigPathFunc = func() (string, error) {
|
|
return "", errMCPClientUserHomeDirUnavailable
|
|
}
|
|
codexConfigPathFunc = func() (string, error) {
|
|
return "", errMCPClientUserHomeDirUnavailable
|
|
}
|
|
localMCPExecutablePathFunc = func() (string, error) {
|
|
return `C:\Program Files\GoNavi\GoNavi.exe`, nil
|
|
}
|
|
localCLICommandPathFunc = func(string) (string, error) {
|
|
return "", errors.New("not found")
|
|
}
|
|
|
|
service := NewService()
|
|
service.AISetLanguage(string(i18n.LanguageEnUS))
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
|
|
if _, err := service.AIInstallClaudeCodeMCP(); err == nil {
|
|
t.Fatal("expected Claude Code install home-dir error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Unable to determine the current user home directory") {
|
|
t.Fatalf("Claude Code install home-dir detail should be localized, got %q", err.Error())
|
|
}
|
|
|
|
if _, err := service.AIInstallCodexMCP(); err == nil {
|
|
t.Fatal("expected Codex install home-dir error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Unable to determine the current user home directory") {
|
|
t.Fatalf("Codex install home-dir detail should be localized, got %q", err.Error())
|
|
}
|
|
|
|
statuses := service.AIGetMCPClientInstallStatuses()
|
|
if len(statuses) < 2 {
|
|
t.Fatalf("expected at least Claude Code and Codex statuses, got %d", len(statuses))
|
|
}
|
|
if hanText.MatchString(statuses[0].Message) || !strings.Contains(statuses[0].Message, "Unable to determine the current user home directory") {
|
|
t.Fatalf("Claude Code status home-dir detail should be localized, got %q", statuses[0].Message)
|
|
}
|
|
if hanText.MatchString(statuses[1].Message) || !strings.Contains(statuses[1].Message, "Unable to determine the current user home directory") {
|
|
t.Fatalf("Codex status home-dir detail should be localized, got %q", statuses[1].Message)
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallExecutablePathFailuresUseServiceLanguage(t *testing.T) {
|
|
originalClaudeConfigPathFunc := claudeCodeConfigPathFunc
|
|
originalCodexConfigPathFunc := codexConfigPathFunc
|
|
originalExecutablePathFunc := localMCPExecutablePathFunc
|
|
originalCLIPathFunc := localCLICommandPathFunc
|
|
t.Cleanup(func() {
|
|
claudeCodeConfigPathFunc = originalClaudeConfigPathFunc
|
|
codexConfigPathFunc = originalCodexConfigPathFunc
|
|
localMCPExecutablePathFunc = originalExecutablePathFunc
|
|
localCLICommandPathFunc = originalCLIPathFunc
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
claudeConfigPath := filepath.Join(tempDir, ".claude.json")
|
|
codexConfigPath := filepath.Join(tempDir, ".codex", "config.toml")
|
|
claudeCodeConfigPathFunc = func() (string, error) {
|
|
return claudeConfigPath, nil
|
|
}
|
|
codexConfigPathFunc = func() (string, error) {
|
|
return codexConfigPath, nil
|
|
}
|
|
localCLICommandPathFunc = func(string) (string, error) {
|
|
return "", errors.New("not found")
|
|
}
|
|
if err := os.WriteFile(claudeConfigPath, []byte(`{"mcpServers":{"gonavi":{"type":"stdio","command":"old-gonavi","args":["mcp-server"]}}}`), 0o644); err != nil {
|
|
t.Fatalf("WriteFile Claude config returned error: %v", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(codexConfigPath), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll Codex config dir returned error: %v", err)
|
|
}
|
|
if err := os.WriteFile(codexConfigPath, []byte("[mcp_servers.gonavi]\ncommand = 'old-gonavi'\nargs = ['mcp-server']\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile Codex config returned error: %v", err)
|
|
}
|
|
|
|
service := NewService()
|
|
service.AISetLanguage(string(i18n.LanguageEnUS))
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
|
|
localMCPExecutablePathFunc = func() (string, error) {
|
|
return "", errors.New("executable lookup denied")
|
|
}
|
|
if _, err := service.AIInstallClaudeCodeMCP(); err == nil {
|
|
t.Fatal("expected Claude Code install executable path error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to locate the current GoNavi executable") || !strings.Contains(err.Error(), "executable lookup denied") {
|
|
t.Fatalf("Claude Code executable path error should use English wrapper and keep raw detail, got %q", err.Error())
|
|
}
|
|
statuses := service.AIGetMCPClientInstallStatuses()
|
|
if len(statuses) < 2 {
|
|
t.Fatalf("expected at least Claude Code and Codex statuses, got %d", len(statuses))
|
|
}
|
|
for _, status := range statuses[:2] {
|
|
if hanText.MatchString(status.Message) || !strings.Contains(status.Message, "Failed to locate the current GoNavi executable") || !strings.Contains(status.Message, "executable lookup denied") {
|
|
t.Fatalf("%s status executable path error should use English wrapper and keep raw detail, got %q", status.Client, status.Message)
|
|
}
|
|
}
|
|
|
|
localMCPExecutablePathFunc = func() (string, error) {
|
|
return " ", nil
|
|
}
|
|
if _, err := service.AIInstallCodexMCP(); err == nil {
|
|
t.Fatal("expected Codex install empty executable path error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Current GoNavi executable path is empty") {
|
|
t.Fatalf("Codex empty executable path error should use English wrapper, got %q", err.Error())
|
|
}
|
|
statuses = service.AIGetMCPClientInstallStatuses()
|
|
for _, status := range statuses[:2] {
|
|
if hanText.MatchString(status.Message) || !strings.Contains(status.Message, "Current GoNavi executable path is empty") {
|
|
t.Fatalf("%s status empty executable path error should use English wrapper, got %q", status.Client, status.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallConfigFormatFailuresUseServiceLanguage(t *testing.T) {
|
|
originalClaudeConfigPathFunc := claudeCodeConfigPathFunc
|
|
originalCodexConfigPathFunc := codexConfigPathFunc
|
|
originalExecutablePathFunc := localMCPExecutablePathFunc
|
|
originalCLIPathFunc := localCLICommandPathFunc
|
|
t.Cleanup(func() {
|
|
claudeCodeConfigPathFunc = originalClaudeConfigPathFunc
|
|
codexConfigPathFunc = originalCodexConfigPathFunc
|
|
localMCPExecutablePathFunc = originalExecutablePathFunc
|
|
localCLICommandPathFunc = originalCLIPathFunc
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
claudeConfigPath := filepath.Join(tempDir, ".claude.json")
|
|
codexConfigPath := filepath.Join(tempDir, ".codex", "config.toml")
|
|
claudeCodeConfigPathFunc = func() (string, error) {
|
|
return claudeConfigPath, nil
|
|
}
|
|
codexConfigPathFunc = func() (string, error) {
|
|
return codexConfigPath, nil
|
|
}
|
|
localMCPExecutablePathFunc = func() (string, error) {
|
|
return `C:\Program Files\GoNavi\GoNavi.exe`, nil
|
|
}
|
|
localCLICommandPathFunc = func(string) (string, error) {
|
|
return "", errors.New("not found")
|
|
}
|
|
|
|
service := NewService()
|
|
service.AISetLanguage(string(i18n.LanguageEnUS))
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
|
|
if err := os.WriteFile(claudeConfigPath, []byte(`{"mcpServers":[]}`), 0o644); err != nil {
|
|
t.Fatalf("WriteFile Claude config returned error: %v", err)
|
|
}
|
|
if _, err := service.AIInstallClaudeCodeMCP(); err == nil {
|
|
t.Fatal("expected Claude Code invalid config shape error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Claude Code configuration format is invalid") || !strings.Contains(err.Error(), "mcpServers") {
|
|
t.Fatalf("Claude Code config format error should use English wrapper and keep raw path segment, got %q", err.Error())
|
|
}
|
|
|
|
if err := os.WriteFile(claudeConfigPath, []byte(`{"mcpServers":{"gonavi":[]}}`), 0o644); err != nil {
|
|
t.Fatalf("WriteFile Claude config returned error: %v", err)
|
|
}
|
|
statuses := service.AIGetMCPClientInstallStatuses()
|
|
if len(statuses) < 2 {
|
|
t.Fatalf("expected at least Claude Code and Codex statuses, got %d", len(statuses))
|
|
}
|
|
if hanText.MatchString(statuses[0].Message) || !strings.Contains(statuses[0].Message, "Claude Code configuration format is invalid") || !strings.Contains(statuses[0].Message, "mcpServers.gonavi") {
|
|
t.Fatalf("Claude Code status format error should use English wrapper and keep raw path segment, got %q", statuses[0].Message)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(codexConfigPath), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll Codex config dir returned error: %v", err)
|
|
}
|
|
if err := os.WriteFile(codexConfigPath, []byte("[mcp_servers.gonavi]\ncommand = [bad]\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile Codex config returned error: %v", err)
|
|
}
|
|
statuses = service.AIGetMCPClientInstallStatuses()
|
|
if hanText.MatchString(statuses[1].Message) || !strings.Contains(statuses[1].Message, "Codex configuration format is invalid") || !strings.Contains(statuses[1].Message, "mcp_servers.gonavi.command") {
|
|
t.Fatalf("Codex status format error should use English wrapper and keep raw path segment, got %q", statuses[1].Message)
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallConfigIOFailuresUseServiceLanguage(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
service := NewService()
|
|
service.AISetLanguage(string(i18n.LanguageEnUS))
|
|
hanText := regexp.MustCompile(`\p{Han}`)
|
|
|
|
claudeConfigDir := filepath.Join(tempDir, "claude-dir")
|
|
if err := os.MkdirAll(claudeConfigDir, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll Claude config dir returned error: %v", err)
|
|
}
|
|
if _, err := readClaudeCodeConfig(claudeConfigDir, service.serviceText); err == nil {
|
|
t.Fatal("expected Claude Code read config error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to read Claude Code configuration") {
|
|
t.Fatalf("Claude Code read error should use English wrapper, got %q", err.Error())
|
|
}
|
|
|
|
invalidClaudeConfigPath := filepath.Join(tempDir, ".claude-invalid.json")
|
|
if err := os.WriteFile(invalidClaudeConfigPath, []byte(`{"mcpServers":`), 0o644); err != nil {
|
|
t.Fatalf("WriteFile invalid Claude config returned error: %v", err)
|
|
}
|
|
if _, err := readClaudeCodeConfig(invalidClaudeConfigPath, service.serviceText); err == nil {
|
|
t.Fatal("expected Claude Code parse config error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to parse Claude Code configuration") {
|
|
t.Fatalf("Claude Code parse error should use English wrapper, got %q", err.Error())
|
|
}
|
|
|
|
blockingFile := filepath.Join(tempDir, "blocking-file")
|
|
if err := os.WriteFile(blockingFile, []byte("not a directory"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile blocking file returned error: %v", err)
|
|
}
|
|
blockedClaudeConfigPath := filepath.Join(blockingFile, ".claude.json")
|
|
if err := upsertClaudeCodeMCPServerConfig(blockedClaudeConfigPath, gonaviMCPServerID, claudeCodeMCPServerConfig{
|
|
Type: "stdio",
|
|
Command: "GoNavi.exe",
|
|
}, service.serviceText); err == nil {
|
|
t.Fatal("expected Claude Code directory creation error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to create Claude Code configuration directory") {
|
|
t.Fatalf("Claude Code directory creation error should use English wrapper, got %q", err.Error())
|
|
}
|
|
|
|
codexConfigDir := filepath.Join(tempDir, "codex-dir")
|
|
if err := os.MkdirAll(codexConfigDir, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll Codex config dir returned error: %v", err)
|
|
}
|
|
if _, _, err := readCodexMCPServerConfig(codexConfigDir, gonaviMCPServerID, service.serviceText); err == nil {
|
|
t.Fatal("expected Codex read config error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to read Codex configuration") {
|
|
t.Fatalf("Codex read error should use English wrapper, got %q", err.Error())
|
|
}
|
|
|
|
blockedCodexConfigPath := filepath.Join(blockingFile, "config.toml")
|
|
if err := upsertCodexMCPServerConfig(blockedCodexConfigPath, gonaviMCPServerID, codexMCPServerConfig{
|
|
Command: "GoNavi.exe",
|
|
Args: []string{"mcp-server"},
|
|
}, service.serviceText); err == nil {
|
|
t.Fatal("expected Codex directory creation error")
|
|
} else if hanText.MatchString(err.Error()) || !strings.Contains(err.Error(), "Failed to create Codex configuration directory") {
|
|
t.Fatalf("Codex directory creation error should use English wrapper, got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestMCPClientInstallMessageCatalogKeysExist(t *testing.T) {
|
|
catalogs, err := i18n.LoadCatalogs()
|
|
if err != nil {
|
|
t.Fatalf("LoadCatalogs() error = %v", err)
|
|
}
|
|
|
|
keys := []string{
|
|
"ai.service.mcp_client.claude_code.install_success",
|
|
"ai.service.mcp_client.codex.install_success",
|
|
"ai.service.mcp_client.user_home_dir_unavailable",
|
|
"ai.service.mcp_client.claude_code.config_path_failed",
|
|
"ai.service.mcp_client.codex.config_path_failed",
|
|
"ai.service.mcp_client.executable_path_failed",
|
|
"ai.service.mcp_client.executable_path_empty",
|
|
"ai.service.mcp_client.claude_code.config_format_invalid",
|
|
"ai.service.mcp_client.codex.config_format_invalid",
|
|
"ai.service.mcp_client.claude_code.config_read_failed",
|
|
"ai.service.mcp_client.claude_code.config_parse_failed",
|
|
"ai.service.mcp_client.claude_code.config_serialize_failed",
|
|
"ai.service.mcp_client.claude_code.config_dir_create_failed",
|
|
"ai.service.mcp_client.claude_code.config_write_failed",
|
|
"ai.service.mcp_client.codex.config_read_failed",
|
|
"ai.service.mcp_client.codex.config_dir_create_failed",
|
|
"ai.service.mcp_client.codex.config_write_failed",
|
|
"ai.service.mcp_client.claude_code.status.missing",
|
|
"ai.service.mcp_client.codex.status.missing",
|
|
"ai.service.mcp_client.claude_code.status.path_check_failed",
|
|
"ai.service.mcp_client.codex.status.path_check_failed",
|
|
"ai.service.mcp_client.claude_code.status.connected",
|
|
"ai.service.mcp_client.codex.status.connected",
|
|
"ai.service.mcp_client.claude_code.status.path_mismatch",
|
|
"ai.service.mcp_client.codex.status.path_mismatch",
|
|
"ai.service.mcp_client.remote.status.message",
|
|
}
|
|
for _, language := range i18n.SupportedLanguages() {
|
|
catalog := catalogs[language]
|
|
for _, key := range keys {
|
|
if strings.TrimSpace(catalog[key]) == "" {
|
|
t.Fatalf("%s catalog missing MCP client install message key %q", language, key)
|
|
}
|
|
}
|
|
}
|
|
}
|