iop/apps/edge/internal/service/run_dispatch_internal_test.go

926 lines
29 KiB
Go

package service
import (
"context"
"net"
"strings"
"sync"
"testing"
"time"
toki "git.toki-labs.com/toki/proto-socket/go"
"google.golang.org/protobuf/proto"
edgeevents "iop/apps/edge/internal/events"
edgenode "iop/apps/edge/internal/node"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
func TestRunHandleCloseIsIdempotent(t *testing.T) {
calls := 0
handle := &RunHandle{
close: func() {
calls++
},
}
handle.Close()
handle.Close()
if calls != 1 {
t.Fatalf("close called %d times, want 1", calls)
}
}
func waitForCondition(t *testing.T, cond func() bool, msg string) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal(msg)
}
func inflightRunCount(q *modelQueueManager) int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.inflightByRun)
}
// TestRouteProviderTunnelFrameDoesNotPublishToBus verifies that tunnel frames
// reach only the request-bound tunnel subscriber and never the run event bus
// fanout (negative test for the passthrough/no-sideband boundary).
func TestRouteProviderTunnelFrameDoesNotPublishToBus(t *testing.T) {
bus := edgeevents.NewBus()
svc := New(edgenode.NewRegistry(), bus)
runCh, unsubRun := bus.SubscribeAllRuns(8)
defer unsubRun()
tunnelCh, unsubTunnel := svc.tunnels.subscribe("tunnel-neg-1", 8)
defer unsubTunnel()
frame := &iop.ProviderTunnelFrame{
RunId: "run-neg-1",
TunnelId: "tunnel-neg-1",
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: raw provider bytes\n\n"),
}
svc.RouteProviderTunnelFrame(frame)
select {
case got := <-tunnelCh:
if string(got.GetBody()) != string(frame.GetBody()) {
t.Fatalf("tunnel frame body mismatch: %q", got.GetBody())
}
case <-time.After(time.Second):
t.Fatal("tunnel subscriber did not receive the frame")
}
select {
case e := <-runCh:
t.Fatalf("tunnel frame leaked into run event bus: %+v", e)
case <-time.After(100 * time.Millisecond):
}
}
// TestRouteProviderTunnelFrameWithoutSubscriberIsDropped verifies routing a
// frame with no subscriber neither blocks nor panics.
func TestRouteProviderTunnelFrameWithoutSubscriberIsDropped(t *testing.T) {
svc := New(edgenode.NewRegistry(), nil)
svc.RouteProviderTunnelFrame(&iop.ProviderTunnelFrame{TunnelId: "no-sub"})
}
type providerTunnelTestEnv struct {
svc *Service
nodeClient *toki.TcpClient
capturedMu sync.Mutex
captured *iop.ProviderTunnelRequest
}
func (e *providerTunnelTestEnv) capturedRequest() *iop.ProviderTunnelRequest {
e.capturedMu.Lock()
defer e.capturedMu.Unlock()
return e.captured
}
// newProviderTunnelTestEnv wires a provider-pool service to a fake node over
// net.Pipe and captures the ProviderTunnelRequest the node receives.
func newProviderTunnelTestEnv(t *testing.T) *providerTunnelTestEnv {
t.Helper()
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.ProviderTunnelRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
env := &providerTunnelTestEnv{nodeClient: nodeClient}
toki.AddListenerTyped[*iop.ProviderTunnelRequest](&nodeClient.Communicator, func(req *iop.ProviderTunnelRequest) {
env.capturedMu.Lock()
env.captured = req
env.capturedMu.Unlock()
})
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-pool",
Runtime: config.RuntimeConf{Concurrency: 4},
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-vllm-01",
Adapter: "vllm-gpu",
Type: "vllm",
Models: []string{"served-qwen"},
Health: "available",
Capacity: 1,
},
},
})
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-pool",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
svc := New(reg, edgeevents.NewBus())
svc.SetNodeStore(store)
svc.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-vllm-01": "served-qwen"}},
})
env.svc = svc
return env
}
// TestSubmitProviderTunnelProviderPoolSendsRequestAndReleasesSlotOnEnd covers
// the provider-pool tunnel path end to end: admission rewrites adapter/target,
// BuildBody receives the served target, ordered frames reach the handle, and
// the admission slot is released on the END frame.
func TestSubmitProviderTunnelProviderPoolSendsRequestAndReleasesSlotOnEnd(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
var builtTarget string
handle, err := svc.SubmitProviderTunnel(context.Background(), SubmitProviderTunnelRequest{
RunID: "run-tunnel-001",
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
BuildBody: func(target string) ([]byte, error) {
builtTarget = target
return []byte(`{"model":"` + target + `","stream":true}`), nil
},
Stream: true,
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel: %v", err)
}
defer handle.Close()
if builtTarget != "served-qwen" {
t.Errorf("BuildBody target: got %q, want served-qwen", builtTarget)
}
dispatch := handle.Dispatch()
if dispatch.Adapter != "vllm-gpu" || dispatch.Target != "served-qwen" {
t.Errorf("dispatch adapter/target: got %q/%q", dispatch.Adapter, dispatch.Target)
}
waitForCondition(t, func() bool { return env.capturedRequest() != nil },
"fake node did not receive ProviderTunnelRequest")
captured := env.capturedRequest()
if captured.GetAdapter() != "vllm-gpu" || captured.GetTarget() != "served-qwen" {
t.Errorf("wire adapter/target: got %q/%q", captured.GetAdapter(), captured.GetTarget())
}
if !strings.Contains(string(captured.GetBody()), `"model":"served-qwen"`) {
t.Errorf("wire body missing served model: %s", captured.GetBody())
}
if inflightRunCount(svc.queue) != 1 {
t.Fatalf("expected 1 inflight run after dispatch, got %d", inflightRunCount(svc.queue))
}
tunnelID := captured.GetTunnelId()
frames := []*iop.ProviderTunnelFrame{
{RunId: captured.GetRunId(), TunnelId: tunnelID, Sequence: 0, Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200},
{RunId: captured.GetRunId(), TunnelId: tunnelID, Sequence: 1, Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte("data: chunk-1\n\n")},
{RunId: captured.GetRunId(), TunnelId: tunnelID, Sequence: 2, Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true},
}
for _, f := range frames {
svc.RouteProviderTunnelFrame(f)
}
stream := handle.Stream()
for i, want := range frames {
select {
case got := <-stream.Frames:
if got.GetSequence() != want.GetSequence() || got.GetKind() != want.GetKind() {
t.Fatalf("frame %d: got seq=%d kind=%v", i, got.GetSequence(), got.GetKind())
}
case <-time.After(time.Second):
t.Fatalf("frame %d not delivered", i)
}
}
select {
case _, open := <-stream.Frames:
if open {
t.Fatal("expected frame channel to close after END")
}
case <-time.After(time.Second):
t.Fatal("frame channel not closed after END")
}
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after END frame")
}
// TestSubmitProviderTunnelCloseReleasesSlot verifies the cancel path: closing
// the handle before a terminal frame releases the provider-pool slot.
func TestSubmitProviderTunnelCloseReleasesSlot(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
handle, err := svc.SubmitProviderTunnel(context.Background(), SubmitProviderTunnelRequest{
RunID: "run-tunnel-cancel",
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
Body: []byte(`{"model":"qwen3.6:35b"}`),
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel: %v", err)
}
if inflightRunCount(svc.queue) != 1 {
t.Fatalf("expected 1 inflight run after dispatch, got %d", inflightRunCount(svc.queue))
}
handle.Close()
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after Close")
}
// TestProviderTunnelRouterBackpressureBlocksWithoutDrop verifies the router
// blocks the producer while the subscriber buffer is full instead of dropping
// frames, and delivers every frame in order once the subscriber drains.
func TestProviderTunnelRouterBackpressureBlocksWithoutDrop(t *testing.T) {
svc := New(edgenode.NewRegistry(), nil)
ch, unsub := svc.tunnels.subscribe("tunnel-bp", 2)
defer unsub()
const total = 6
routed := make(chan struct{})
go func() {
defer close(routed)
for i := 0; i < total; i++ {
svc.tunnels.route(&iop.ProviderTunnelFrame{
TunnelId: "tunnel-bp",
Sequence: int64(i),
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
})
}
}()
// With buffer 2 the producer cannot finish all routes before the
// subscriber drains: full buffer must block, not drop.
select {
case <-routed:
t.Fatal("route calls finished while the subscriber buffer was full; expected blocking backpressure")
case <-time.After(100 * time.Millisecond):
}
for i := 0; i < total; i++ {
select {
case f := <-ch:
if f.GetSequence() != int64(i) {
t.Fatalf("frame %d: got sequence %d; frames must stay ordered and lossless under backpressure", i, f.GetSequence())
}
case <-time.After(time.Second):
t.Fatalf("frame %d not delivered after draining", i)
}
}
select {
case <-routed:
case <-time.After(time.Second):
t.Fatal("producer still blocked after all frames were drained")
}
}
// TestProviderTunnelRouterCloseUnblocksBlockedRoute verifies unsubscribe (the
// handle Close/cancel path) releases a route call blocked on a full buffer, so
// an abandoned tunnel can never wedge the transport frame reader.
func TestProviderTunnelRouterCloseUnblocksBlockedRoute(t *testing.T) {
svc := New(edgenode.NewRegistry(), nil)
_, unsub := svc.tunnels.subscribe("tunnel-close", 1)
frame := func(seq int64) *iop.ProviderTunnelFrame {
return &iop.ProviderTunnelFrame{
TunnelId: "tunnel-close",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
}
}
blocked := make(chan struct{})
go func() {
defer close(blocked)
svc.tunnels.route(frame(0)) // fills the buffer
svc.tunnels.route(frame(1)) // blocks until close releases it
}()
select {
case <-blocked:
t.Fatal("second route must block on the full subscriber buffer")
case <-time.After(100 * time.Millisecond):
}
unsub()
select {
case <-blocked:
case <-time.After(time.Second):
t.Fatal("unsubscribe did not release the blocked route call")
}
// A late frame for the closed tunnel is dropped without blocking.
svc.tunnels.route(frame(2))
}
// TestProviderTunnelReleaseExactlyOnceAcrossEndAndClose verifies the tracked
// provider-pool slot (capacity 1) is released exactly once when both the
// terminal END frame and handle Close fire, and that the freed slot admits the
// next tunnel immediately instead of queueing.
func TestProviderTunnelReleaseExactlyOnceAcrossEndAndClose(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
submit := func(runID string) ProviderTunnelResult {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
handle, err := svc.SubmitProviderTunnel(ctx, SubmitProviderTunnelRequest{
RunID: runID,
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
Body: []byte(`{"model":"qwen3.6:35b"}`),
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel(%s): %v", runID, err)
}
return handle
}
handle1 := submit("run-release-1")
waitForCondition(t, func() bool {
captured := env.capturedRequest()
return captured != nil && captured.GetRunId() == "run-release-1"
}, "fake node did not receive the first ProviderTunnelRequest")
captured := env.capturedRequest()
svc.RouteProviderTunnelFrame(&iop.ProviderTunnelFrame{
RunId: captured.GetRunId(),
TunnelId: captured.GetTunnelId(),
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END,
End: true,
})
stream := handle1.Stream()
for {
if _, open := <-stream.Frames; !open {
break
}
}
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after END frame")
// Close after the terminal frame must stay a no-op release.
handle1.Close()
if got := inflightRunCount(svc.queue); got != 0 {
t.Fatalf("inflight count after END+Close: got %d, want 0", got)
}
// The capacity-1 slot is free again: the next tunnel is admitted without
// waiting on the queue (a leaked slot would block this submit).
handle2 := submit("run-release-2")
defer handle2.Close()
waitForCondition(t, func() bool {
captured := env.capturedRequest()
return captured != nil && captured.GetRunId() == "run-release-2"
}, "fake node did not receive the second ProviderTunnelRequest after slot release")
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("inflight count after second dispatch: got %d, want 1", got)
}
}
// TestSubmitProviderTunnelErrorFrameReleasesSlot verifies the ERROR frame is
// terminal and releases the provider-pool slot like END.
func TestSubmitProviderTunnelErrorFrameReleasesSlot(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
handle, err := svc.SubmitProviderTunnel(context.Background(), SubmitProviderTunnelRequest{
RunID: "run-tunnel-error",
ModelGroupKey: "qwen3.6:35b",
Method: "POST",
Path: "/v1/chat/completions",
Body: []byte(`{"model":"qwen3.6:35b"}`),
ProviderPool: true,
})
if err != nil {
t.Fatalf("SubmitProviderTunnel: %v", err)
}
defer handle.Close()
waitForCondition(t, func() bool { return env.capturedRequest() != nil },
"fake node did not receive ProviderTunnelRequest")
captured := env.capturedRequest()
svc.RouteProviderTunnelFrame(&iop.ProviderTunnelFrame{
RunId: captured.GetRunId(),
TunnelId: captured.GetTunnelId(),
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR,
Error: "provider unavailable",
})
stream := handle.Stream()
select {
case got := <-stream.Frames:
if got.GetKind() != iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR {
t.Fatalf("expected ERROR frame, got %v", got.GetKind())
}
case <-time.After(time.Second):
t.Fatal("ERROR frame not delivered")
}
waitForCondition(t, func() bool { return inflightRunCount(svc.queue) == 0 },
"admission slot not released after ERROR frame")
}
// TestSubmitProviderPoolSelectsTunnelProviderAndReleasesSlot verifies that
// SubmitProviderPool with an OpenAI-compatible provider (tunnel path) performs
// a single admission, sends a ProviderTunnelRequest, and releases the slot on
// tunnel END frame.
func TestSubmitProviderPoolSelectsTunnelProviderAndReleasesSlot(t *testing.T) {
env := newProviderTunnelTestEnv(t)
svc := env.svc
// Catalog entry with a vLLM provider (tunnel path).
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-vllm-01": "served-qwen"}},
}
svc.SetModelCatalog(catalog)
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "qwen3.6:35b",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Path != ProviderPoolPathTunnel {
t.Fatalf("expected path=tunnel, got %q", result.Path)
}
if result.Tunnel == nil {
t.Fatal("expected tunnel handle for tunnel path")
}
if result.Run != nil {
t.Fatal("expected nil run for tunnel path")
}
// Verify inflight after dispatch.
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight after dispatch, got %d", got)
}
}
// TestSubmitProviderPoolSelectsNormalizedProviderAndDoesNotOpenTunnel verifies
// that SubmitProviderPool with an Ollama provider (normalized path) performs
// a single admission, sends a RunRequest (not ProviderTunnelRequest), and
// the tunnel handle is nil.
func TestSubmitProviderPoolSelectsNormalizedProviderAndDoesNotOpenTunnel(t *testing.T) {
// Build a provider-pool service with an Ollama provider.
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
// Capture the RunRequest received by the fake node.
var capturedRunReq *iop.RunRequest
var capturedMu sync.Mutex
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
capturedMu.Lock()
capturedRunReq = req
capturedMu.Unlock()
})
// Build NodeStore with an Ollama provider (normalized path).
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-ollama",
Runtime: config.RuntimeConf{Concurrency: 2},
Adapters: config.AdaptersConf{
OllamaInstances: []config.OllamaInstanceConf{
{Name: "ollama", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-ollama-01",
Adapter: "ollama",
Type: "ollama",
Models: []string{"served-qwen"},
Health: "available",
Capacity: 2,
},
},
})
// Build catalog with Ollama provider.
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-ollama-01": "served-qwen"}},
}
// Build registry with the fake node.
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-ollama",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
// Create Service with queue and catalog.
bus := edgeevents.NewBus()
svc := New(reg, bus)
svc.SetNodeStore(store)
svc.SetModelCatalog(catalog)
// SubmitProviderPool with Ollama provider → normalized path.
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "qwen3.6:35b",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Path != ProviderPoolPathNormalized {
t.Fatalf("expected path=normalized, got %q", result.Path)
}
if result.Run == nil {
t.Fatal("expected run handle for normalized path")
}
if result.Tunnel != nil {
t.Fatal("expected nil tunnel for normalized path")
}
// Wait for the fake node to receive the RunRequest.
time.Sleep(50 * time.Millisecond)
capturedMu.Lock()
defer capturedMu.Unlock()
if capturedRunReq == nil {
t.Fatal("no RunRequest captured from fake node; SubmitProviderPool did not send")
}
// Verify that the adapter and target were rewritten from the provider-pool candidate.
if capturedRunReq.GetAdapter() != "ollama" {
t.Errorf("adapter: got %q, want %q", capturedRunReq.GetAdapter(), "ollama")
}
if capturedRunReq.GetTarget() != "served-qwen" {
t.Errorf("target: got %q, want %q", capturedRunReq.GetTarget(), "served-qwen")
}
// Verify inflight after dispatch.
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight after dispatch, got %d", got)
}
}
// TestSubmitProviderPoolUsesSingleAdmissionForMixedCandidates verifies that
// SubmitProviderPool performs exactly one queue admission when presented with
// mixed tunnel+normalized candidates (e.g., vLLM + Ollama). The selected
// candidate (by in-flight/priority rotation) determines the path.
func TestSubmitProviderPoolUsesSingleAdmissionForMixedCandidates(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
var capturedRunReq *iop.RunRequest
var capturedMu sync.Mutex
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
capturedMu.Lock()
capturedRunReq = req
capturedMu.Unlock()
})
// Build NodeStore with two providers: vLLM (tunnel) and Ollama (normalized).
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-mixed",
Runtime: config.RuntimeConf{Concurrency: 4},
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
},
OllamaInstances: []config.OllamaInstanceConf{
{Name: "ollama", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-vllm",
Adapter: "vllm-gpu",
Type: "vllm",
Models: []string{"served-model"},
Health: "available",
Capacity: 2,
},
{
ID: "prov-ollama",
Adapter: "ollama",
Type: "ollama",
Models: []string{"served-model"},
Health: "available",
Capacity: 2,
},
},
})
// Catalog with both providers.
catalog := []config.ModelCatalogEntry{
{ID: "mixed-model", Providers: map[string]string{"prov-vllm": "served-model", "prov-ollama": "served-model"}},
}
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-mixed",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
bus := edgeevents.NewBus()
svc := New(reg, bus)
svc.SetNodeStore(store)
svc.SetModelCatalog(catalog)
// SubmitProviderPool → should do exactly one admission.
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "mixed-model",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
// The selected provider depends on admission order (vLLM or Ollama by
// providerID alphabetical tie-break). Verify only one request was sent.
time.Sleep(50 * time.Millisecond)
capturedMu.Lock()
defer capturedMu.Unlock()
if capturedRunReq == nil {
t.Fatal("expected exactly one RunRequest for normalized path; admission must be single")
}
// Verify the request target matches the catalog served model.
if capturedRunReq.GetTarget() != "served-model" {
t.Errorf("target: got %q, want %q", capturedRunReq.GetTarget(), "served-model")
}
// Verify inflight is exactly 1 (single admission).
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight (single admission), got %d", got)
}
}
// TestSubmitProviderPoolDispatchInfoObservation verifies that the provider-pool
// one-shot dispatch carries selected provider id, provider type, and execution
// path in RunDispatch on both tunnel and normalized paths (SURFACE_OBS-1).
func TestSubmitProviderPoolDispatchInfoObservation(t *testing.T) {
for _, tc := range []struct {
name string
providerType string
wantPath providerPoolPath
wantExecPath string
}{
{
name: "vllm_tunnel",
providerType: "vllm",
wantPath: ProviderPoolPathTunnel,
wantExecPath: "provider_tunnel",
},
{
name: "ollama_normalized",
providerType: "ollama",
wantPath: ProviderPoolPathNormalized,
wantExecPath: "normalized",
},
{
name: "cli_normalized",
providerType: "cli",
wantPath: ProviderPoolPathNormalized,
wantExecPath: "normalized",
},
} {
t.Run(tc.name, func(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
t.Cleanup(func() {
_ = edgeConn.Close()
_ = nodeConn.Close()
})
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.RunRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.ProviderTunnelRequest{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
_ = toki.NewTcpClient(nodeConn, 0, 0, parserMap)
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-obs",
Runtime: config.RuntimeConf{Concurrency: 2},
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{{Name: "vllm-gpu", Enabled: true}},
OllamaInstances: []config.OllamaInstanceConf{{Name: "ollama", Enabled: true}},
OpenAICompatInstances: []config.OpenAICompatInstanceConf{{Name: "openai", Enabled: true}},
},
Providers: []config.NodeProviderConf{
{
ID: "prov-obs",
Adapter: "vllm-gpu",
Type: tc.providerType,
Models: []string{"served-model"},
Health: "available",
Capacity: 2,
},
},
})
catalog := []config.ModelCatalogEntry{
{ID: "obs-model", Providers: map[string]string{"prov-obs": "served-model"}},
}
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-obs",
LifecycleState: edgenode.LifecycleConnected,
Client: edgeClient,
})
bus := edgeevents.NewBus()
svc := New(reg, bus)
svc.SetNodeStore(store)
svc.SetModelCatalog(catalog)
result, err := svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
Run: SubmitRunRequest{
ModelGroupKey: "obs-model",
ProviderPool: true,
Background: true,
},
})
if err != nil {
t.Fatalf("SubmitProviderPool: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Path != tc.wantPath {
t.Fatalf("path: got %q, want %q", result.Path, tc.wantPath)
}
disp := result.DispatchInfo
if disp.ProviderID != "prov-obs" {
t.Errorf("provider_id: got %q, want %q", disp.ProviderID, "prov-obs")
}
if disp.ProviderType != tc.providerType {
t.Errorf("provider_type: got %q, want %q", disp.ProviderType, tc.providerType)
}
if disp.ExecutionPath != tc.wantExecPath {
t.Errorf("execution_path: got %q, want %q", disp.ExecutionPath, tc.wantExecPath)
}
if disp.QueueReason != "dispatched" {
t.Errorf("queue_reason: got %q, want %q", disp.QueueReason, "dispatched")
}
if disp.Adapter != "vllm-gpu" {
t.Errorf("adapter: got %q, want %q", disp.Adapter, "vllm-gpu")
}
if disp.Target != "served-model" {
t.Errorf("target: got %q, want %q", disp.Target, "served-model")
}
// Also verify tunnel/normalized handle DispatchInfo matches.
switch result.Path {
case ProviderPoolPathTunnel:
if result.Tunnel == nil {
t.Fatal("expected tunnel handle for tunnel path")
}
hDisp := result.Tunnel.Dispatch()
if hDisp.ProviderID != "prov-obs" {
t.Errorf("tunnel handle provider_id: got %q, want %q", hDisp.ProviderID, "prov-obs")
}
if hDisp.ProviderType != tc.providerType {
t.Errorf("tunnel handle provider_type: got %q, want %q", hDisp.ProviderType, tc.providerType)
}
if hDisp.ExecutionPath != tc.wantExecPath {
t.Errorf("tunnel handle execution_path: got %q, want %q", hDisp.ExecutionPath, tc.wantExecPath)
}
// Close releases the inflight slot.
result.Tunnel.Close()
if got := inflightRunCount(svc.queue); got != 0 {
t.Fatalf("expected 0 inflight after tunnel close, got %d", got)
}
case ProviderPoolPathNormalized:
if result.Run == nil {
t.Fatal("expected run handle for normalized path")
}
hDisp := result.Run.Dispatch()
if hDisp.ProviderID != "prov-obs" {
t.Errorf("run handle provider_id: got %q, want %q", hDisp.ProviderID, "prov-obs")
}
if hDisp.ProviderType != tc.providerType {
t.Errorf("run handle provider_type: got %q, want %q", hDisp.ProviderType, tc.providerType)
}
if hDisp.ExecutionPath != tc.wantExecPath {
t.Errorf("run handle execution_path: got %q, want %q", hDisp.ExecutionPath, tc.wantExecPath)
}
// Inflight should be 1 for normalized (run hasn't completed yet).
if got := inflightRunCount(svc.queue); got != 1 {
t.Fatalf("expected 1 inflight for normalized, got %d", got)
}
result.Run.Close()
}
})
}
}