package remote import ( "encoding/json" "os" "path/filepath" "strings" "testing" "time" "lingma-ipc-proxy/internal/toolemulation" ) func TestNewKeepsZeroTimeoutUnlimited(t *testing.T) { client := New(Config{Timeout: 0}) if client.client.Timeout != 0 { t.Fatalf("timeout = %v, want 0", client.client.Timeout) } } func TestNewKeepsPositiveTimeout(t *testing.T) { client := New(Config{Timeout: 7 * time.Second}) if client.client.Timeout != 7*time.Second { t.Fatalf("timeout = %v, want 7s", client.client.Timeout) } } func TestExtractBaseURLFromEndpointLog(t *testing.T) { got := extractBaseURLFromText(`2026-04-10 INFO Update endpoint success. endpoint config: https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`) want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestExtractBaseURLFromMarketplaceLog(t *testing.T) { got := extractBaseURLFromText(`2026-04-30 [info] [Marketplace] Using service url: https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com/marketplace/_apis/public/gallery`) want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestExtractBaseURLFromRawWindowsLogURL(t *testing.T) { got := extractBaseURLFromText(`2026-05-06T12:00:00 endpoint=https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com/algo/api/v2/model/list`) want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestExtractBaseURLIgnoresLingmaOSSAssetHost(t *testing.T) { got := extractBaseURLFromText(`2026-05-06 endpoint config: https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com 2026-05-06 Download asset from: https://lingma-ide.oss-rg-china-mainland.aliyuncs.com/lingma-extension/download?name=plugin.zip`) want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestNormalizeBaseURLRepairsMissingLeadingH(t *testing.T) { got := normalizeRemoteBaseURLHint(`ttps://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`) want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestNormalizeBaseURLRejectsLingmaOSSAssetHost(t *testing.T) { if got := normalizeRemoteBaseURLHint(`https://lingma-ide.oss-rg-china-mainland.aliyuncs.com/lingma-extension/download`); got != "" { t.Fatalf("got %q, want empty", got) } } func TestNormalizeBaseURLRejectsUnsupportedScheme(t *testing.T) { if got := normalizeRemoteBaseURLHint(`ftp://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`); got != "" { t.Fatalf("got %q, want empty", got) } } func TestModelListStatusErrorSuggestsManualRemoteBaseURLOn404(t *testing.T) { client := New(Config{BaseURL: "https://lingma-ide.oss-rg-china-mainland.aliyuncs.com"}) err := client.modelListStatusError(404, `NoSuchKey`) if err == nil { t.Fatal("expected error") } text := err.Error() for _, want := range []string{ "https://lingma-ide.oss-rg-china-mainland.aliyuncs.com", "远端 API 域名自动探测命中了错误地址", "https://lingma.alibabacloud.com", } { if !strings.Contains(text, want) { t.Fatalf("error %q missing %q", text, want) } } } func TestBuildBodyProjectsNativeTools(t *testing.T) { client := New(Config{}) body, err := client.buildBody("req-1", ChatRequest{ Model: "kmodel", Prompt: "read file", Tools: []toolemulation.ToolDef{{ Name: "read_file", Description: "Read a local file", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ "file_path": map[string]any{"type": "string"}, }, "required": []any{"file_path"}, }, }}, ToolChoice: toolemulation.ToolChoice{Mode: "tool", Name: "read_file"}, }) if err != nil { t.Fatal(err) } var payload map[string]any if err := json.Unmarshal([]byte(body), &payload); err != nil { t.Fatal(err) } tools, ok := payload["tools"].([]any) if !ok || len(tools) != 1 { t.Fatalf("tools = %#v", payload["tools"]) } tool := tools[0].(map[string]any) fn := tool["function"].(map[string]any) if tool["type"] != "function" || fn["name"] != "read_file" { t.Fatalf("unexpected tool projection: %#v", tool) } choice := payload["tool_choice"].(map[string]any) choiceFn := choice["function"].(map[string]any) if choice["type"] != "function" || choiceFn["name"] != "read_file" { t.Fatalf("unexpected tool choice: %#v", payload["tool_choice"]) } } func TestBuildBodyPreservesStructuredToolMessages(t *testing.T) { client := New(Config{}) body, err := client.buildBody("req-1", ChatRequest{ Model: "kmodel", Prompt: "fallback prompt", Messages: []Message{ {Role: "user", Content: "查看项目"}, {Role: "assistant", ToolCalls: []toolemulation.ToolCall{{ ID: "call_1", Name: "Bash", Arguments: map[string]any{"command": "pwd && ls -la"}, }}}, {Role: "tool", ToolCallID: "call_1", Content: "total 10"}, }, }) if err != nil { t.Fatal(err) } var payload map[string]any if err := json.Unmarshal([]byte(body), &payload); err != nil { t.Fatal(err) } messages := payload["messages"].([]any) if len(messages) != 3 { t.Fatalf("messages = %#v", messages) } assistant := messages[1].(map[string]any) calls := assistant["tool_calls"].([]any) call := calls[0].(map[string]any) fn := call["function"].(map[string]any) args := fn["arguments"].(string) if assistant["role"] != "assistant" || fn["name"] != "Bash" || !strings.Contains(args, "pwd") || !strings.Contains(args, "ls -la") { t.Fatalf("unexpected assistant message: %#v", assistant) } tool := messages[2].(map[string]any) if tool["role"] != "tool" || tool["tool_call_id"] != "call_1" || tool["content"] != "total 10" { t.Fatalf("unexpected tool message: %#v", tool) } } func TestBuildBodyProjectsRemoteImages(t *testing.T) { client := New(Config{}) body, err := client.buildBody("req-1", ChatRequest{ Model: "kmodel", Prompt: "看图", Messages: []Message{{ Role: "user", Content: "看图", Images: []Image{{ MediaType: "image/png", Data: "iVBORw0KGgo=", }}, }}, Images: []Image{{ MediaType: "image/png", Data: "iVBORw0KGgo=", }}, }) if err != nil { t.Fatal(err) } var payload map[string]any if err := json.Unmarshal([]byte(body), &payload); err != nil { t.Fatal(err) } images, ok := payload["image_urls"].([]any) if !ok || len(images) != 1 { t.Fatalf("image_urls = %#v", payload["image_urls"]) } image, ok := images[0].(string) if !ok || !strings.HasPrefix(image, "data:image/png;base64,") { t.Fatalf("unexpected image projection: %#v", images[0]) } modelConfig := payload["model_config"].(map[string]any) if modelConfig["is_vl"] != true { t.Fatalf("model_config.is_vl = %#v, want true", modelConfig["is_vl"]) } messages := payload["messages"].([]any) message := messages[0].(map[string]any) content := message["content"].([]any) if content[0].(map[string]any)["type"] != "text" || content[1].(map[string]any)["type"] != "image_url" { t.Fatalf("unexpected message content: %#v", content) } } func TestParseSSEPayloadExtractsNativeToolCallFragments(t *testing.T) { payload := `{"body":"{\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"read_file\",\"arguments\":\"{\\\"file_path\\\":\\\"/tmp/a.txt\\\"}\"}}]}}]}","statusCodeValue":200}` event, ok, err := parseSSEPayload(payload) if err != nil { t.Fatal(err) } if !ok { t.Fatal("event not parsed") } if len(event.ToolCalls) != 1 { t.Fatalf("tool calls = %#v", event.ToolCalls) } call := event.ToolCalls[0] if call.ID != "call_1" || call.Name != "read_file" || call.ArgumentsFragment != `{"file_path":"/tmp/a.txt"}` { t.Fatalf("unexpected call = %#v", call) } } func TestRemoteToolCallBufferMergesArgumentFragments(t *testing.T) { buffer := newRemoteToolCallBuffer() buffer.Add([]remoteToolCallFragment{{ Index: 0, ID: "call_1", Type: "function", Name: "read_file", }}) buffer.Add([]remoteToolCallFragment{{Index: 0, ArgumentsFragment: `{"file_path":"/tmp`}}) buffer.Add([]remoteToolCallFragment{{Index: 0, ArgumentsFragment: `/lingma-native`}}) buffer.Add([]remoteToolCallFragment{{Index: 0, ArgumentsFragment: `-tool-test.txt"}`}}) calls := buffer.Calls() if len(calls) != 1 { t.Fatalf("calls = %#v", calls) } call := calls[0] if call.ID != "call_1" || call.Name != "read_file" || call.Arguments["file_path"] != "/tmp/lingma-native-tool-test.txt" { t.Fatalf("unexpected merged call = %#v", call) } } func TestExtractMachineIDFromTextMarkers(t *testing.T) { got := extractMachineIDFromText(`2026-05-06 info using machine id from file: abcdef1234567890abcdef`) if got != "abcdef1234567890abcdef" { t.Fatalf("machine id = %q", got) } } func TestExtractMachineIDFromTextJSON(t *testing.T) { got := extractMachineIDFromText(`{"machineId":"windows-machine-id-1234567890","other":true}`) if got != "windows-machine-id-1234567890" { t.Fatalf("machine id = %q", got) } } func TestCandidateLingmaCacheDirsIncludesVSCodeSharedClientCache(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) t.Setenv("LINGMA_CACHE_DIR", "") dirs := candidateLingmaCacheDirs() want := filepath.Join(home, ".lingma", "vscode", "sharedClientCache") for _, dir := range dirs { if dir == want { return } } t.Fatalf("missing vscode shared client cache %q in %#v", want, dirs) } func TestLoadMachineIDReadsVSCodeSharedClientCacheID(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "cache"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "cache", "id"), []byte("abcdefghijklmnop1234"), 0644); err != nil { t.Fatal(err) } got, err := loadMachineID(dir) if err != nil { t.Fatal(err) } if got != "abcdefghijklmnop1234" { t.Fatalf("machine id = %q", got) } }