From 604449feb9f24e44d761203b6b504810fc0c6a63 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 5 Apr 2026 22:38:15 +0800 Subject: [PATCH] Fix desktop ACP provider catalog routing --- go/go_core/internal/acp/providers_sync.go | 5 - .../internal/acp/providers_sync_test.go | 23 +++++ go/go_core/internal/acp/routing_test.go | 95 +++++++++++++++---- go/go_core/internal/router/router.go | 7 +- go/go_core/internal/router/router_test.go | 23 +++++ 5 files changed, 123 insertions(+), 30 deletions(-) diff --git a/go/go_core/internal/acp/providers_sync.go b/go/go_core/internal/acp/providers_sync.go index 850df7e6..4d1daadc 100644 --- a/go/go_core/internal/acp/providers_sync.go +++ b/go/go_core/internal/acp/providers_sync.go @@ -3,8 +3,6 @@ package acp import ( "sort" "strings" - - "xworkmate/go_core/internal/shared" ) type syncedProvider struct { @@ -66,9 +64,6 @@ func (s *Server) syncedProviderByID(providerID string) (syncedProvider, bool) { func (s *Server) availableProviders() []string { providers := make(map[string]struct{}) - for _, provider := range shared.DetectACPProviders() { - providers[provider] = struct{}{} - } s.mu.Lock() for _, provider := range s.providerCatalog { if !provider.Enabled || strings.TrimSpace(provider.Endpoint) == "" { diff --git a/go/go_core/internal/acp/providers_sync_test.go b/go/go_core/internal/acp/providers_sync_test.go index 2e6d648c..bbe25e6e 100644 --- a/go/go_core/internal/acp/providers_sync_test.go +++ b/go/go_core/internal/acp/providers_sync_test.go @@ -4,11 +4,34 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" "testing" "xworkmate/go_core/internal/shared" ) +func TestCapabilitiesIgnoreLocalProviderAutodetectUntilSync(t *testing.T) { + fakeProvider := t.TempDir() + "/fake-claude" + if err := os.WriteFile(fakeProvider, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatalf("write fake provider: %v", err) + } + t.Setenv("ACP_CLAUDE_BIN", fakeProvider) + + server := NewServer() + result, rpcErr := server.handleRequest(shared.RPCRequest{ + Method: "acp.capabilities", + Params: map[string]any{}, + }, func(map[string]any) {}) + if rpcErr != nil { + t.Fatalf("expected capabilities success, got %v", rpcErr) + } + + providers, _ := result["providers"].([]string) + if len(providers) != 0 { + t.Fatalf("expected no providers before sync, got %#v", providers) + } +} + func TestProvidersSyncUpdatesCapabilities(t *testing.T) { server := NewServer() diff --git a/go/go_core/internal/acp/routing_test.go b/go/go_core/internal/acp/routing_test.go index 7e452dce..1fb97a51 100644 --- a/go/go_core/internal/acp/routing_test.go +++ b/go/go_core/internal/acp/routing_test.go @@ -1,6 +1,7 @@ package acp import ( + "encoding/json" "net/http" "net/http/httptest" "os" @@ -11,6 +12,36 @@ import ( "xworkmate/go_core/internal/shared" ) +func newExternalSingleAgentProvider( + t *testing.T, + providerID string, + output string, +) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/acp/rpc" { + http.NotFound(w, r) + return + } + defer r.Body.Close() + var request map[string]any + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Fatalf("decode request: %v", err) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": request["id"], + "result": map[string]any{ + "success": true, + "output": output, + "turnId": "turn-" + providerID, + "provider": providerID, + "mode": "single-agent", + }, + }) + })) +} + func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) { localAvailableSkills := []map[string]any{ {"id": "pptx", "label": "PPTX", "description": "slides", "installed": true}, @@ -139,19 +170,17 @@ func TestExecuteSessionTaskAutoRoutingRecordsProjectMemory(t *testing.T) { t.Fatalf("create workspace: %v", err) } - fakeProvider := filepath.Join(t.TempDir(), "fake-claude.sh") - if err := os.WriteFile( - fakeProvider, - []byte("#!/bin/sh\nprintf 'done'\n"), - 0o755, - ); err != nil { - t.Fatalf("write fake provider: %v", err) - } - t.Setenv("HOME", homeDir) - t.Setenv("ACP_CLAUDE_BIN", fakeProvider) server := NewServer() + providerServer := newExternalSingleAgentProvider(t, "claude", "done") + defer providerServer.Close() + server.syncProviders([]syncedProvider{{ + ProviderID: "claude", + Label: "Claude", + Endpoint: providerServer.URL, + Enabled: true, + }}) response, rpcErr := server.executeSessionTask(task{ req: shared.RPCRequest{ Params: map[string]any{ @@ -212,19 +241,17 @@ func TestExecuteSessionTaskExplicitRoutingDoesNotRecordProjectMemory(t *testing. t.Fatalf("create workspace: %v", err) } - fakeProvider := filepath.Join(t.TempDir(), "fake-claude.sh") - if err := os.WriteFile( - fakeProvider, - []byte("#!/bin/sh\nprintf 'done'\n"), - 0o755, - ); err != nil { - t.Fatalf("write fake provider: %v", err) - } - t.Setenv("HOME", homeDir) - t.Setenv("ACP_CLAUDE_BIN", fakeProvider) server := NewServer() + providerServer := newExternalSingleAgentProvider(t, "claude", "done") + defer providerServer.Close() + server.syncProviders([]syncedProvider{{ + ProviderID: "claude", + Label: "Claude", + Endpoint: providerServer.URL, + Enabled: true, + }}) response, rpcErr := server.executeSessionTask(task{ req: shared.RPCRequest{ Params: map[string]any{ @@ -270,6 +297,34 @@ func TestExecuteSessionTaskExplicitRoutingDoesNotRecordProjectMemory(t *testing. } } +func TestExecuteSessionTaskExplicitProviderRequiresSyncedCatalog(t *testing.T) { + server := NewServer() + response, rpcErr := server.executeSessionTask(task{ + req: shared.RPCRequest{ + Method: "session.start", + Params: map[string]any{ + "sessionId": "session-explicit-provider", + "threadId": "thread-explicit-provider", + "taskPrompt": "create a powerpoint deck for launch", + "routing": map[string]any{ + "routingMode": "explicit", + "explicitExecutionTarget": "singleAgent", + "explicitProviderId": "claude", + }, + }, + }, + }) + if rpcErr != nil { + t.Fatalf("expected structured unavailable response, got rpc error: %v", rpcErr) + } + if got := response["unavailable"]; got != true { + t.Fatalf("expected unavailable response, got %#v", response) + } + if got := response["unavailableCode"]; got != "PROVIDER_UNAVAILABLE" { + t.Fatalf("expected PROVIDER_UNAVAILABLE, got %#v", response) + } +} + func TestExecuteSessionTaskRequiresRouting(t *testing.T) { server := NewServer() _, rpcErr := server.executeSessionTask(task{ diff --git a/go/go_core/internal/router/router.go b/go/go_core/internal/router/router.go index 9311003d..86828c5c 100644 --- a/go/go_core/internal/router/router.go +++ b/go/go_core/internal/router/router.go @@ -78,8 +78,8 @@ func (r Resolver) Resolve(req Request) Result { availableProviders := normalizeProviders(req.AvailableProviders) result := Result{ - ResolvedModel: strings.TrimSpace(req.ExplicitModel), - MemorySources: mem.Sources, + ResolvedModel: strings.TrimSpace(req.ExplicitModel), + MemorySources: mem.Sources, } result.ResolvedExecutionTarget, result.ResolvedEndpointTarget = r.resolveExecution(req, mem.Preferences) @@ -226,9 +226,6 @@ func resolveProvider( ) (string, bool, string, string) { explicitProviderID := normalize(strings.TrimSpace(req.ExplicitProviderID)) if explicitProviderID != "" { - if len(availableProviders) == 0 { - return explicitProviderID, false, "", "" - } if containsProvider(availableProviders, explicitProviderID) { return explicitProviderID, false, "", "" } diff --git a/go/go_core/internal/router/router_test.go b/go/go_core/internal/router/router_test.go index 94e54b02..46a52d52 100644 --- a/go/go_core/internal/router/router_test.go +++ b/go/go_core/internal/router/router_test.go @@ -26,6 +26,7 @@ func TestResolveExplicitTargetOverridesAuto(t *testing.T) { ExplicitExecutionTarget: "singleAgent", ExplicitProviderID: "codex", ExplicitModel: "gpt-5.4", + AvailableProviders: []string{"codex"}, }) if result.ResolvedExecutionTarget != ExecutionTargetSingleAgent { @@ -39,6 +40,28 @@ func TestResolveExplicitTargetOverridesAuto(t *testing.T) { } } +func TestResolveExplicitProviderRequiresAvailability(t *testing.T) { + resolver := Resolver{ + SkillFinder: skills.StaticFinder{}, + SkillInstaller: nil, + MemoryService: memory.Service{}, + } + + result := resolver.Resolve(Request{ + Prompt: "search the web and summarize results", + RoutingMode: RoutingModeExplicit, + ExplicitExecutionTarget: "singleAgent", + ExplicitProviderID: "codex", + }) + + if !result.Unavailable { + t.Fatalf("expected explicit provider to be unavailable without synced catalog, got %#v", result) + } + if result.UnavailableCode != "PROVIDER_UNAVAILABLE" { + t.Fatalf("expected PROVIDER_UNAVAILABLE, got %#v", result) + } +} + func TestResolveAutoLocalTaskToSingleAgent(t *testing.T) { resolver := Resolver{ SkillFinder: skills.StaticFinder{},