package service import ( "encoding/json" "strings" "testing" ) func TestNormalizeWindsurfRequestCanonicalizesToolsChoiceAndHistory(t *testing.T) { req := windsurfMessagesRequest{ Tools: []windsurfRequestTool{ { Name: "list_files", Description: "List files", InputSchema: json.RawMessage(`{"type":"object"}`), }, { Name: "glob", Description: "Duplicate canonical alias", InputSchema: json.RawMessage(`{"type":"object","properties":{"path":{"type":"string"}}}`), }, { Name: "applyPatch", Description: "Patch files", InputSchema: json.RawMessage(`{"type":"object"}`), }, }, ToolChoice: map[string]any{ "type": "tool", "name": "searchFiles", }, Messages: []windsurfRequestMessage{ { Role: "assistant", Content: json.RawMessage(`[ {"type":"tool_use","id":"call-1","name":"read_file","input":{"filePath":"a.go"}}, {"type":"text","text":"done"} ]`), }, }, } normalizeWindsurfRequest(&req) if len(req.Tools) != 2 { t.Fatalf("normalized tools len = %d, want 2", len(req.Tools)) } if req.Tools[0].Name != "glob" { t.Fatalf("first tool name = %q, want glob", req.Tools[0].Name) } if req.Tools[1].Name != "edit" { t.Fatalf("second tool name = %q, want edit", req.Tools[1].Name) } toolChoice, ok := req.ToolChoice.(map[string]any) if !ok { t.Fatalf("normalized tool choice type = %T, want map[string]any", req.ToolChoice) } if toolChoice["name"] != "grep" { t.Fatalf("tool choice name = %v, want grep", toolChoice["name"]) } var blocks []windsurfContentBlock if err := json.Unmarshal(req.Messages[0].Content, &blocks); err != nil { t.Fatalf("unmarshal normalized message content: %v", err) } if len(blocks) == 0 || blocks[0].Name != "read" { t.Fatalf("tool_use name = %q, want read", blocks[0].Name) } } func TestWindsurfExtractContentTextFromRawPreservesStructuredToolResult(t *testing.T) { raw := json.RawMessage(`[ {"type":"text","text":"summary"}, {"type":"json","value":{"entries":["main.go"]}} ]`) got := windsurfExtractContentTextFromRaw(raw) if !strings.Contains(got, `"type":"json"`) { t.Fatalf("structured tool_result content should be preserved, got %q", got) } } func TestNormalizeWindsurfRequestInjectsWorkspaceContext(t *testing.T) { req := windsurfMessagesRequest{ CWD: "/Users/alice/projects/billing", Messages: []windsurfRequestMessage{{Role: "user", Content: json.RawMessage(`"analyze billing"`)}}, } normalizeWindsurfRequest(&req) var system []windsurfContentBlock if err := json.Unmarshal(req.System, &system); err != nil { t.Fatalf("unmarshal system: %v", err) } if len(system) == 0 || system[0].Type != "text" { t.Fatalf("expected injected text system block, got %#v", system) } if !strings.Contains(system[0].Text, "Working directory: /Users/alice/projects/billing") { t.Fatalf("system context should include normalized cwd, got %q", system[0].Text) } if !strings.Contains(system[0].Text, "Relative file paths") { t.Fatalf("system context should explain relative path semantics, got %q", system[0].Text) } } func TestNormalizeWindsurfRequestPreservesExistingSystemAfterWorkspaceContext(t *testing.T) { req := windsurfMessagesRequest{ Metadata: map[string]any{ "workspace": map[string]any{"path": "/home/bob/src/project"}, }, System: json.RawMessage(`"existing instructions"`), Messages: []windsurfRequestMessage{{Role: "user", Content: json.RawMessage(`"hello"`)}}, } normalizeWindsurfRequest(&req) var system []windsurfContentBlock if err := json.Unmarshal(req.System, &system); err != nil { t.Fatalf("unmarshal system: %v", err) } if len(system) != 2 { t.Fatalf("system len = %d, want 2", len(system)) } if !strings.Contains(system[0].Text, "Workspace: /home/bob/src/project") { t.Fatalf("first system block should contain workspace context, got %q", system[0].Text) } if system[1].Text != "existing instructions" { t.Fatalf("existing system should be preserved after context block, got %q", system[1].Text) } } func TestNormalizeWindsurfRequestLiftsCWDFromEnvSystem(t *testing.T) { req := windsurfMessagesRequest{ System: json.RawMessage(`"You are Claude Code.\n\nWorking directory: /Users/alice/IdeaProjects/flux-panel\nIs directory a git repo: Yes\n"`), Messages: []windsurfRequestMessage{ {Role: "user", Content: json.RawMessage(`"check branches"`)}, }, } normalizeWindsurfRequest(&req) var system []windsurfContentBlock if err := json.Unmarshal(req.System, &system); err != nil { t.Fatalf("unmarshal system: %v", err) } if !strings.Contains(system[0].Text, "Working directory: /Users/alice/IdeaProjects/flux-panel") { t.Fatalf("expected cwd lifted from system env, got %q", system[0].Text) } } func TestNormalizeWindsurfRequestLiftsCWDFromSystemReminderBarePath(t *testing.T) { reminder := "" + strings.Repeat("x", 1000) + "\n\n" content, err := json.Marshal(reminder + `C:\Users\renfei\Downloads\WindsurfAPI-master 分析下这个项目`) if err != nil { t.Fatal(err) } req := windsurfMessagesRequest{ Messages: []windsurfRequestMessage{ {Role: "user", Content: content}, }, } normalizeWindsurfRequest(&req) var system []windsurfContentBlock if err := json.Unmarshal(req.System, &system); err != nil { t.Fatalf("unmarshal system: %v", err) } if !strings.Contains(system[0].Text, `Working directory: C:\Users\renfei\Downloads\WindsurfAPI-master`) { t.Fatalf("expected cwd lifted after system reminder, got %q", system[0].Text) } } func TestNormalizeWindsurfRequestDoesNotLiftSingleFilePathAsCWD(t *testing.T) { req := windsurfMessagesRequest{ Messages: []windsurfRequestMessage{ {Role: "user", Content: json.RawMessage(`"C:\\Users\\me\\notes.md 解释这个文件"`)}, }, } normalizeWindsurfRequest(&req) if len(req.System) != 0 { t.Fatalf("single file path should not be lifted as cwd, got %s", string(req.System)) } } func TestWindsurfExtractContentTextStripsSlashCommandSpec(t *testing.T) { raw := json.RawMessage(`[ {"type":"text","text":"/ccg:plan\nAsk the user for a feature name.\n分析一下这个项目 我感觉 计费逻辑出问题了\n真正的问题"} ]`) got := windsurfExtractContentText(raw) if strings.Contains(got, "Ask the user for a feature name") { t.Fatalf("command-message spec should be stripped, got %q", got) } if strings.Contains(got, "") { t.Fatalf("command-args tag should be stripped, got %q", got) } if !strings.Contains(got, "真正的问题") { t.Fatalf("normal user content should remain, got %q", got) } }