package adapters import ( "reflect" "strings" "testing" "go.uber.org/zap" "google.golang.org/protobuf/types/known/structpb" "iop/packages/go/config" iop "iop/proto/gen/iop" ) func TestProtocolProfileExtensionsRoundTrip(t *testing.T) { profile := mustResolveProfileForAdapter(t, "openai") profile.Extensions = map[string]any{ "nested": map[string]any{ "enabled": true, "items": []any{"one", map[string]any{"leaf": "two"}}, }, } wire := concreteProfileToProtoForTest(&profile) decoded := concreteProfileFromProto(wire) if decoded == nil { t.Fatal("decoded profile is nil") } if !reflect.DeepEqual(decoded.Extensions, profile.Extensions) { t.Fatalf("extensions = %#v, want %#v", decoded.Extensions, profile.Extensions) } profile.Extensions["nested"].(map[string]any)["enabled"] = false if got := decoded.Extensions["nested"].(map[string]any)["enabled"]; got != true { t.Fatalf("source mutation crossed Node boundary: %v", got) } decoded.Extensions["nested"].(map[string]any)["items"].([]any)[1].(map[string]any)["leaf"] = "changed" if got := wire.GetExtensions().AsMap()["nested"].(map[string]any)["items"].([]any)[1].(map[string]any)["leaf"]; got != "two" { t.Fatalf("decoded mutation crossed wire boundary: %v", got) } payload := func(profile *iop.ConcreteProtocolProfile) *iop.NodeConfigPayload { return &iop.NodeConfigPayload{Adapters: []*iop.AdapterConfig{{ Type: "openai_compat", Enabled: true, Name: "ext-adapter", Config: &iop.AdapterConfig_OpenaiCompat{OpenaiCompat: &iop.OpenAICompatAdapterConfig{ProtocolProfile: profile}}, }}} } first, err := BuildConfigSet(payload(wire), zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet first: %v", err) } wireChanged := concreteProfileToProtoForTest(&profile) second, err := BuildConfigSet(payload(wireChanged), zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet second: %v", err) } if first.Items["ext-adapter"].Fingerprint == second.Items["ext-adapter"].Fingerprint { t.Fatal("nested extension change did not change config fingerprint") } } func TestProtocolProfileAdapterBackedConfigFingerprint(t *testing.T) { profile := mustResolveProfileForAdapter(t, "openai") profile.ModelMapping = map[string]string{"canonical": "upstream-a"} payload := func(p config.ConcreteProtocolProfile) *iop.NodeConfigPayload { return &iop.NodeConfigPayload{Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "profile-provider", Config: &iop.AdapterConfig_OpenaiCompat{OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Provider: "lemonade", Endpoint: "http://backing:8000/v1", Headers: map[string]string{"Authorization": "Bearer secret"}, ProtocolProfile: concreteProfileToProtoForTest(&p), }}, }, { Type: "openai_compat", Enabled: true, Name: "legacy-backing", Config: &iop.AdapterConfig_OpenaiCompat{OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Provider: "lemonade", Endpoint: "http://backing:8000/v1", Headers: map[string]string{"Authorization": "Bearer secret"}, }}, }, }} } first, err := BuildConfigSet(payload(profile), zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet first: %v", err) } if len(first.Items) != 2 { t.Fatalf("items = %d, want profile provider plus retained backing", len(first.Items)) } fingerprint := first.Items["profile-provider"].Fingerprint for _, fragment := range []string{"http://backing:8000/v1", "Authorization", "upstream-a"} { if !strings.Contains(fingerprint, fragment) { t.Errorf("profile fingerprint does not contain %q: %s", fragment, fingerprint) } } profile.ModelMapping["canonical"] = "upstream-b" second, err := BuildConfigSet(payload(profile), zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet second: %v", err) } diff := DiffConfigSets(first, second) if !reflect.DeepEqual(diff.Updated, []string{"profile-provider"}) { t.Fatalf("profile-only diff = %+v", diff) } if first.Items["legacy-backing"].Fingerprint != second.Items["legacy-backing"].Fingerprint { t.Fatal("profile change altered retained legacy backing fingerprint") } } // TestProtocolProfileConfigUpdate verifies that a profile change in the config // payload produces a different fingerprint, triggering a config update. func TestProtocolProfileConfigUpdate(t *testing.T) { profile := mustResolveProfileForAdapter(t, "openai") payload1 := &iop.NodeConfigPayload{ Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "my-adapter", Config: &iop.AdapterConfig_OpenaiCompat{ OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Provider: "openai_api", Endpoint: "https://api.openai.com/v1", Capacity: 4, ProtocolProfile: concreteProfileToProtoForTest(&profile), }, }, }, }, } set1, err := BuildConfigSet(payload1, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet 1: %v", err) } // Change the profile to anthropic. anthropicProfile := mustResolveProfileForAdapter(t, "anthropic") payload2 := &iop.NodeConfigPayload{ Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "my-adapter", Config: &iop.AdapterConfig_OpenaiCompat{ OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Provider: "openai_api", Endpoint: "https://api.openai.com/v1", Capacity: 4, ProtocolProfile: concreteProfileToProtoForTest(&anthropicProfile), }, }, }, }, } set2, err := BuildConfigSet(payload2, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet 2: %v", err) } diff := DiffConfigSets(set1, set2) if len(diff.Updated) != 1 || diff.Updated[0] != "my-adapter" { t.Fatalf("expected my-adapter to be updated, got diff: %+v", diff) } } // TestProtocolProfileConfigInheritedStateAbsent verifies that the concrete // profile snapshot does not carry inherited Base state into the adapter config. func TestProtocolProfileConfigInheritedStateAbsent(t *testing.T) { profile := mustResolveProfileForAdapter(t, "openai") payload := &iop.NodeConfigPayload{ Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "my-adapter", Config: &iop.AdapterConfig_OpenaiCompat{ OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Provider: "openai_api", Endpoint: "https://api.openai.com/v1", ProtocolProfile: concreteProfileToProtoForTest(&profile), }, }, }, }, } set, err := BuildConfigSet(payload, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet: %v", err) } item, ok := set.Items["my-adapter"] if !ok { t.Fatal("expected my-adapter in config set") } if item.Type != "openai_compat" { t.Errorf("Type = %q, want %q", item.Type, "openai_compat") } if item.Fingerprint == "" { t.Error("expected non-empty fingerprint") } } // TestProtocolProfileConfigNilProfile verifies that a payload without a profile // (legacy) still builds a config set with a valid fingerprint. func TestProtocolProfileConfigNilProfile(t *testing.T) { payload := &iop.NodeConfigPayload{ Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "legacy-adapter", Config: &iop.AdapterConfig_OpenaiCompat{ OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Provider: "openai_api", Endpoint: "https://api.openai.com/v1", Capacity: 4, // ProtocolProfile is nil (legacy payload) }, }, }, }, } set, err := BuildConfigSet(payload, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet: %v", err) } item, ok := set.Items["legacy-adapter"] if !ok { t.Fatal("expected legacy-adapter in config set") } if item.Fingerprint == "" { t.Error("expected non-empty fingerprint for legacy config") } } // TestProtocolProfileConfigFingerprintIncludesProfile verifies that the // fingerprint includes the profile data so that profile changes are detected. func TestProtocolProfileConfigFingerprintIncludesProfile(t *testing.T) { profile1 := mustResolveProfileForAdapter(t, "openai") profile2 := mustResolveProfileForAdapter(t, "anthropic") payload1 := &iop.NodeConfigPayload{ Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "fp-adapter", Config: &iop.AdapterConfig_OpenaiCompat{ OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Endpoint: "https://api.openai.com/v1", ProtocolProfile: concreteProfileToProtoForTest(&profile1), }, }, }, }, } payload2 := &iop.NodeConfigPayload{ Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "fp-adapter", Config: &iop.AdapterConfig_OpenaiCompat{ OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Endpoint: "https://api.openai.com/v1", ProtocolProfile: concreteProfileToProtoForTest(&profile2), }, }, }, }, } set1, err := BuildConfigSet(payload1, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet 1: %v", err) } set2, err := BuildConfigSet(payload2, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet 2: %v", err) } fp1 := set1.Items["fp-adapter"].Fingerprint fp2 := set2.Items["fp-adapter"].Fingerprint if fp1 == fp2 { t.Error("expected different fingerprints for different profiles") } } // TestProtocolProfileConfigDiffUnchanged verifies that identical profile // payloads produce no diff. func TestProtocolProfileConfigDiffUnchanged(t *testing.T) { profile := mustResolveProfileForAdapter(t, "openai") payload := &iop.NodeConfigPayload{ Adapters: []*iop.AdapterConfig{ { Type: "openai_compat", Enabled: true, Name: "same-adapter", Config: &iop.AdapterConfig_OpenaiCompat{ OpenaiCompat: &iop.OpenAICompatAdapterConfig{ Endpoint: "https://api.openai.com/v1", ProtocolProfile: concreteProfileToProtoForTest(&profile), }, }, }, }, } set1, err := BuildConfigSet(payload, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet 1: %v", err) } set2, err := BuildConfigSet(payload, zap.NewNop()) if err != nil { t.Fatalf("BuildConfigSet 2: %v", err) } diff := DiffConfigSets(set1, set2) if len(diff.Updated) != 0 { t.Errorf("expected no updated items, got %v", diff.Updated) } if len(diff.Added) != 0 { t.Errorf("expected no added items, got %v", diff.Added) } } // concreteProfileToProtoForTest converts a config.ConcreteProtocolProfile to // its wire representation for test payloads. func concreteProfileToProtoForTest(p *config.ConcreteProtocolProfile) *iop.ConcreteProtocolProfile { if p == nil { return nil } cp := p.Clone() proto := &iop.ConcreteProtocolProfile{ Id: cp.ID, Driver: string(cp.Driver), BaseUrl: cp.BaseURL, Auth: &iop.ProtocolAuth{Header: cp.Auth.Header, Scheme: cp.Auth.Scheme}, Capabilities: append([]string(nil), cp.Capabilities...), } if len(cp.Operations) > 0 { proto.Operations = make(map[string]string, len(cp.Operations)) for k, v := range cp.Operations { proto.Operations[k] = v } } if len(cp.ModelMapping) > 0 { proto.ModelMapping = make(map[string]string, len(cp.ModelMapping)) for k, v := range cp.ModelMapping { proto.ModelMapping[k] = v } } if len(cp.Extensions) > 0 { if s, err := structpb.NewStruct(cp.Extensions); err == nil { proto.Extensions = s } } return proto } // mustResolveProfileForAdapter resolves a built-in profile for use in adapter // config tests. func mustResolveProfileForAdapter(t *testing.T, id string) config.ConcreteProtocolProfile { t.Helper() profile, err := config.ResolveProtocolProfile(id, "", config.BuiltInProtocolProfiles) if err != nil { t.Fatalf("ResolveProtocolProfile(%q): %v", id, err) } return profile }