diff --git a/pkg/services/agent_projects.go b/pkg/services/agent_projects.go index 9cb4f99..6d34d00 100644 --- a/pkg/services/agent_projects.go +++ b/pkg/services/agent_projects.go @@ -242,7 +242,9 @@ func (svc *AgentProjectService) Delete(id string) error { // Import imports an agent project bundle into the platform. // conflictMode controls how a collision with an existing project is handled ("keep-both" or "replace"). -// providerResolutions is sent as a map of agent UUID to nil, since no provider profiles are resolved by ipctl. +// providerResolutions maps each agent UUID to the profileName and modelName from that agent's own +// provider in the bundle, so the destination is expected to have a profile with the same name. +// Agents without a complete provider are sent as nil and the server chooses. func (svc *AgentProjectService) Import(bundle AgentProjectBundle, conflictMode string) (*AgentProjectBundle, error) { logging.Trace() @@ -264,7 +266,7 @@ func (svc *AgentProjectService) Import(bundle AgentProjectBundle, conflictMode s continue } - providerResolutions[uuid] = nil + providerResolutions[uuid] = agentProviderResolution(agent) } body := map[string]interface{}{ @@ -288,6 +290,27 @@ func (svc *AgentProjectService) Import(bundle AgentProjectBundle, conflictMode s return &res.Data, nil } +// agentProviderResolution returns the providerResolutions entry for an agent, built from the +// agent's own provider. It returns nil when the provider or either of its names is missing. +func agentProviderResolution(agent map[string]interface{}) interface{} { + provider, ok := agent["provider"].(map[string]interface{}) + if !ok { + return nil + } + + profileName, _ := provider["profileName"].(string) + modelName, _ := provider["modelName"].(string) + + if profileName == "" || modelName == "" { + return nil + } + + return map[string]interface{}{ + "profileName": profileName, + "modelName": modelName, + } +} + // Create creates a new agent project with the specified name and description. func (svc *AgentProjectService) Create(name string, description string) (*AgentProject, error) { logging.Trace() diff --git a/pkg/services/agent_projects_test.go b/pkg/services/agent_projects_test.go index 0610170..3dbf499 100644 --- a/pkg/services/agent_projects_test.go +++ b/pkg/services/agent_projects_test.go @@ -5,7 +5,9 @@ package services import ( + "encoding/json" "fmt" + "io" "net/http" "path/filepath" "testing" @@ -225,6 +227,54 @@ func TestAgentProjectService_Import_Error(t *testing.T) { assert.Nil(t, res) } +func TestAgentProjectService_Import_ProviderResolutions(t *testing.T) { + svc := setupAgentProjectService() + defer testlib.Teardown() + + var got map[string]interface{} + + testlib.AddHandlerToMux("/agent-project-service/project-bundles/import", func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(b, &got) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"message":"ok","data":{"_id":"new-id","name":"p"}}`)) + }) + + bundle := AgentProjectBundle{ + Name: "p", + Agents: []map[string]interface{}{ + { + "_id": "agent-with-provider", + "provider": map[string]interface{}{"profileName": "anthropic", "modelName": "claude-sonnet-4-6"}, + }, + { + "_id": "agent-model-only", + "provider": map[string]interface{}{"modelName": "claude-sonnet-4-6"}, + }, + {"_id": "agent-no-provider"}, + { + "uuid": "agent-uuid-key", + "provider": map[string]interface{}{"profileName": "openai", "modelName": "gpt-5"}, + }, + }, + } + + _, err := svc.Import(bundle, "keep-both") + assert.Nil(t, err) + + resolutions, ok := got["providerResolutions"].(map[string]interface{}) + assert.True(t, ok) + assert.Len(t, resolutions, 4) + + assert.Equal(t, map[string]interface{}{"profileName": "anthropic", "modelName": "claude-sonnet-4-6"}, resolutions["agent-with-provider"]) + assert.Equal(t, map[string]interface{}{"profileName": "openai", "modelName": "gpt-5"}, resolutions["agent-uuid-key"]) + + assert.Contains(t, resolutions, "agent-model-only") + assert.Nil(t, resolutions["agent-model-only"]) + assert.Contains(t, resolutions, "agent-no-provider") + assert.Nil(t, resolutions["agent-no-provider"]) +} + func TestAgentProjectService_Create(t *testing.T) { svc := setupAgentProjectService() defer testlib.Teardown()