516 lines
14 KiB
Go
516 lines
14 KiB
Go
package config_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
// TestLoadEdgeModelExecutionPresetOneOf covers the one-of admission rule for
|
|
// ModelCatalogEntry: exactly one of providers or execution_preset must be set,
|
|
// preset ids must resolve to an execution_presets[] entry, and existing
|
|
// provider-only fixtures must keep working unchanged.
|
|
func TestLoadEdgeModelExecutionPresetOneOf(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
|
|
// ---- Happy path: provider-only (existing behavior) ----
|
|
t.Run("provider-only entry loads unchanged", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
vllm-gpu: "nvidia/Qwen3.6-35B"
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B"
|
|
capacity: 4
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(cfg.Models) != 1 {
|
|
t.Fatalf("expected 1 model, got %d", len(cfg.Models))
|
|
}
|
|
if cfg.Models[0].ExecutionPreset != "" {
|
|
t.Errorf("provider-only entry should not have execution_preset set, got %q", cfg.Models[0].ExecutionPreset)
|
|
}
|
|
if len(cfg.Models[0].Providers) != 1 {
|
|
t.Errorf("expected 1 provider, got %d", len(cfg.Models[0].Providers))
|
|
}
|
|
})
|
|
|
|
// ---- Happy path: preset-only (virtual model) ----
|
|
t.Run("preset-only entry loads as virtual model", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "virtual-model"
|
|
execution_preset: "fast-path"
|
|
execution_presets:
|
|
- id: "fast-path"
|
|
selector:
|
|
model: "virtual-model"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(cfg.Models) != 1 {
|
|
t.Fatalf("expected 1 model, got %d", len(cfg.Models))
|
|
}
|
|
m := cfg.Models[0]
|
|
if m.ID != "virtual-model" {
|
|
t.Errorf("model id = %q, want virtual-model", m.ID)
|
|
}
|
|
if m.ExecutionPreset != "fast-path" {
|
|
t.Errorf("execution_preset = %q, want fast-path", m.ExecutionPreset)
|
|
}
|
|
if len(m.Providers) != 0 {
|
|
t.Errorf("virtual model should have empty providers, got %v", m.Providers)
|
|
}
|
|
})
|
|
|
|
// ---- Error: both providers and execution_preset set ----
|
|
t.Run("both providers and execution_preset rejected", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "confused-model"
|
|
execution_preset: "fast-path"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "fast-path"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for both providers and execution_preset set")
|
|
}
|
|
if !strings.Contains(err.Error(), "exactly one of providers or execution_preset must be set") {
|
|
t.Fatalf("expected one-of error, got %v", err)
|
|
}
|
|
})
|
|
|
|
// ---- Error: neither providers nor execution_preset ----
|
|
t.Run("neither providers nor execution_preset rejected", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "empty-model"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for neither providers nor execution_preset")
|
|
}
|
|
if !strings.Contains(err.Error(), "providers must not be empty") {
|
|
t.Fatalf("expected providers must not be empty error, got %v", err)
|
|
}
|
|
})
|
|
|
|
// ---- Error: dangling execution_preset id ----
|
|
t.Run("dangling execution_preset id rejected", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "dangling-model"
|
|
execution_preset: "non-existent-preset"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for dangling execution_preset id")
|
|
}
|
|
if !strings.Contains(err.Error(), "execution_preset") && !strings.Contains(err.Error(), "does not match any execution_presets") {
|
|
t.Fatalf("expected dangling preset error, got %v", err)
|
|
}
|
|
})
|
|
|
|
// ---- Compatibility: mixed catalog with both provider-only and preset-only ----
|
|
t.Run("mixed catalog with provider-only and preset-only entries", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
vllm-gpu: "nvidia/Qwen3.6-35B"
|
|
- id: "virtual-light"
|
|
execution_preset: "review-path"
|
|
execution_presets:
|
|
- id: "review-path"
|
|
selector:
|
|
model: "qwen3.6:35b"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "qwen3.6:35b"
|
|
- role: "review"
|
|
model: "qwen3.6:35b"
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B"
|
|
capacity: 4
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(cfg.Models) != 2 {
|
|
t.Fatalf("expected 2 models, got %d", len(cfg.Models))
|
|
}
|
|
byID := map[string]config.ModelCatalogEntry{}
|
|
for _, m := range cfg.Models {
|
|
byID[m.ID] = m
|
|
}
|
|
// Provider-only entry should be unchanged.
|
|
provModel := byID["qwen3.6:35b"]
|
|
if len(provModel.Providers) != 1 {
|
|
t.Errorf("provider-only model should have 1 provider, got %d", len(provModel.Providers))
|
|
}
|
|
if provModel.ExecutionPreset != "" {
|
|
t.Errorf("provider-only model should not have execution_preset, got %q", provModel.ExecutionPreset)
|
|
}
|
|
// Virtual entry should reference the preset.
|
|
virtualModel := byID["virtual-light"]
|
|
if virtualModel.ExecutionPreset != "review-path" {
|
|
t.Errorf("virtual model execution_preset = %q, want review-path", virtualModel.ExecutionPreset)
|
|
}
|
|
if len(virtualModel.Providers) != 0 {
|
|
t.Errorf("virtual model should have empty providers, got %v", virtualModel.Providers)
|
|
}
|
|
})
|
|
|
|
// ---- Compatibility: provider-only fixture with budget must still validate ----
|
|
t.Run("provider-only entry with insufficient budget still rejected", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
context_window_tokens: 262144
|
|
providers:
|
|
vllm-gpu: "nvidia/Qwen3.6-35B"
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B"
|
|
capacity: 4
|
|
total_context_tokens: 262144
|
|
long_context_capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for insufficient long-context budget on provider-only entry")
|
|
}
|
|
if !strings.Contains(err.Error(), "total_context_tokens") {
|
|
t.Fatalf("expected budget error, got %v", err)
|
|
}
|
|
})
|
|
|
|
// ---- Edge: whitespace-only execution_preset treated as unset ----
|
|
t.Run("whitespace-only execution_preset treated as unset", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "whitespace-model"
|
|
execution_preset: " "
|
|
providers:
|
|
prov-a: "model-a"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(cfg.Models) != 1 {
|
|
t.Fatalf("expected 1 model, got %d", len(cfg.Models))
|
|
}
|
|
// Whitespace-only preset should be treated as unset, so provider-only
|
|
// path should apply.
|
|
if cfg.Models[0].ExecutionPreset != "" {
|
|
t.Errorf("whitespace preset should be treated as unset, got %q", cfg.Models[0].ExecutionPreset)
|
|
}
|
|
})
|
|
|
|
// ---- Normalization: non-empty execution_preset is stored canonically ----
|
|
t.Run("non-empty execution_preset is normalized", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
vllm-gpu: "nvidia/Qwen3.6-35B"
|
|
- id: "virtual-fast"
|
|
execution_preset: " fast-path "
|
|
execution_presets:
|
|
- id: "fast-path"
|
|
selector:
|
|
model: "qwen3.6:35b"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B"
|
|
capacity: 4
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
byID := map[string]config.ModelCatalogEntry{}
|
|
for _, m := range cfg.Models {
|
|
byID[m.ID] = m
|
|
}
|
|
// The padded valid preset id must be persisted in canonical (trimmed)
|
|
// form so exact downstream lookups match the admitted value.
|
|
virtual := byID["virtual-fast"]
|
|
if virtual.ExecutionPreset != "fast-path" {
|
|
t.Errorf("execution_preset = %q, want canonical %q", virtual.ExecutionPreset, "fast-path")
|
|
}
|
|
if len(virtual.Providers) != 0 {
|
|
t.Errorf("virtual model should have empty providers, got %v", virtual.Providers)
|
|
}
|
|
// Provider-only entry stays unchanged.
|
|
prov := byID["qwen3.6:35b"]
|
|
if prov.ExecutionPreset != "" {
|
|
t.Errorf("provider-only entry should not have execution_preset set, got %q", prov.ExecutionPreset)
|
|
}
|
|
if len(prov.Providers) != 1 {
|
|
t.Errorf("provider-only entry should have 1 provider, got %d", len(prov.Providers))
|
|
}
|
|
})
|
|
|
|
// ---- Error: empty execution_preset string with no providers ----
|
|
t.Run("explicit empty execution_preset with no providers rejected", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "empty-preset-model"
|
|
execution_preset: ""
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty execution_preset with no providers")
|
|
}
|
|
if !strings.Contains(err.Error(), "providers must not be empty") {
|
|
t.Fatalf("expected providers must not be empty error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestModelCatalogEntry_ValidateVirtualEntryUnit covers unit-level Validate
|
|
// behavior for the one-of rule without going through LoadEdge.
|
|
func TestModelCatalogEntry_ValidateVirtualEntryUnit(t *testing.T) {
|
|
providerIDs := map[string]struct{}{
|
|
"vllm-gpu": {},
|
|
}
|
|
serveModels := map[string]map[string]struct{}{
|
|
"vllm-gpu": {"model-a": {}},
|
|
}
|
|
|
|
t.Run("provider-only validates", func(t *testing.T) {
|
|
e := config.ModelCatalogEntry{
|
|
ID: "model-a",
|
|
Providers: map[string]string{"vllm-gpu": "model-a"},
|
|
}
|
|
if err := e.Validate(providerIDs, serveModels); err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("preset-only validates (returns nil, preset resolved later)", func(t *testing.T) {
|
|
e := config.ModelCatalogEntry{
|
|
ID: "virtual-model",
|
|
ExecutionPreset: "fast-path",
|
|
}
|
|
if err := e.Validate(providerIDs, serveModels); err != nil {
|
|
t.Fatalf("expected no error for preset-only, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("both providers and execution_preset rejected", func(t *testing.T) {
|
|
e := config.ModelCatalogEntry{
|
|
ID: "bad-model",
|
|
Providers: map[string]string{"vllm-gpu": "model-a"},
|
|
ExecutionPreset: "fast-path",
|
|
}
|
|
if err := e.Validate(providerIDs, serveModels); err == nil {
|
|
t.Fatal("expected error for both set")
|
|
}
|
|
})
|
|
|
|
t.Run("neither providers nor execution_preset rejected", func(t *testing.T) {
|
|
e := config.ModelCatalogEntry{
|
|
ID: "empty-model",
|
|
}
|
|
if err := e.Validate(providerIDs, serveModels); err == nil {
|
|
t.Fatal("expected error for neither set")
|
|
}
|
|
})
|
|
|
|
t.Run("whitespace execution_preset treated as unset", func(t *testing.T) {
|
|
e := config.ModelCatalogEntry{
|
|
ID: "ws-model",
|
|
Providers: map[string]string{"vllm-gpu": "model-a"},
|
|
ExecutionPreset: " ",
|
|
}
|
|
if err := e.Validate(providerIDs, serveModels); err != nil {
|
|
t.Fatalf("expected no error (whitespace preset treated as unset), got: %v", err)
|
|
}
|
|
})
|
|
}
|