iop/apps/edge/internal/service/protocol_profile_test.go
toki f2306f4dc8 feat(protocol-profile): 멀티 프로토콜 프로필을 추가한다
OpenAI 호환 및 Anthropic Messages 경로의 프로토콜 프로필, 라우팅 계약, 테스트와 실행 증거를 함께 반영한다.
2026-08-01 10:33:10 +09:00

682 lines
26 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
edgeevents "iop/apps/edge/internal/events"
edgenode "iop/apps/edge/internal/node"
"iop/packages/go/config"
)
// TestProtocolProfileCandidateCopy verifies that the concrete profile is
// deep-copied onto each provider candidate and that profileFacts extracts
// the correct id and driver.
func TestProtocolProfileCandidateCopy(t *testing.T) {
profile := mustResolveProfileForService(t, "openai")
rec := &edgenode.NodeRecord{
ID: "node-profile-copy",
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
},
},
Providers: []config.NodeProviderConf{{
ID: "prov-copy",
Type: "openai_api",
Adapter: "vllm-gpu",
Models: []string{"gpt-4"},
Health: "available",
Capacity: 4,
Profile: "openai",
RuntimeProfile: &profile,
}},
}
store := edgenode.NewNodeStore()
store.Add(rec)
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-profile-copy"})
svc := New(reg, edgeevents.NewBus())
svc.SetRuntimeConfig(store, []config.ModelCatalogEntry{{
ID: "gpt-4",
Providers: map[string]string{"prov-copy": "gpt-4"},
}}, groupPolicy{})
cands, _, err := svc.resolveProviderPoolCandidates(SubmitRunRequest{
ModelGroupKey: "gpt-4", ProviderPool: true,
}, store, []config.ModelCatalogEntry{{
ID: "gpt-4",
Providers: map[string]string{"prov-copy": "gpt-4"},
}})
if err != nil {
t.Fatalf("resolveProviderPoolCandidates: %v", err)
}
if len(cands) != 1 {
t.Fatalf("expected 1 candidate, got %d", len(cands))
}
if cands[0].adapter != "prov-copy" {
t.Fatalf("profile-backed dispatch adapter = %q, want provider id", cands[0].adapter)
}
c := cands[0]
if c.profile == nil {
t.Fatal("expected non-nil profile on candidate")
}
if c.profile.ID != "openai" {
t.Errorf("profile.ID = %q, want %q", c.profile.ID, "openai")
}
if string(c.profile.Driver) != string(config.ProtocolDriverOpenAIChat) {
t.Errorf("profile.Driver = %q, want %q", c.profile.Driver, config.ProtocolDriverOpenAIChat)
}
if c.profile.BaseURL != "https://api.openai.com/v1" {
t.Errorf("profile.BaseURL = %q, want %q", c.profile.BaseURL, "https://api.openai.com/v1")
}
profile.Operations[string(config.OperationChatCompletions)] = "/mutated"
profile.Capabilities[0] = "mutated"
if c.profile.Operations[string(config.OperationChatCompletions)] == "/mutated" || c.profile.Capabilities[0] == "mutated" {
t.Fatal("source profile mutation crossed candidate snapshot boundary")
}
// Verify profileFacts extracts the correct values.
id, driver := profileFacts(c.profile)
if id != "openai" {
t.Errorf("profileFacts id = %q, want %q", id, "openai")
}
if driver != string(config.ProtocolDriverOpenAIChat) {
t.Errorf("profileFacts driver = %q, want %q", driver, config.ProtocolDriverOpenAIChat)
}
// Verify profileFacts returns empty for nil profile (legacy candidate).
id2, driver2 := profileFacts(nil)
if id2 != "" || driver2 != "" {
t.Errorf("profileFacts(nil) = (%q, %q), want empty", id2, driver2)
}
}
func TestProtocolProfileAdapterDispatchUsesProviderID(t *testing.T) {
profile := mustResolveProfileForService(t, "openai")
provider := config.NodeProviderConf{
ID: "provider-dispatch", Type: "openai_compat", Adapter: "backing", Models: []string{"served"},
Health: "available", Capacity: 1, RuntimeProfile: &profile,
}
makeService := func(p config.NodeProviderConf) (*Service, *edgenode.NodeStore, []config.ModelCatalogEntry) {
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{ID: "node-dispatch", Adapters: config.AdaptersConf{
OpenAICompatInstances: []config.OpenAICompatInstanceConf{{Name: "backing", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"}},
}, Providers: []config.NodeProviderConf{p}})
catalog := []config.ModelCatalogEntry{{ID: "model", Providers: map[string]string{p.ID: "served"}}}
registry := edgenode.NewRegistry()
registry.Register(&edgenode.NodeEntry{NodeID: "node-dispatch"})
service := New(registry, nil)
return service, store, catalog
}
service, store, catalog := makeService(provider)
candidates, _, err := service.resolveProviderPoolCandidates(SubmitRunRequest{ModelGroupKey: "model", ProviderPool: true}, store, catalog)
if err != nil || len(candidates) != 1 || candidates[0].adapter != "provider-dispatch" {
t.Fatalf("profile dispatch candidates=%+v err=%v", candidates, err)
}
provider.RuntimeProfile = nil
service, store, catalog = makeService(provider)
candidates, _, err = service.resolveProviderPoolCandidates(SubmitRunRequest{ModelGroupKey: "model", ProviderPool: true}, store, catalog)
if err != nil || len(candidates) != 1 || candidates[0].adapter != "backing" {
t.Fatalf("legacy dispatch candidates=%+v err=%v", candidates, err)
}
}
// TestProtocolProfileCandidateCallback verifies that the
// ProviderPoolCandidatePredicate receives the concrete profile facts and can
// reject candidates based on them.
func TestProtocolProfileCandidateCallback(t *testing.T) {
profile := mustResolveProfileForService(t, "anthropic")
rec := &edgenode.NodeRecord{
ID: "node-callback",
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
},
},
Providers: []config.NodeProviderConf{{
ID: "prov-cb",
Type: "openai_api",
Adapter: "vllm-gpu",
Models: []string{"claude-3"},
Health: "available",
Capacity: 4,
Profile: "anthropic",
RuntimeProfile: &profile,
}},
}
store := edgenode.NewNodeStore()
store.Add(rec)
catalog := []config.ModelCatalogEntry{{
ID: "claude-3",
Providers: map[string]string{"prov-cb": "claude-3"},
}}
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-callback"})
svc := New(reg, nil)
svc.SetRuntimeConfig(store, catalog, groupPolicy{})
cands, _, err := svc.resolveProviderPoolCandidates(SubmitRunRequest{
ModelGroupKey: "claude-3", ProviderPool: true,
}, store, catalog)
if err != nil {
t.Fatalf("resolveProviderPoolCandidates: %v", err)
}
if len(cands) != 1 {
t.Fatalf("expected 1 candidate, got %d", len(cands))
}
// Accept predicate that only accepts anthropic_messages driver.
accept := func(c ProviderPoolCandidate) bool {
return c.ProfileDriver == string(config.ProtocolDriverAnthropicMessages)
}
accepted, rejected := filterProviderPoolCandidates(cands, accept)
if rejected {
t.Fatal("expected candidate to be accepted")
}
if len(accepted) != 1 {
t.Fatalf("expected 1 accepted candidate, got %d", len(accepted))
}
if accepted[0].profile.ID != "anthropic" {
t.Errorf("accepted profile ID = %q, want %q", accepted[0].profile.ID, "anthropic")
}
// Reject predicate that only accepts openai_chat driver.
reject := func(c ProviderPoolCandidate) bool {
return c.ProfileDriver == string(config.ProtocolDriverOpenAIChat)
}
accepted2, rejected2 := filterProviderPoolCandidates(cands, reject)
if !rejected2 {
t.Fatal("expected candidate to be rejected")
}
if len(accepted2) != 0 {
t.Fatalf("expected 0 accepted candidates, got %d", len(accepted2))
}
}
// TestProtocolProfileReResolution verifies that the concrete profile is
// refreshed from NodeProviderConf when the store is updated via SetRuntimeConfig.
func TestProtocolProfileReResolution(t *testing.T) {
openaiProfile := mustResolveProfileForService(t, "openai")
anthropicProfile := mustResolveProfileForService(t, "anthropic")
newStore := func(profile *config.ConcreteProtocolProfile, profileID string) *edgenode.NodeStore {
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-rez",
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
},
},
Providers: []config.NodeProviderConf{{
ID: "prov-rez",
Type: "openai_api",
Adapter: "vllm-gpu",
Models: []string{"model-a"},
Health: "available",
Capacity: 4,
Profile: profileID,
RuntimeProfile: profile,
}},
})
return store
}
catalog := []config.ModelCatalogEntry{{
ID: "model-a",
Providers: map[string]string{"prov-rez": "model-a"},
}}
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-rez"})
svc := New(reg, edgeevents.NewBus())
svc.SetRuntimeConfig(newStore(&openaiProfile, "openai"), catalog, groupPolicy{})
store, _, _ := svc.runtimeConfigSnapshot()
cands, _, err := svc.resolveProviderPoolCandidates(SubmitRunRequest{
ModelGroupKey: "model-a", ProviderPool: true,
}, store, catalog)
if err != nil {
t.Fatalf("resolve before refresh: %v", err)
}
if len(cands) != 1 || cands[0].profile.ID != "openai" {
t.Fatalf("before refresh: expected profile ID openai, got %+v", cands)
}
queuedSnapshot := cands[0]
// Refresh: swap to anthropic profile.
svc.SetRuntimeConfig(newStore(&anthropicProfile, "anthropic"), catalog, groupPolicy{})
svc.queue.mu.Lock()
liveCandidate, eligible := svc.queue.liveCandidateLocked(&queuedSnapshot)
svc.queue.mu.Unlock()
if !eligible || liveCandidate.profile == nil || liveCandidate.profile.ID != "anthropic" {
t.Fatalf("queued refresh did not clone the live profile: eligible=%v candidate=%+v", eligible, liveCandidate)
}
if queuedSnapshot.profile == nil || queuedSnapshot.profile.ID != "openai" {
t.Fatalf("queued snapshot changed across refresh: %+v", queuedSnapshot.profile)
}
liveCandidate.profile.Operations[string(config.OperationMessages)] = "/mutated-live"
refreshedStore, _, _ := svc.runtimeConfigSnapshot()
refreshedRecord, _ := refreshedStore.FindByID("node-rez")
if got := refreshedRecord.Providers[0].RuntimeProfile.Operations[string(config.OperationMessages)]; got == "/mutated-live" {
t.Fatal("live candidate mutation crossed refreshed store boundary")
}
store, _, _ = svc.runtimeConfigSnapshot()
cands, _, err = svc.resolveProviderPoolCandidates(SubmitRunRequest{
ModelGroupKey: "model-a", ProviderPool: true,
}, store, catalog)
if err != nil {
t.Fatalf("resolve after refresh: %v", err)
}
if len(cands) != 1 || cands[0].profile.ID != "anthropic" {
t.Fatalf("after refresh: expected profile ID anthropic, got %+v", cands)
}
if string(cands[0].profile.Driver) != string(config.ProtocolDriverAnthropicMessages) {
t.Errorf("after refresh: expected driver anthropic_messages, got %q", cands[0].profile.Driver)
}
}
// TestProtocolProfileOperationWirePropagation verifies that the operation field
// is propagated through the wire request builder.
func TestProtocolProfileOperationWirePropagation(t *testing.T) {
tunnelReq := SubmitProviderTunnelRequest{
RunID: "run-op-wire",
Adapter: "openai_compat",
Target: "gpt-4",
Method: "POST",
Operation: string(config.OperationChatCompletions),
Path: "/v1/chat/completions",
Body: []byte(`{"model":"gpt-4","messages":[]}`),
Headers: map[string]string{"Authorization": "Bearer key"},
}
wire, runID, err := buildProviderTunnelRequest(tunnelReq, "openai_compat", "gpt-4")
if err != nil {
t.Fatalf("buildProviderTunnelRequest: %v", err)
}
if wire.GetOperation() != string(config.OperationChatCompletions) {
t.Errorf("wire Operation = %q, want %q", wire.GetOperation(), config.OperationChatCompletions)
}
if wire.GetPath() != "/v1/chat/completions" {
t.Errorf("wire Path = %q, want %q", wire.GetPath(), "/v1/chat/completions")
}
if runID != "run-op-wire" {
t.Errorf("runID = %q, want %q", runID, "run-op-wire")
}
}
// TestProtocolProfileLegacyPathFallback verifies that when no profile is
// present (legacy candidate), the Path field is still carried through.
func TestProtocolProfileLegacyPathFallback(t *testing.T) {
tunnelReq := SubmitProviderTunnelRequest{
RunID: "run-legacy-wire",
Adapter: "openai_compat",
Target: "gpt-4",
Method: "POST",
Path: "/v1/chat/completions",
Body: []byte(`{"model":"gpt-4"}`),
}
wire, _, err := buildProviderTunnelRequest(tunnelReq, "openai_compat", "gpt-4")
if err != nil {
t.Fatalf("buildProviderTunnelRequest: %v", err)
}
if wire.GetPath() != "/v1/chat/completions" {
t.Errorf("wire Path = %q, want %q", wire.GetPath(), "/v1/chat/completions")
}
if wire.GetOperation() != "" {
t.Errorf("wire Operation = %q, want empty for legacy", wire.GetOperation())
}
}
// TestProtocolProfileUnsupportedProfileAdmission verifies that candidates
// with nil profiles (legacy) are still eligible for dispatch and that the
// filter does not panic when profile is nil.
func TestProtocolProfileUnsupportedProfileAdmission(t *testing.T) {
entry := &edgenode.NodeEntry{NodeID: "node-nil-profile"}
cands := []candidateNode{{
entry: entry,
capacity: 4,
providerID: "prov-nil",
servedTarget: "model-x",
// profile is nil (legacy candidate)
}}
// An accept predicate that checks profile facts should get empty strings
// for nil profiles and still be able to accept them.
accept := func(c ProviderPoolCandidate) bool {
// Legacy candidates have empty profile facts but are still valid.
return c.ProfileID == "" && c.ProviderID == "prov-nil"
}
accepted, rejected := filterProviderPoolCandidates(cands, accept)
if rejected {
t.Fatal("expected legacy candidate with nil profile to be accepted")
}
if len(accepted) != 1 {
t.Fatalf("expected 1 accepted candidate, got %d", len(accepted))
}
}
func TestProtocolProfileOperationAdmissionFiltersUnsupportedCandidate(t *testing.T) {
env := newProviderTunnelTestEnv(t)
store, catalog, policy := env.svc.runtimeConfigSnapshot()
record, _ := store.FindByID("node-pool")
gemini := mustResolveProfileForService(t, "gemini")
openai := mustResolveProfileForService(t, "openai")
record.Providers[0].RuntimeProfile = &gemini
record.Providers[0].Profile = "gemini"
record.Providers[0].Priority = 0
record.Providers = append(record.Providers, config.NodeProviderConf{
ID: "prov-responses", Type: "openai_api", Adapter: "vllm-gpu", Models: []string{"served-responses"},
Health: "available", Capacity: 1, Priority: 10, Profile: "openai", RuntimeProfile: &openai,
})
catalog[0].Providers["prov-responses"] = "served-responses"
env.svc.SetRuntimeConfig(store, catalog, policy)
result, err := env.svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{Method: "POST", Operation: string(config.OperationResponses), Path: "/responses"},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
defer result.Tunnel.Close()
if result.DispatchInfo.ProviderID != "prov-responses" {
t.Fatalf("selected provider = %q, want operation-compatible provider", result.DispatchInfo.ProviderID)
}
}
func TestProtocolProfileOperationAdmissionRejectsUnsupportedCandidate(t *testing.T) {
env := newProviderTunnelTestEnv(t)
store, catalog, policy := env.svc.runtimeConfigSnapshot()
record, _ := store.FindByID("node-pool")
anthropic := mustResolveProfileForService(t, "anthropic")
record.Providers[0].Profile = "anthropic"
record.Providers[0].RuntimeProfile = &anthropic
env.svc.SetRuntimeConfig(store, catalog, policy)
_, submitErr := env.svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{Method: "POST", Operation: string(config.OperationResponses), Path: "/responses"},
})
if submitErr == nil {
t.Fatal("expected SubmitProviderPool error for unsupported operation, got nil")
}
var opErr *ProviderPoolOperationUnsupportedError
if !errors.As(submitErr, &opErr) {
t.Fatalf("expected ProviderPoolOperationUnsupportedError, got %T (%v)", submitErr, submitErr)
}
if opErr.Operation != string(config.OperationResponses) {
t.Fatalf("opErr.Operation = %q, want %q", opErr.Operation, config.OperationResponses)
}
if !errors.Is(submitErr, ErrProviderPoolCandidateRejected) {
t.Fatalf("errors.Is(submitErr, ErrProviderPoolCandidateRejected) = false, want true")
}
if env.capturedRequest() != nil {
t.Fatal("unsupported operation must not send any Node wire request")
}
if inflightRunCount(env.svc.queue) != 0 {
t.Fatal("unsupported operation must leave zero stranded queue admission")
}
}
func TestProtocolProfileOperationAdmissionNilProfileCompatibility(t *testing.T) {
env := newProviderTunnelTestEnv(t)
result, err := env.svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{Method: "POST", Operation: string(config.OperationMessages), Path: "/messages"},
})
if err != nil {
t.Fatalf("legacy nil-profile candidate was rejected: %v", err)
}
defer result.Tunnel.Close()
if result.DispatchInfo.ProviderID != "prov-vllm-01" {
t.Fatalf("selected provider = %q", result.DispatchInfo.ProviderID)
}
}
func TestProtocolProfileOperationAdmissionReResolution(t *testing.T) {
env := newProviderTunnelTestEnv(t)
store, catalog, policy := env.svc.runtimeConfigSnapshot()
record, _ := store.FindByID("node-pool")
openai := mustResolveProfileForService(t, "openai")
record.Providers[0].Profile = "openai"
record.Providers[0].RuntimeProfile = &openai
record.Providers[0].Capacity = 1
env.svc.SetRuntimeConfig(store, catalog, policy)
candidates, _, err := env.svc.resolveQueueCandidates(SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true})
if err != nil {
t.Fatalf("resolve holder: %v", err)
}
holder, err := env.svc.queue.admit(t.Context(), "qwen3.6:35b", "", "", candidates, groupPolicy{}, nil, false, true)
if err != nil {
t.Fatalf("reserve holder: %v", err)
}
resultCh := make(chan error, 1)
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
go func() {
_, submitErr := env.svc.SubmitProviderPool(ctx, ProviderPoolDispatchRequest{
Run: SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{Method: "POST", Operation: string(config.OperationResponses), Path: "/responses"},
})
resultCh <- submitErr
}()
requireProviderPoolPending(t, env.svc.queue, 1)
refreshed := edgenode.NewNodeStore()
copyRecord := *record
copyRecord.Providers = append([]config.NodeProviderConf(nil), record.Providers...)
anthropic := mustResolveProfileForService(t, "anthropic")
copyRecord.Providers[0].Profile = "anthropic"
copyRecord.Providers[0].RuntimeProfile = &anthropic
refreshed.Add(&copyRecord)
env.svc.SetRuntimeConfig(refreshed, catalog, policy)
env.svc.queue.releaseLease(holder.leaseID, "test-release")
submitErr := <-resultCh
if submitErr == nil {
t.Fatal("expected queued re-resolution error, got nil")
}
var opErr *ProviderPoolOperationUnsupportedError
if !errors.As(submitErr, &opErr) {
t.Fatalf("queued re-resolution error = %T (%v), want ProviderPoolOperationUnsupportedError", submitErr, submitErr)
}
if opErr.Operation != string(config.OperationResponses) {
t.Fatalf("queued re-resolution opErr.Operation = %q, want %q", opErr.Operation, config.OperationResponses)
}
if !errors.Is(submitErr, ErrProviderPoolCandidateRejected) {
t.Fatalf("queued re-resolution error = %v, want candidate rejection sentinel compatibility", submitErr)
}
if env.capturedRequest() != nil {
t.Fatal("re-resolution rejection must not send any Node wire request")
}
}
// TestProtocolProfileCandidateFactsExtraction verifies that profileFacts
// correctly extracts id and driver from a concrete profile and returns
// empty strings for nil.
func TestProtocolProfileCandidateFactsExtraction(t *testing.T) {
profile := mustResolveProfileForService(t, "gemini")
id, driver := profileFacts(&profile)
if id != "gemini" {
t.Errorf("id = %q, want %q", id, "gemini")
}
if driver != string(config.ProtocolDriverOpenAIChat) {
t.Errorf("driver = %q, want %q", driver, config.ProtocolDriverOpenAIChat)
}
// Nil profile returns empty.
id2, driver2 := profileFacts(nil)
if id2 != "" || driver2 != "" {
t.Errorf("profileFacts(nil) = (%q, %q), want empty", id2, driver2)
}
}
// TestProtocolProfileCandidateRejectedByPredicate verifies that
// ErrProviderPoolCandidateRejected is returned when all candidates are
// rejected by the accept predicate, and that the error is correctly typed.
func TestProtocolProfileCandidateRejectedByPredicate(t *testing.T) {
profile := mustResolveProfileForService(t, "openai")
entry := &edgenode.NodeEntry{NodeID: "node-reject"}
cands := []candidateNode{{
entry: entry,
capacity: 4,
providerID: "prov-reject",
servedTarget: "model-y",
profile: &profile,
}}
// Reject everything.
accept := func(c ProviderPoolCandidate) bool { return false }
accepted, rejected := filterProviderPoolCandidates(cands, accept)
if !rejected {
t.Fatal("expected rejection when all candidates are rejected")
}
if len(accepted) != 0 {
t.Fatalf("expected 0 accepted, got %d", len(accepted))
}
}
// TestProtocolProfileCandidatePredicateNil verifies that a nil predicate
// passes all candidates through.
func TestProtocolProfileCandidatePredicateNil(t *testing.T) {
profile := mustResolveProfileForService(t, "openai")
entry := &edgenode.NodeEntry{NodeID: "node-nil-pred"}
cands := []candidateNode{{
entry: entry,
capacity: 4,
providerID: "prov-nil-pred",
servedTarget: "model-z",
profile: &profile,
}}
accepted, rejected := filterProviderPoolCandidates(cands, nil)
if rejected {
t.Fatal("expected no rejection with nil predicate")
}
if len(accepted) != 1 {
t.Fatalf("expected 1 accepted candidate with nil predicate, got %d", len(accepted))
}
}
// mustResolveProfileForService resolves a built-in profile for use in service
// tests.
func mustResolveProfileForService(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
}
// TestProtocolProfileSubmitProviderPoolTunnelOperation verifies that the
// operation field survives the SubmitProviderTunnel path and reaches the
// wire request.
func TestProtocolProfileSubmitProviderPoolTunnelOperation(t *testing.T) {
newProfileEnv := func(t *testing.T) *providerTunnelTestEnv {
t.Helper()
env := newProviderTunnelTestEnv(t)
store, catalog, policy := env.svc.runtimeConfigSnapshot()
record, ok := store.FindByID("node-pool")
if !ok {
t.Fatal("node-pool not found")
}
profile := mustResolveProfileForService(t, "openai")
profile.Extensions = map[string]any{"nested": map[string]any{"revision": "one"}}
record.Providers[0].Type = "ollama" // profile driver must override legacy type classification.
record.Providers[0].Profile = "openai"
record.Providers[0].RuntimeProfile = &profile
env.svc.SetRuntimeConfig(store, catalog, policy)
return env
}
t.Run("protocol_hook_selected_snapshot", func(t *testing.T) {
env := newProfileEnv(t)
protocolCalls := 0
legacyCalls := 0
result, err := env.svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "qwen3.6:35b", ProviderID: "not-the-selected-provider", ProviderPool: true,
},
Tunnel: SubmitProviderTunnelRequest{Method: "POST", Path: "/v1/chat/completions"},
PrepareProtocolTunnel: func(req SubmitProviderTunnelRequest, selected ProviderPoolCandidate) (SubmitProviderTunnelRequest, error) {
protocolCalls++
if req.ProviderID != "prov-vllm-01" || selected.ProviderID != "prov-vllm-01" {
t.Fatalf("selected provider mismatch: req=%q candidate=%q", req.ProviderID, selected.ProviderID)
}
if selected.ProfileID != "openai" || selected.ProfileDriver != string(config.ProtocolDriverOpenAIChat) || selected.ProtocolProfile == nil {
t.Fatalf("selected profile snapshot = %+v", selected)
}
selected.ProtocolProfile.Operations[string(config.OperationChatCompletions)] = "/callback-mutated"
req.Operation = string(config.OperationChatCompletions)
return req, nil
},
PrepareTunnel: func(req SubmitProviderTunnelRequest) (SubmitProviderTunnelRequest, error) {
legacyCalls++
return req, nil
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
defer result.Tunnel.Close()
if protocolCalls != 1 || legacyCalls != 0 {
t.Fatalf("protocol calls=%d legacy calls=%d", protocolCalls, legacyCalls)
}
waitForCondition(t, func() bool { return env.capturedRequest() != nil }, "fake node did not receive protocol tunnel")
wire := env.capturedRequest()
if wire.GetOperation() != string(config.OperationChatCompletions) {
t.Fatalf("wire operation = %q", wire.GetOperation())
}
dispatch := result.DispatchInfo
if dispatch.ProfileID != "openai" || dispatch.ProfileDriver != string(config.ProtocolDriverOpenAIChat) || dispatch.ExecutionPath != string(providerExecutionPathTunnel) {
t.Fatalf("dispatch profile facts = %+v", dispatch)
}
if got := result.Tunnel.Dispatch().ProfileCapabilities; len(got) == 0 {
t.Fatal("tunnel handle lost profile capabilities")
}
})
t.Run("legacy_hook_compatibility", func(t *testing.T) {
env := newProfileEnv(t)
legacyCalls := 0
result, err := env.svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true},
Tunnel: SubmitProviderTunnelRequest{Method: "POST", Path: "/v1/chat/completions"},
PrepareTunnel: func(req SubmitProviderTunnelRequest) (SubmitProviderTunnelRequest, error) {
legacyCalls++
req.Operation = string(config.OperationChatCompletions)
return req, nil
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
defer result.Tunnel.Close()
waitForCondition(t, func() bool { return env.capturedRequest() != nil }, "fake node did not receive legacy tunnel")
if legacyCalls != 1 || env.capturedRequest().GetOperation() != string(config.OperationChatCompletions) {
t.Fatalf("legacy hook calls=%d operation=%q", legacyCalls, env.capturedRequest().GetOperation())
}
waitForCondition(t, func() bool { return inflightRunCount(env.svc.queue) == 1 }, "tunnel admission was not tracked")
result.Tunnel.Close()
waitForCondition(t, func() bool { return inflightRunCount(env.svc.queue) == 0 }, "legacy tunnel admission did not release")
})
}