3724 lines
105 KiB
Go
3724 lines
105 KiB
Go
package config_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
func TestLoadEdge_EdgeIdentity(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nedge:\n id: \"edge-dgx-group\"\n name: \"DGX Group\"\n"
|
|
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 cfg.Edge.ID != "edge-dgx-group" {
|
|
t.Fatalf("expected edge.id=%q, got %q", "edge-dgx-group", cfg.Edge.ID)
|
|
}
|
|
if cfg.Edge.Name != "DGX Group" {
|
|
t.Fatalf("expected edge.name=%q, got %q", "DGX Group", cfg.Edge.Name)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_EdgeIdentityEmptyByDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Edge.ID != "" || cfg.Edge.Name != "" {
|
|
t.Fatalf("expected empty edge identity by default, got id=%q name=%q", cfg.Edge.ID, cfg.Edge.Name)
|
|
}
|
|
}
|
|
|
|
func TestEdgeRefreshDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Refresh.Enabled {
|
|
t.Fatalf("expected refresh.enabled=false by default, got true")
|
|
}
|
|
if cfg.Refresh.Listen != "127.0.0.1:19093" {
|
|
t.Fatalf("expected refresh.listen=%q by default, got %q", "127.0.0.1:19093", cfg.Refresh.Listen)
|
|
}
|
|
}
|
|
|
|
func TestEdgeRefreshUnmarshal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nrefresh:\n enabled: true\n listen: \"127.0.0.1:0\"\n"
|
|
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 !cfg.Refresh.Enabled {
|
|
t.Fatalf("expected refresh.enabled=true")
|
|
}
|
|
if cfg.Refresh.Listen != "127.0.0.1:0" {
|
|
t.Fatalf("expected refresh.listen=%q, got %q", "127.0.0.1:0", cfg.Refresh.Listen)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTimeoutDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.TimeoutSec != 120 {
|
|
t.Fatalf("expected default timeout_sec=120, got %d", cfg.Console.TimeoutSec)
|
|
}
|
|
if cfg.Console.Adapter != "cli" {
|
|
t.Fatalf("expected default console.adapter=%q, got %q", "cli", cfg.Console.Adapter)
|
|
}
|
|
if cfg.Console.ResolveTarget() != "claude" {
|
|
t.Fatalf("expected default console.target=%q, got %q", "claude", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.OpenAI.Enabled {
|
|
t.Fatal("expected openai.enabled=false by default")
|
|
}
|
|
if cfg.OpenAI.Listen != "0.0.0.0:18081" {
|
|
t.Fatalf("expected openai.listen default, got %q", cfg.OpenAI.Listen)
|
|
}
|
|
if cfg.OpenAI.BearerToken != "" {
|
|
t.Fatalf("expected openai.bearer_token empty by default, got %q", cfg.OpenAI.BearerToken)
|
|
}
|
|
if cfg.OpenAI.ProviderAuth.Enabled {
|
|
t.Fatal("expected openai.provider_auth.enabled=false by default")
|
|
}
|
|
if cfg.OpenAI.ProviderAuth.FromHeader != "" || cfg.OpenAI.ProviderAuth.TargetHeader != "" || cfg.OpenAI.ProviderAuth.Scheme != "" || cfg.OpenAI.ProviderAuth.Required {
|
|
t.Fatalf("expected openai.provider_auth fields empty/false by default, got %+v", cfg.OpenAI.ProviderAuth)
|
|
}
|
|
if cfg.OpenAI.Adapter != "ollama" {
|
|
t.Fatalf("expected openai.adapter=%q, got %q", "ollama", cfg.OpenAI.Adapter)
|
|
}
|
|
if cfg.OpenAI.SessionID != "openai" {
|
|
t.Fatalf("expected openai.session_id=%q, got %q", "openai", cfg.OpenAI.SessionID)
|
|
}
|
|
if cfg.OpenAI.TimeoutSec != 120 {
|
|
t.Fatalf("expected openai.timeout_sec=120, got %d", cfg.OpenAI.TimeoutSec)
|
|
}
|
|
if !cfg.OpenAI.StrictOutput {
|
|
t.Fatal("expected openai.strict_output=true")
|
|
}
|
|
if cfg.OpenAI.StrictStreamBuffer {
|
|
t.Fatal("expected openai.strict_stream_buffer=false")
|
|
}
|
|
if cfg.Metrics.Port != 19092 {
|
|
t.Fatalf("expected metrics.port=19092, got %d", cfg.Metrics.Port)
|
|
}
|
|
if cfg.LongContextThresholdTokens != 100000 {
|
|
t.Fatalf("expected long_context_threshold_tokens=100000 by default, got %d", cfg.LongContextThresholdTokens)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_LongContextThresholdOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
long_context_threshold_tokens: 120000
|
|
`
|
|
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 cfg.LongContextThresholdTokens != 120000 {
|
|
t.Fatalf("expected long_context_threshold_tokens=120000, got %d", cfg.LongContextThresholdTokens)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_LongContextThresholdRejectsNonPositive(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
value int
|
|
}{
|
|
{name: "zero", value: 0},
|
|
{name: "negative", value: -1},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := fmt.Sprintf(`
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
long_context_threshold_tokens: %d
|
|
`, tc.value)
|
|
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 non-positive long_context_threshold_tokens")
|
|
}
|
|
if !strings.Contains(err.Error(), "long_context_threshold_tokens must be positive") {
|
|
t.Fatalf("expected error mentioning positive threshold, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
listen: "127.0.0.1:8088"
|
|
bearer_token: "secret-token"
|
|
node: "node0"
|
|
adapter: "ollama"
|
|
target: "llama-test"
|
|
models:
|
|
- "llama-test"
|
|
session_id: "cline"
|
|
timeout_sec: 45
|
|
strict_output: false
|
|
strict_stream_buffer: true
|
|
`
|
|
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 !cfg.OpenAI.Enabled {
|
|
t.Fatal("expected openai.enabled=true")
|
|
}
|
|
if cfg.OpenAI.Listen != "127.0.0.1:8088" || cfg.OpenAI.NodeRef != "node0" {
|
|
t.Fatalf("unexpected openai routing config: %+v", cfg.OpenAI)
|
|
}
|
|
if cfg.OpenAI.BearerToken != "secret-token" {
|
|
t.Fatalf("expected openai.bearer_token=%q, got %q", "secret-token", cfg.OpenAI.BearerToken)
|
|
}
|
|
if cfg.OpenAI.Target != "llama-test" || len(cfg.OpenAI.Models) != 1 || cfg.OpenAI.Models[0] != "llama-test" {
|
|
t.Fatalf("unexpected openai model config: %+v", cfg.OpenAI)
|
|
}
|
|
if cfg.OpenAI.SessionID != "cline" || cfg.OpenAI.TimeoutSec != 45 {
|
|
t.Fatalf("unexpected openai execution config: %+v", cfg.OpenAI)
|
|
}
|
|
if cfg.OpenAI.StrictOutput {
|
|
t.Fatalf("unexpected openai strict output config: %+v", cfg.OpenAI)
|
|
}
|
|
if !cfg.OpenAI.StrictStreamBuffer {
|
|
t.Fatalf("unexpected openai strict stream buffer config: %+v", cfg.OpenAI)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIProviderAuthEnabledDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
provider_auth:
|
|
enabled: true
|
|
`
|
|
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)
|
|
}
|
|
auth := cfg.OpenAI.ProviderAuth
|
|
if !auth.Enabled {
|
|
t.Fatal("expected provider_auth.enabled=true")
|
|
}
|
|
if auth.FromHeader != "X-IOP-Provider-Authorization" {
|
|
t.Fatalf("expected default from_header, got %q", auth.FromHeader)
|
|
}
|
|
if auth.TargetHeader != "Authorization" {
|
|
t.Fatalf("expected default target_header, got %q", auth.TargetHeader)
|
|
}
|
|
if auth.Scheme != "Bearer" {
|
|
t.Fatalf("expected default scheme Bearer, got %q", auth.Scheme)
|
|
}
|
|
if !auth.Required {
|
|
t.Fatal("expected required=true by default when provider_auth is enabled")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIProviderAuthOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
provider_auth:
|
|
enabled: true
|
|
from_header: "X-Seulgivibe-Token"
|
|
target_header: "X-Provider-Authorization"
|
|
scheme: "Token"
|
|
required: false
|
|
`
|
|
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)
|
|
}
|
|
auth := cfg.OpenAI.ProviderAuth
|
|
if !auth.Enabled {
|
|
t.Fatal("expected provider_auth.enabled=true")
|
|
}
|
|
if auth.FromHeader != "X-Seulgivibe-Token" || auth.TargetHeader != "X-Provider-Authorization" || auth.Scheme != "Token" || auth.Required {
|
|
t.Fatalf("unexpected provider_auth override: %+v", auth)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIProviderAuthRejectsBlankHeaders(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
field string
|
|
wantMsg string
|
|
}{
|
|
{name: "from_header", field: "from_header", wantMsg: "from_header"},
|
|
{name: "target_header", field: "target_header", wantMsg: "target_header"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := fmt.Sprintf(`
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
provider_auth:
|
|
enabled: true
|
|
%s: " "
|
|
`, tc.field)
|
|
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 blank provider_auth header error")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantMsg) {
|
|
t.Fatalf("expected error mentioning %q, got %v", tc.wantMsg, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIPrincipalTokens(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
principal_tokens:
|
|
- token_ref: "iop-tok-alice"
|
|
token_hash_sha256: "` + strings.Repeat("a1", 32) + `"
|
|
principal_ref: "user:alice"
|
|
principal_alias: "alice"
|
|
- token_ref: "iop-tok-bob"
|
|
token_hash_sha256: "` + strings.Repeat("b2", 32) + `"
|
|
principal_ref: "user:bob"
|
|
`
|
|
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.OpenAI.PrincipalTokens) != 2 {
|
|
t.Fatalf("expected 2 principal_tokens, got %d", len(cfg.OpenAI.PrincipalTokens))
|
|
}
|
|
first := cfg.OpenAI.PrincipalTokens[0]
|
|
if first.TokenRef != "iop-tok-alice" || first.PrincipalRef != "user:alice" || first.PrincipalAlias != "alice" {
|
|
t.Fatalf("unexpected first principal token: %+v", first)
|
|
}
|
|
if first.TokenHashSHA256 != strings.Repeat("a1", 32) {
|
|
t.Fatalf("unexpected token hash: %q", first.TokenHashSHA256)
|
|
}
|
|
second := cfg.OpenAI.PrincipalTokens[1]
|
|
if second.TokenRef != "iop-tok-bob" || second.PrincipalRef != "user:bob" || second.PrincipalAlias != "" {
|
|
t.Fatalf("unexpected second principal token: %+v", second)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIPrincipalTokensAllowMultipleTokensPerPrincipal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
principal_tokens:
|
|
- token_ref: "alice-app-a"
|
|
token_hash_sha256: "` + strings.Repeat("a1", 32) + `"
|
|
principal_ref: "user:alice"
|
|
principal_alias: "alice"
|
|
- token_ref: "alice-app-b"
|
|
token_hash_sha256: "` + strings.Repeat("b2", 32) + `"
|
|
principal_ref: "user:alice"
|
|
principal_alias: "alice"
|
|
`
|
|
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.OpenAI.PrincipalTokens) != 2 {
|
|
t.Fatalf("expected 2 principal_tokens, got %d", len(cfg.OpenAI.PrincipalTokens))
|
|
}
|
|
if cfg.OpenAI.PrincipalTokens[0].PrincipalRef != cfg.OpenAI.PrincipalTokens[1].PrincipalRef {
|
|
t.Fatalf("expected both tokens to map to the same principal: %+v", cfg.OpenAI.PrincipalTokens)
|
|
}
|
|
if cfg.OpenAI.PrincipalTokens[0].TokenRef == cfg.OpenAI.PrincipalTokens[1].TokenRef {
|
|
t.Fatalf("token_ref must distinguish app/integration tokens: %+v", cfg.OpenAI.PrincipalTokens)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIPrincipalTokensRejectInvalid(t *testing.T) {
|
|
validHash := strings.Repeat("a1", 32)
|
|
for _, tc := range []struct {
|
|
name string
|
|
yaml string
|
|
}{
|
|
{
|
|
name: "duplicate token_ref",
|
|
yaml: `
|
|
openai:
|
|
principal_tokens:
|
|
- token_ref: "dup"
|
|
token_hash_sha256: "` + validHash + `"
|
|
principal_ref: "user:alice"
|
|
- token_ref: "dup"
|
|
token_hash_sha256: "` + strings.Repeat("b2", 32) + `"
|
|
principal_ref: "user:bob"
|
|
`,
|
|
},
|
|
{
|
|
name: "duplicate token_hash_sha256",
|
|
yaml: `
|
|
openai:
|
|
principal_tokens:
|
|
- token_ref: "one"
|
|
token_hash_sha256: "` + validHash + `"
|
|
principal_ref: "user:alice"
|
|
- token_ref: "two"
|
|
token_hash_sha256: "` + validHash + `"
|
|
principal_ref: "user:bob"
|
|
`,
|
|
},
|
|
{
|
|
name: "empty principal_ref",
|
|
yaml: `
|
|
openai:
|
|
principal_tokens:
|
|
- token_ref: "one"
|
|
token_hash_sha256: "` + validHash + `"
|
|
principal_ref: ""
|
|
`,
|
|
},
|
|
{
|
|
name: "non-hex hash",
|
|
yaml: `
|
|
openai:
|
|
principal_tokens:
|
|
- token_ref: "one"
|
|
token_hash_sha256: "` + strings.Repeat("z", 64) + `"
|
|
principal_ref: "user:alice"
|
|
`,
|
|
},
|
|
{
|
|
name: "non-64-length hash",
|
|
yaml: `
|
|
openai:
|
|
principal_tokens:
|
|
- token_ref: "one"
|
|
token_hash_sha256: "abcd"
|
|
principal_ref: "user:alice"
|
|
`,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
body := "server:\n listen: \"0.0.0.0:9090\"\n" + tc.yaml
|
|
if err := os.WriteFile(f, []byte(body), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
if _, err := config.LoadEdge(f); err == nil {
|
|
t.Fatal("expected validation error, got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOpenAIPrincipalTokenConf_NoRawTokenField guards against reintroducing a
|
|
// raw-token-storing field on the principal token mapping struct: only
|
|
// token_ref, token_hash_sha256, principal_ref, and principal_alias are
|
|
// allowed YAML keys.
|
|
func TestOpenAIPrincipalTokenConf_NoRawTokenField(t *testing.T) {
|
|
allowed := map[string]struct{}{
|
|
"token_ref": {},
|
|
"token_hash_sha256": {},
|
|
"principal_ref": {},
|
|
"principal_alias": {},
|
|
}
|
|
typ := reflect.TypeOf(config.OpenAIPrincipalTokenConf{})
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
tag := typ.Field(i).Tag.Get("yaml")
|
|
key := strings.SplitN(tag, ",", 2)[0]
|
|
if _, ok := allowed[key]; !ok {
|
|
t.Fatalf("unexpected field %q (yaml tag %q) on OpenAIPrincipalTokenConf; raw token values must not be stored in config", typ.Field(i).Name, tag)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEdgeOpenAIProviderAuthConf_NoRawTokenField(t *testing.T) {
|
|
allowed := map[string]struct{}{
|
|
"enabled": {},
|
|
"from_header": {},
|
|
"target_header": {},
|
|
"scheme": {},
|
|
"required": {},
|
|
}
|
|
typ := reflect.TypeOf(config.EdgeOpenAIProviderAuthConf{})
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
tag := typ.Field(i).Tag.Get("yaml")
|
|
key := strings.SplitN(tag, ",", 2)[0]
|
|
if _, ok := allowed[key]; !ok {
|
|
t.Fatalf("unexpected field %q (yaml tag %q) on EdgeOpenAIProviderAuthConf; raw provider tokens must not be stored in config", typ.Field(i).Name, tag)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OllamaContextSize(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://192.168.0.91:11434"
|
|
context_size: 262144
|
|
capacity: 3
|
|
max_queue: 8
|
|
queue_timeout_ms: 1500
|
|
request_timeout_ms: 30000
|
|
`
|
|
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)
|
|
}
|
|
ollama := cfg.Nodes[0].Adapters.Ollama
|
|
if !ollama.Enabled {
|
|
t.Fatal("expected ollama.enabled=true")
|
|
}
|
|
if ollama.BaseURL != "http://192.168.0.91:11434" || ollama.ContextSize != 262144 {
|
|
t.Fatalf("unexpected ollama config: %+v", ollama)
|
|
}
|
|
if ollama.Capacity != 3 || ollama.MaxQueue != 8 || ollama.QueueTimeoutMS != 1500 || ollama.RequestTimeoutMS != 30000 {
|
|
t.Fatalf("unexpected ollama queue config: %+v", ollama)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleSessionDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.SessionID != "default" {
|
|
t.Fatalf("expected default session_id=%q, got %q", "default", cfg.Console.SessionID)
|
|
}
|
|
if cfg.Console.Background {
|
|
t.Fatal("expected default background=false")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleSessionOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n session_id: \"worker-1\"\n background: true\n"
|
|
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 cfg.Console.SessionID != "worker-1" {
|
|
t.Fatalf("expected session_id=%q, got %q", "worker-1", cfg.Console.SessionID)
|
|
}
|
|
if !cfg.Console.Background {
|
|
t.Fatal("expected background=true")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTimeoutOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n timeout_sec: 45\n"
|
|
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 cfg.Console.TimeoutSec != 45 {
|
|
t.Fatalf("expected timeout_sec=45, got %d", cfg.Console.TimeoutSec)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTargetOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n target: \"codex\"\n"
|
|
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 cfg.Console.ResolveTarget() != "codex" {
|
|
t.Fatalf("expected console.target to resolve to %q, got %q", "codex", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTargetFallbackFromLegacyAgent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n agent: \"codex\"\n"
|
|
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 cfg.Console.ResolveTarget() != "codex" {
|
|
t.Fatalf("expected legacy console.agent fallback to resolve to %q, got %q", "codex", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTargetFallbackFromLegacyModel(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n model: \"codex\"\n"
|
|
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 cfg.Console.ResolveTarget() != "codex" {
|
|
t.Fatalf("expected legacy console.model fallback to resolve to %q, got %q", "codex", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_CodexProfile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
codex:
|
|
command: "codex"
|
|
output_format: "codex-json"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
codex, ok := cfg.Nodes[0].Adapters.CLI.Profiles["codex"]
|
|
if !ok {
|
|
t.Fatal("expected codex profile")
|
|
}
|
|
if codex.Command != "codex" || codex.OutputFormat != "codex-json" {
|
|
t.Errorf("unexpected codex profile: %+v", codex)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_AntigravityProfile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
antigravity:
|
|
command: "agy"
|
|
args:
|
|
- "--dangerously-skip-permissions"
|
|
- "--print-timeout"
|
|
- "10m"
|
|
- "--print"
|
|
resume_args:
|
|
- "--dangerously-skip-permissions"
|
|
- "--print-timeout"
|
|
- "10m"
|
|
- "--conversation"
|
|
mode: "antigravity-print"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
agy, ok := cfg.Nodes[0].Adapters.CLI.Profiles["antigravity"]
|
|
if !ok {
|
|
t.Fatal("expected antigravity profile")
|
|
}
|
|
if agy.Command != "agy" {
|
|
t.Errorf("expected command agy, got %q", agy.Command)
|
|
}
|
|
expectedArgs := []string{"--dangerously-skip-permissions", "--print-timeout", "10m", "--print"}
|
|
if len(agy.Args) != len(expectedArgs) {
|
|
t.Fatalf("expected %d args, got %d: %v", len(expectedArgs), len(agy.Args), agy.Args)
|
|
}
|
|
for i, v := range expectedArgs {
|
|
if agy.Args[i] != v {
|
|
t.Errorf("args[%d]: expected %q, got %q", i, v, agy.Args[i])
|
|
}
|
|
}
|
|
if agy.OutputFormat != "" {
|
|
t.Errorf("expected empty output format, got %q", agy.OutputFormat)
|
|
}
|
|
if agy.Mode != "antigravity-print" {
|
|
t.Errorf("expected mode antigravity-print, got %q", agy.Mode)
|
|
}
|
|
expectedResume := []string{"--dangerously-skip-permissions", "--print-timeout", "10m", "--conversation"}
|
|
if len(agy.ResumeArgs) != len(expectedResume) {
|
|
t.Fatalf("expected %d resume_args, got %d: %v", len(expectedResume), len(agy.ResumeArgs), agy.ResumeArgs)
|
|
}
|
|
for i, v := range expectedResume {
|
|
if agy.ResumeArgs[i] != v {
|
|
t.Errorf("resume_args[%d]: expected %q, got %q", i, v, agy.ResumeArgs[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLIProfileConf_CompletionMarkerUnmarshal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
marker-test:
|
|
command: "mycli"
|
|
completion_marker:
|
|
line: "<<END_OF_RESPONSE>>"
|
|
regex: "^DONE \\d+$"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["marker-test"]
|
|
if !ok {
|
|
t.Fatal("expected marker-test profile")
|
|
}
|
|
if profile.CompletionMarker.Line != "<<END_OF_RESPONSE>>" {
|
|
t.Errorf("expected Line=%q, got %q", "<<END_OF_RESPONSE>>", profile.CompletionMarker.Line)
|
|
}
|
|
if profile.CompletionMarker.Regex != "^DONE \\d+$" {
|
|
t.Errorf("expected Regex=%q, got %q", "^DONE \\d+$", profile.CompletionMarker.Regex)
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_Empty(t *testing.T) {
|
|
var empty config.CompletionMarkerConf
|
|
if !empty.Empty() {
|
|
t.Fatal("expected Empty() == true for zero value")
|
|
}
|
|
|
|
lineOnly := config.CompletionMarkerConf{Line: "<<END>>"}
|
|
if lineOnly.Empty() {
|
|
t.Fatal("expected Empty() == false when Line is set")
|
|
}
|
|
|
|
regexOnly := config.CompletionMarkerConf{Regex: "^END$"}
|
|
if regexOnly.Empty() {
|
|
t.Fatal("expected Empty() == false when Regex is set")
|
|
}
|
|
|
|
bothSet := config.CompletionMarkerConf{Line: "<<END>>", Regex: "^END$"}
|
|
if bothSet.Empty() {
|
|
t.Fatal("expected Empty() == false when both are set")
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_LineOnlyUnmarshal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
line-only:
|
|
command: "mycli"
|
|
completion_marker:
|
|
line: "<<END>>"
|
|
`
|
|
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)
|
|
}
|
|
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["line-only"]
|
|
if !ok {
|
|
t.Fatal("expected line-only profile")
|
|
}
|
|
if profile.CompletionMarker.Line != "<<END>>" {
|
|
t.Errorf("expected Line=%q, got %q", "<<END>>", profile.CompletionMarker.Line)
|
|
}
|
|
if profile.CompletionMarker.Regex != "" {
|
|
t.Errorf("expected empty Regex, got %q", profile.CompletionMarker.Regex)
|
|
}
|
|
if profile.CompletionMarker.Empty() {
|
|
t.Fatal("expected Empty() == false when only Line is set")
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_RegexOnlyUnmarshal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
regex-only:
|
|
command: "mycli"
|
|
completion_marker:
|
|
regex: "^DONE \\d+$"
|
|
`
|
|
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)
|
|
}
|
|
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["regex-only"]
|
|
if !ok {
|
|
t.Fatal("expected regex-only profile")
|
|
}
|
|
if profile.CompletionMarker.Regex != "^DONE \\d+$" {
|
|
t.Errorf("expected Regex=%q, got %q", "^DONE \\d+$", profile.CompletionMarker.Regex)
|
|
}
|
|
if profile.CompletionMarker.Line != "" {
|
|
t.Errorf("expected empty Line, got %q", profile.CompletionMarker.Line)
|
|
}
|
|
if profile.CompletionMarker.Empty() {
|
|
t.Fatal("expected Empty() == false when only Regex is set")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_CLIProfileMode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
codex:
|
|
command: "codex"
|
|
args:
|
|
- "exec"
|
|
- "--json"
|
|
resume_args:
|
|
- "exec"
|
|
- "resume"
|
|
- "--json"
|
|
mode: "codex-exec"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["codex"]
|
|
if !ok {
|
|
t.Fatal("expected codex profile")
|
|
}
|
|
if profile.Mode != "codex-exec" {
|
|
t.Errorf("expected mode=%q, got %q", "codex-exec", profile.Mode)
|
|
}
|
|
expectedResume := []string{"exec", "resume", "--json"}
|
|
if len(profile.ResumeArgs) != len(expectedResume) {
|
|
t.Fatalf("expected %d resume_args, got %d: %v", len(expectedResume), len(profile.ResumeArgs), profile.ResumeArgs)
|
|
}
|
|
for i, v := range expectedResume {
|
|
if profile.ResumeArgs[i] != v {
|
|
t.Errorf("resume_args[%d]: expected %q, got %q", i, v, profile.ResumeArgs[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpencodeSSEProfile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
opencode:
|
|
command: "/usr/local/bin/opencode"
|
|
args:
|
|
- "--title"
|
|
- "untitle"
|
|
- "--model"
|
|
- "ollama-dgx/qwen3.6:35b-a3b-bf16"
|
|
- "--dangerously-skip-permissions"
|
|
mode: "opencode-sse"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
prof, ok := cfg.Nodes[0].Adapters.CLI.Profiles["opencode"]
|
|
if !ok {
|
|
t.Fatal("expected opencode profile")
|
|
}
|
|
if prof.Mode != "opencode-sse" {
|
|
t.Errorf("mode: got %q", prof.Mode)
|
|
}
|
|
expectedArgs := []string{"--title", "untitle", "--model", "ollama-dgx/qwen3.6:35b-a3b-bf16", "--dangerously-skip-permissions"}
|
|
if len(prof.Args) != len(expectedArgs) {
|
|
t.Fatalf("args len: got %d, want %d", len(prof.Args), len(expectedArgs))
|
|
}
|
|
for i, want := range expectedArgs {
|
|
if prof.Args[i] != want {
|
|
t.Errorf("args[%d]: got %q, want %q", i, prof.Args[i], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLIProfileConf_CompletionMarkerEmpty(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
no-marker:
|
|
command: "mycli"
|
|
`
|
|
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)
|
|
}
|
|
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["no-marker"]
|
|
if !ok {
|
|
t.Fatal("expected no-marker profile")
|
|
}
|
|
if !profile.CompletionMarker.Empty() {
|
|
t.Fatal("expected Empty() == true when completion_marker is not specified")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_AdvertiseHostAndArtifactDefaultsEmpty(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Server.AdvertiseHost != "" {
|
|
t.Errorf("expected empty server.advertise_host by default, got %q", cfg.Server.AdvertiseHost)
|
|
}
|
|
if cfg.Bootstrap.ArtifactBaseURL != "" {
|
|
t.Errorf("expected empty bootstrap.artifact_base_url by default, got %q", cfg.Bootstrap.ArtifactBaseURL)
|
|
}
|
|
if cfg.Logging.Path != "" {
|
|
t.Errorf("expected empty logging.path by default, got %q", cfg.Logging.Path)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_AdvertiseHostAndArtifactExplicit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `server:
|
|
listen: "0.0.0.0:9090"
|
|
advertise_host: "edge.example.test"
|
|
bootstrap:
|
|
artifact_base_url: "http://edge.example.test:18080"
|
|
logging:
|
|
path: "/var/log/iop/edge.log"
|
|
`
|
|
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 cfg.Server.AdvertiseHost != "edge.example.test" {
|
|
t.Errorf("server.advertise_host=%q", cfg.Server.AdvertiseHost)
|
|
}
|
|
if cfg.Bootstrap.ArtifactBaseURL != "http://edge.example.test:18080" {
|
|
t.Errorf("bootstrap.artifact_base_url=%q", cfg.Bootstrap.ArtifactBaseURL)
|
|
}
|
|
if cfg.Logging.Path != "/var/log/iop/edge.log" {
|
|
t.Errorf("logging.path=%q", cfg.Logging.Path)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_A2ADefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.A2A.Enabled {
|
|
t.Fatal("expected a2a.enabled=false by default")
|
|
}
|
|
if cfg.A2A.Listen != "0.0.0.0:8081" {
|
|
t.Fatalf("expected a2a.listen default, got %q", cfg.A2A.Listen)
|
|
}
|
|
if cfg.A2A.Path != "/a2a" {
|
|
t.Fatalf("expected a2a.path=%q, got %q", "/a2a", cfg.A2A.Path)
|
|
}
|
|
if cfg.A2A.Adapter != "cli" {
|
|
t.Fatalf("expected a2a.adapter=%q, got %q", "cli", cfg.A2A.Adapter)
|
|
}
|
|
if cfg.A2A.SessionID != "a2a" {
|
|
t.Fatalf("expected a2a.session_id=%q, got %q", "a2a", cfg.A2A.SessionID)
|
|
}
|
|
if cfg.A2A.TimeoutSec != 120 {
|
|
t.Fatalf("expected a2a.timeout_sec=120, got %d", cfg.A2A.TimeoutSec)
|
|
}
|
|
if cfg.A2A.BearerToken != "" {
|
|
t.Fatalf("expected a2a.bearer_token empty by default, got %q", cfg.A2A.BearerToken)
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_RegexAndLineUsage(t *testing.T) {
|
|
lineOnly := config.CompletionMarkerConf{Line: "<<END>>"}
|
|
if !strings.Contains(lineOnly.Line, "<<END>>") {
|
|
t.Errorf("expected Line to contain <<END>>, got %q", lineOnly.Line)
|
|
}
|
|
|
|
regexOnly := config.CompletionMarkerConf{Regex: "^DONE \\d+$"}
|
|
if !strings.Contains(regexOnly.Regex, "DONE") {
|
|
t.Errorf("expected Regex to contain DONE, got %q", regexOnly.Regex)
|
|
}
|
|
|
|
both := config.CompletionMarkerConf{Line: "<<END>>", Regex: "^DONE \\d+$"}
|
|
if both.Line != "<<END>>" || both.Regex != "^DONE \\d+$" {
|
|
t.Errorf("expected both fields set, got %+v", both)
|
|
}
|
|
if both.Empty() {
|
|
t.Fatal("expected Empty() == false when both are set")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_A2AOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
a2a:
|
|
enabled: true
|
|
listen: "127.0.0.1:8082"
|
|
path: "/agent"
|
|
node: "node0"
|
|
adapter: "cli"
|
|
target: "claude"
|
|
session_id: "nomad"
|
|
timeout_sec: 60
|
|
bearer_token: "secret-token"
|
|
`
|
|
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 !cfg.A2A.Enabled {
|
|
t.Fatal("expected a2a.enabled=true")
|
|
}
|
|
if cfg.A2A.Listen != "127.0.0.1:8082" {
|
|
t.Fatalf("expected a2a.listen=%q, got %q", "127.0.0.1:8082", cfg.A2A.Listen)
|
|
}
|
|
if cfg.A2A.Path != "/agent" {
|
|
t.Fatalf("expected a2a.path=%q, got %q", "/agent", cfg.A2A.Path)
|
|
}
|
|
if cfg.A2A.NodeRef != "node0" {
|
|
t.Fatalf("expected a2a.node=%q, got %q", "node0", cfg.A2A.NodeRef)
|
|
}
|
|
if cfg.A2A.Target != "claude" {
|
|
t.Fatalf("expected a2a.target=%q, got %q", "claude", cfg.A2A.Target)
|
|
}
|
|
if cfg.A2A.SessionID != "nomad" {
|
|
t.Fatalf("expected a2a.session_id=%q, got %q", "nomad", cfg.A2A.SessionID)
|
|
}
|
|
if cfg.A2A.TimeoutSec != 60 {
|
|
t.Fatalf("expected a2a.timeout_sec=60, got %d", cfg.A2A.TimeoutSec)
|
|
}
|
|
if cfg.A2A.BearerToken != "secret-token" {
|
|
t.Fatalf("expected a2a.bearer_token=%q, got %q", "secret-token", cfg.A2A.BearerToken)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeAgentKindGeneric(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "node-local"
|
|
token: "token-node"
|
|
agent_kind: "generic-node"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
if cfg.Nodes[0].AgentKind != config.AgentKindGenericNode {
|
|
t.Fatalf("expected agent_kind=%q, got %q", config.AgentKindGenericNode, cfg.Nodes[0].AgentKind)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeAgentKindDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "node-local"
|
|
token: "token-node"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
if cfg.Nodes[0].AgentKind != config.AgentKindGenericNode {
|
|
t.Fatalf("expected default agent_kind=%q, got %q", config.AgentKindGenericNode, cfg.Nodes[0].AgentKind)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeAgentKindRejectsInvalid(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "bad-local"
|
|
token: "token-bad"
|
|
agent_kind: "not-a-kind"
|
|
`
|
|
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 invalid agent_kind error")
|
|
}
|
|
if !strings.Contains(err.Error(), "agent_kind") {
|
|
t.Fatalf("expected error to mention agent_kind, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderGlobalUniqueness(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "node-a"
|
|
providers:
|
|
- id: "shared-provider"
|
|
type: "ollama"
|
|
category: "cli"
|
|
models:
|
|
- "model-a"
|
|
- alias: "node-b"
|
|
providers:
|
|
- id: "shared-provider"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "model-b"
|
|
`
|
|
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 duplicate provider id error across nodes")
|
|
}
|
|
if !strings.Contains(err.Error(), "shared-provider") {
|
|
t.Fatalf("expected error to mention 'shared-provider', got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderEnabledDefaultsTrue(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "node-a"
|
|
token: "tok-a"
|
|
adapters:
|
|
vllm:
|
|
enabled: true
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
providers:
|
|
- id: "prov-default"
|
|
type: "vllm"
|
|
category: "api"
|
|
adapter: "vllm"
|
|
models: ["model-x"]
|
|
capacity: 2
|
|
- id: "prov-explicit-true"
|
|
type: "vllm"
|
|
category: "api"
|
|
adapter: "vllm"
|
|
models: ["model-y"]
|
|
capacity: 2
|
|
enabled: true
|
|
- id: "prov-explicit-false"
|
|
type: "vllm"
|
|
category: "api"
|
|
adapter: "vllm"
|
|
models: ["model-z"]
|
|
capacity: 2
|
|
enabled: false
|
|
`
|
|
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.Nodes) != 1 {
|
|
t.Fatalf("expected 1 node, got %d", len(cfg.Nodes))
|
|
}
|
|
provs := cfg.Nodes[0].Providers
|
|
if len(provs) != 3 {
|
|
t.Fatalf("expected 3 providers, got %d", len(provs))
|
|
}
|
|
byID := map[string]config.NodeProviderConf{}
|
|
for _, p := range provs {
|
|
byID[p.ID] = p
|
|
}
|
|
|
|
// prov-default: Enabled is nil → ProviderEnabled returns true.
|
|
def := byID["prov-default"]
|
|
if def.Enabled != nil {
|
|
t.Errorf("prov-default: expected Enabled=nil (omitted), got %v", def.Enabled)
|
|
}
|
|
if !config.ProviderEnabled(def) {
|
|
t.Error("prov-default: ProviderEnabled must be true when Enabled is nil")
|
|
}
|
|
|
|
// prov-explicit-true: Enabled is *true → ProviderEnabled returns true.
|
|
exTrue := byID["prov-explicit-true"]
|
|
if exTrue.Enabled == nil || !*exTrue.Enabled {
|
|
t.Errorf("prov-explicit-true: expected Enabled=*true, got %v", exTrue.Enabled)
|
|
}
|
|
if !config.ProviderEnabled(exTrue) {
|
|
t.Error("prov-explicit-true: ProviderEnabled must be true when Enabled=true")
|
|
}
|
|
|
|
// prov-explicit-false: Enabled is *false → ProviderEnabled returns false.
|
|
exFalse := byID["prov-explicit-false"]
|
|
if exFalse.Enabled == nil || *exFalse.Enabled {
|
|
t.Errorf("prov-explicit-false: expected Enabled=*false, got %v", exFalse.Enabled)
|
|
}
|
|
if config.ProviderEnabled(exFalse) {
|
|
t.Error("prov-explicit-false: ProviderEnabled must be false when Enabled=false")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ControlPlaneDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.ControlPlane.Enabled {
|
|
t.Fatal("expected control_plane.enabled=false by default")
|
|
}
|
|
if cfg.ControlPlane.WireAddr != "" {
|
|
t.Fatalf("expected control_plane.wire_addr empty by default, got %q", cfg.ControlPlane.WireAddr)
|
|
}
|
|
if cfg.ControlPlane.ReconnectIntervalSec != 5 {
|
|
t.Fatalf("expected control_plane.reconnect_interval_sec=5, got %d", cfg.ControlPlane.ReconnectIntervalSec)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_MultiOllamaInstances(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "multi"
|
|
adapters:
|
|
ollama_instances:
|
|
- name: "local"
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
context_size: 131072
|
|
capacity: 2
|
|
max_queue: 4
|
|
queue_timeout_ms: 1000
|
|
request_timeout_ms: 20000
|
|
- name: "dgx"
|
|
enabled: true
|
|
base_url: "http://192.168.0.91:11434"
|
|
context_size: 262144
|
|
capacity: 6
|
|
max_queue: 12
|
|
queue_timeout_ms: 2000
|
|
request_timeout_ms: 60000
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
insts := cfg.Nodes[0].Adapters.OllamaInstances
|
|
if len(insts) != 2 {
|
|
t.Fatalf("expected 2 ollama instances, got %d", len(insts))
|
|
}
|
|
if insts[0].Name != "local" || insts[0].BaseURL != "http://127.0.0.1:11434" || insts[0].ContextSize != 131072 {
|
|
t.Errorf("unexpected instance[0]: %+v", insts[0])
|
|
}
|
|
if insts[0].Capacity != 2 || insts[0].MaxQueue != 4 || insts[0].QueueTimeoutMS != 1000 || insts[0].RequestTimeoutMS != 20000 {
|
|
t.Errorf("unexpected instance[0] queue config: %+v", insts[0])
|
|
}
|
|
if insts[1].Name != "dgx" || insts[1].BaseURL != "http://192.168.0.91:11434" || insts[1].ContextSize != 262144 {
|
|
t.Errorf("unexpected instance[1]: %+v", insts[1])
|
|
}
|
|
if insts[1].Capacity != 6 || insts[1].MaxQueue != 12 || insts[1].QueueTimeoutMS != 2000 || insts[1].RequestTimeoutMS != 60000 {
|
|
t.Errorf("unexpected instance[1] queue config: %+v", insts[1])
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_MultiVllmInstances(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "multi-vllm"
|
|
adapters:
|
|
vllm_instances:
|
|
- name: "a100"
|
|
enabled: true
|
|
endpoint: "http://10.0.0.5:8000"
|
|
capacity: 4
|
|
max_queue: 10
|
|
queue_timeout_ms: 1500
|
|
request_timeout_ms: 45000
|
|
- name: "h100"
|
|
enabled: true
|
|
endpoint: "http://10.0.0.6:8000"
|
|
capacity: 8
|
|
max_queue: 16
|
|
queue_timeout_ms: 2500
|
|
request_timeout_ms: 90000
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
insts := cfg.Nodes[0].Adapters.VllmInstances
|
|
if len(insts) != 2 {
|
|
t.Fatalf("expected 2 vllm instances, got %d", len(insts))
|
|
}
|
|
if insts[0].Name != "a100" || insts[0].Endpoint != "http://10.0.0.5:8000" {
|
|
t.Errorf("unexpected instance[0]: %+v", insts[0])
|
|
}
|
|
if insts[0].Capacity != 4 || insts[0].MaxQueue != 10 || insts[0].QueueTimeoutMS != 1500 || insts[0].RequestTimeoutMS != 45000 {
|
|
t.Errorf("unexpected instance[0] queue config: %+v", insts[0])
|
|
}
|
|
if insts[1].Name != "h100" || insts[1].Endpoint != "http://10.0.0.6:8000" {
|
|
t.Errorf("unexpected instance[1]: %+v", insts[1])
|
|
}
|
|
if insts[1].Capacity != 8 || insts[1].MaxQueue != 16 || insts[1].QueueTimeoutMS != 2500 || insts[1].RequestTimeoutMS != 90000 {
|
|
t.Errorf("unexpected instance[1] queue config: %+v", insts[1])
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_LegacyOllamaPromotedToInstances(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://192.168.0.91:11434"
|
|
context_size: 262144
|
|
capacity: 5
|
|
max_queue: 9
|
|
queue_timeout_ms: 1700
|
|
request_timeout_ms: 55000
|
|
`
|
|
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)
|
|
}
|
|
insts := cfg.Nodes[0].Adapters.OllamaInstances
|
|
if len(insts) != 1 {
|
|
t.Fatalf("expected 1 ollama instance from legacy field, got %d", len(insts))
|
|
}
|
|
if insts[0].Name != "ollama" {
|
|
t.Errorf("expected instance name %q, got %q", "ollama", insts[0].Name)
|
|
}
|
|
if insts[0].BaseURL != "http://192.168.0.91:11434" {
|
|
t.Errorf("expected base_url %q, got %q", "http://192.168.0.91:11434", insts[0].BaseURL)
|
|
}
|
|
if insts[0].Capacity != 5 || insts[0].MaxQueue != 9 || insts[0].QueueTimeoutMS != 1700 || insts[0].RequestTimeoutMS != 55000 {
|
|
t.Errorf("unexpected promoted queue config: %+v", insts[0])
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_DuplicateOllamaInstanceNameRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "dup"
|
|
adapters:
|
|
ollama_instances:
|
|
- name: "same"
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
- name: "same"
|
|
enabled: true
|
|
base_url: "http://127.0.0.2:11434"
|
|
`
|
|
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 duplicate ollama instance name")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ProviderQueueConfigRejectsNegative(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "bad-queue"
|
|
adapters:
|
|
ollama_instances:
|
|
- name: "local"
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
capacity: -1
|
|
`
|
|
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 negative provider queue config error")
|
|
}
|
|
if !strings.Contains(err.Error(), "capacity") || !strings.Contains(err.Error(), "non-negative") {
|
|
t.Fatalf("expected error to mention capacity non-negative, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIRouteCatalog(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
listen: "0.0.0.0:18081"
|
|
adapter: "ollama"
|
|
model_routes:
|
|
- model: "model-a"
|
|
adapter: "ollama"
|
|
target: "llama3"
|
|
node: "node-01"
|
|
session_id: "sess-a"
|
|
timeout_sec: 30
|
|
max_queue: 10
|
|
queue_timeout_ms: 5000
|
|
- model: "model-b"
|
|
adapter: "vllm"
|
|
target: "qwen"
|
|
`
|
|
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.OpenAI.ModelRoutes) != 2 {
|
|
t.Fatalf("expected 2 model_routes, got %d", len(cfg.OpenAI.ModelRoutes))
|
|
}
|
|
r0 := cfg.OpenAI.ModelRoutes[0]
|
|
if r0.Model != "model-a" || r0.Adapter != "ollama" || r0.Target != "llama3" {
|
|
t.Errorf("route[0] mismatch: %+v", r0)
|
|
}
|
|
if r0.NodeRef != "node-01" || r0.SessionID != "sess-a" || r0.TimeoutSec != 30 {
|
|
t.Errorf("route[0] optional fields mismatch: %+v", r0)
|
|
}
|
|
if r0.MaxQueue != 10 || r0.QueueTimeoutMS != 5000 {
|
|
t.Errorf("route[0] queue policy mismatch: max_queue=%d, queue_timeout_ms=%d", r0.MaxQueue, r0.QueueTimeoutMS)
|
|
}
|
|
if r0.WorkspaceRequired {
|
|
t.Errorf("route[0] workspace_required should default to false: %+v", r0)
|
|
}
|
|
r1 := cfg.OpenAI.ModelRoutes[1]
|
|
if r1.Model != "model-b" || r1.Adapter != "vllm" || r1.Target != "qwen" {
|
|
t.Errorf("route[1] mismatch: %+v", r1)
|
|
}
|
|
if r1.MaxQueue != 0 || r1.QueueTimeoutMS != 0 {
|
|
t.Errorf("route[1] queue policy should default to 0: max_queue=%d, queue_timeout_ms=%d", r1.MaxQueue, r1.QueueTimeoutMS)
|
|
}
|
|
if r1.WorkspaceRequired {
|
|
t.Errorf("route[1] workspace_required should default to false: %+v", r1)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIRouteCatalogWorkspaceRequired(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
model_routes:
|
|
- model: "codex"
|
|
adapter: "cli"
|
|
target: "codex"
|
|
workspace_required: true
|
|
- model: "llama3"
|
|
adapter: "ollama"
|
|
target: "llama3:8b"
|
|
`
|
|
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.OpenAI.ModelRoutes) != 2 {
|
|
t.Fatalf("expected 2 model_routes, got %d", len(cfg.OpenAI.ModelRoutes))
|
|
}
|
|
agent := cfg.OpenAI.ModelRoutes[0]
|
|
if agent.Model != "codex" || agent.Adapter != "cli" || agent.Target != "codex" {
|
|
t.Errorf("agent route mismatch: %+v", agent)
|
|
}
|
|
if !agent.WorkspaceRequired {
|
|
t.Errorf("agent route workspace_required should be true: %+v", agent)
|
|
}
|
|
inference := cfg.OpenAI.ModelRoutes[1]
|
|
if inference.Model != "llama3" || inference.Adapter != "ollama" || inference.Target != "llama3:8b" {
|
|
t.Errorf("inference route mismatch: %+v", inference)
|
|
}
|
|
if inference.WorkspaceRequired {
|
|
t.Errorf("inference route workspace_required should be false: %+v", inference)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIRouteCatalogDuplicateModelRejects(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
model_routes:
|
|
- model: "model-a"
|
|
adapter: "ollama"
|
|
target: "llama3"
|
|
- model: "model-a"
|
|
adapter: "vllm"
|
|
target: "qwen"
|
|
`
|
|
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 duplicate model route")
|
|
}
|
|
if !strings.Contains(err.Error(), "model_routes") || !strings.Contains(err.Error(), "model-a") {
|
|
t.Fatalf("expected error mentioning model_routes and model-a, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIRouteCatalogEmptyModelRejects(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
model_routes:
|
|
- model: ""
|
|
target: "llama3"
|
|
`
|
|
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 model in route catalog")
|
|
}
|
|
if !strings.Contains(err.Error(), "model_routes") {
|
|
t.Fatalf("expected error mentioning model_routes, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIRouteCatalogQueuePolicyRejectsNegative(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Test negative max_queue
|
|
f1 := filepath.Join(dir, "edge_neg_max.yaml")
|
|
yaml1 := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
model_routes:
|
|
- model: "model-a"
|
|
target: "llama3"
|
|
max_queue: -1
|
|
`
|
|
if err := os.WriteFile(f1, []byte(yaml1), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f1)
|
|
if err == nil {
|
|
t.Fatal("expected error for negative max_queue")
|
|
}
|
|
if !strings.Contains(err.Error(), "max_queue must be non-negative") {
|
|
t.Fatalf("expected error mentioning max_queue must be non-negative, got %v", err)
|
|
}
|
|
|
|
// Test negative queue_timeout_ms
|
|
f2 := filepath.Join(dir, "edge_neg_timeout.yaml")
|
|
yaml2 := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
model_routes:
|
|
- model: "model-a"
|
|
target: "llama3"
|
|
queue_timeout_ms: -500
|
|
`
|
|
if err := os.WriteFile(f2, []byte(yaml2), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err = config.LoadEdge(f2)
|
|
if err == nil {
|
|
t.Fatal("expected error for negative queue_timeout_ms")
|
|
}
|
|
if !strings.Contains(err.Error(), "queue_timeout_ms must be non-negative") {
|
|
t.Fatalf("expected error mentioning queue_timeout_ms must be non-negative, got %v", err)
|
|
}
|
|
}
|
|
|
|
// NormalizeAdapters regression tests
|
|
|
|
func TestNormalizeAdapters_OllamaIdempotent(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://127.0.0.1:11434", ContextSize: 4096},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err != nil {
|
|
t.Fatalf("first normalize: %v", err)
|
|
}
|
|
if len(a.OllamaInstances) != 1 {
|
|
t.Fatalf("expected 1 instance after first normalize, got %d", len(a.OllamaInstances))
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err != nil {
|
|
t.Fatalf("second normalize (idempotent): %v", err)
|
|
}
|
|
if len(a.OllamaInstances) != 1 {
|
|
t.Fatalf("expected still 1 instance after second normalize, got %d", len(a.OllamaInstances))
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_VllmIdempotent(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
Vllm: config.VllmConf{Enabled: true, Endpoint: "http://127.0.0.1:8000"},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err != nil {
|
|
t.Fatalf("first normalize: %v", err)
|
|
}
|
|
if len(a.VllmInstances) != 1 {
|
|
t.Fatalf("expected 1 instance after first normalize, got %d", len(a.VllmInstances))
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err != nil {
|
|
t.Fatalf("second normalize (idempotent): %v", err)
|
|
}
|
|
if len(a.VllmInstances) != 1 {
|
|
t.Fatalf("expected still 1 instance after second normalize, got %d", len(a.VllmInstances))
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_OllamaConflictErrors(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://127.0.0.1:11434"},
|
|
OllamaInstances: []config.OllamaInstanceConf{
|
|
{Name: "ollama", Enabled: true, BaseURL: "http://other-host:11434"},
|
|
},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err == nil {
|
|
t.Fatal("expected error for conflicting legacy ollama and explicit instance with same name")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_VllmConflictErrors(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
Vllm: config.VllmConf{Enabled: true, Endpoint: "http://127.0.0.1:8000"},
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm", Enabled: true, Endpoint: "http://other-host:8000"},
|
|
},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err == nil {
|
|
t.Fatal("expected error for conflicting legacy vllm and explicit instance with same name")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_OllamaDisabledSameNameErrors(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://127.0.0.1:11434"},
|
|
OllamaInstances: []config.OllamaInstanceConf{
|
|
{Name: "ollama", Enabled: false, BaseURL: "http://127.0.0.1:11434"},
|
|
},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err == nil {
|
|
t.Fatal("expected error for legacy enabled ollama conflicting with same-name disabled explicit instance")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_VllmDisabledSameNameErrors(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
Vllm: config.VllmConf{Enabled: true, Endpoint: "http://127.0.0.1:8000"},
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm", Enabled: false, Endpoint: "http://127.0.0.1:8000"},
|
|
},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err == nil {
|
|
t.Fatal("expected error for legacy enabled vllm conflicting with same-name disabled explicit instance")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_CodexAppServerDefaultProfile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
codex:
|
|
command: "codex"
|
|
args:
|
|
- "app-server"
|
|
mode: "codex-app-server"
|
|
codex-exec:
|
|
command: "codex"
|
|
args:
|
|
- "exec"
|
|
- "--json"
|
|
resume_args:
|
|
- "exec"
|
|
- "resume"
|
|
- "--json"
|
|
output_format: "codex-json"
|
|
mode: "codex-exec"
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
profiles := cfg.Nodes[0].Adapters.CLI.Profiles
|
|
|
|
codex, ok := profiles["codex"]
|
|
if !ok {
|
|
t.Fatal("expected codex profile")
|
|
}
|
|
if codex.Mode != "codex-app-server" {
|
|
t.Errorf("codex.Mode = %q, want %q", codex.Mode, "codex-app-server")
|
|
}
|
|
if len(codex.Args) != 1 || codex.Args[0] != "app-server" {
|
|
t.Errorf("codex.Args = %v, want [app-server]", codex.Args)
|
|
}
|
|
|
|
exec, ok := profiles["codex-exec"]
|
|
if !ok {
|
|
t.Fatal("expected codex-exec profile")
|
|
}
|
|
if exec.Mode != "codex-exec" {
|
|
t.Errorf("codex-exec.Mode = %q, want %q", exec.Mode, "codex-exec")
|
|
}
|
|
if exec.OutputFormat != "codex-json" {
|
|
t.Errorf("codex-exec.OutputFormat = %q, want %q", exec.OutputFormat, "codex-json")
|
|
}
|
|
expectedResume := []string{"exec", "resume", "--json"}
|
|
if len(exec.ResumeArgs) != len(expectedResume) {
|
|
t.Fatalf("codex-exec.ResumeArgs = %v, want %v", exec.ResumeArgs, expectedResume)
|
|
}
|
|
for i, v := range expectedResume {
|
|
if exec.ResumeArgs[i] != v {
|
|
t.Errorf("codex-exec.ResumeArgs[%d] = %q, want %q", i, exec.ResumeArgs[i], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ControlPlaneOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
control_plane:
|
|
enabled: true
|
|
wire_addr: "cp.example.test:7070"
|
|
reconnect_interval_sec: 10
|
|
`
|
|
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 !cfg.ControlPlane.Enabled {
|
|
t.Fatal("expected control_plane.enabled=true")
|
|
}
|
|
if cfg.ControlPlane.WireAddr != "cp.example.test:7070" {
|
|
t.Fatalf("expected control_plane.wire_addr=%q, got %q", "cp.example.test:7070", cfg.ControlPlane.WireAddr)
|
|
}
|
|
if cfg.ControlPlane.ReconnectIntervalSec != 10 {
|
|
t.Fatalf("expected control_plane.reconnect_interval_sec=10, got %d", cfg.ControlPlane.ReconnectIntervalSec)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAICompatInstances(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "lemonade-node"
|
|
adapters:
|
|
openai_compat_instances:
|
|
- name: "lemonade"
|
|
enabled: true
|
|
provider: "lemonade"
|
|
endpoint: "http://127.0.0.1:13305"
|
|
headers:
|
|
Authorization: "Bearer test-key"
|
|
capacity: 4
|
|
max_queue: 10
|
|
queue_timeout_ms: 1500
|
|
request_timeout_ms: 30000
|
|
`
|
|
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.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
insts := cfg.Nodes[0].Adapters.OpenAICompatInstances
|
|
if len(insts) != 1 {
|
|
t.Fatalf("expected 1 openai_compat instance, got %d", len(insts))
|
|
}
|
|
inst := insts[0]
|
|
if inst.Name != "lemonade" || inst.Provider != "lemonade" || inst.Endpoint != "http://127.0.0.1:13305" {
|
|
t.Errorf("unexpected instance config: %+v", inst)
|
|
}
|
|
if inst.Headers["authorization"] != "Bearer test-key" {
|
|
t.Errorf("expected header authorization Bearer test-key, got %q", inst.Headers["authorization"])
|
|
}
|
|
if inst.Capacity != 4 || inst.MaxQueue != 10 || inst.QueueTimeoutMS != 1500 || inst.RequestTimeoutMS != 30000 {
|
|
t.Errorf("unexpected queue config: %+v", inst)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_OpenAICompatIdempotent(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
OpenAICompat: config.OpenAICompatConf{
|
|
Enabled: true,
|
|
Provider: "lemonade",
|
|
Endpoint: "http://127.0.0.1:13305",
|
|
Headers: map[string]string{"X-Test": "val"},
|
|
},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err != nil {
|
|
t.Fatalf("first normalize: %v", err)
|
|
}
|
|
if len(a.OpenAICompatInstances) != 1 {
|
|
t.Fatalf("expected 1 instance after first normalize, got %d", len(a.OpenAICompatInstances))
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err != nil {
|
|
t.Fatalf("second normalize (idempotent): %v", err)
|
|
}
|
|
if len(a.OpenAICompatInstances) != 1 {
|
|
t.Fatalf("expected still 1 instance after second normalize, got %d", len(a.OpenAICompatInstances))
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_OpenAICompatConflictErrors(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
OpenAICompat: config.OpenAICompatConf{Enabled: true, Endpoint: "http://127.0.0.1:13305"},
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "openai_compat", Enabled: true, Endpoint: "http://other-host:13305"},
|
|
},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err == nil {
|
|
t.Fatal("expected error for conflicting legacy openai_compat and explicit instance with same name")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_OpenAICompatDuplicateNameRejects(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "same", Enabled: true, Endpoint: "http://127.0.0.1:13305"},
|
|
{Name: "same", Enabled: true, Endpoint: "http://127.0.0.2:13305"},
|
|
},
|
|
}
|
|
if err := config.NormalizeAdapters(&a); err == nil {
|
|
t.Fatal("expected error for duplicate openai_compat instance name")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdapters_OpenAICompatQueueValidation(t *testing.T) {
|
|
a := config.AdaptersConf{
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "test", Enabled: true, Endpoint: "http://127.0.0.1:13305", Capacity: -1},
|
|
},
|
|
}
|
|
err := config.NormalizeAdapters(&a)
|
|
if err == nil {
|
|
t.Fatal("expected negative provider queue config error")
|
|
}
|
|
if !strings.Contains(err.Error(), "capacity") {
|
|
t.Fatalf("expected error to mention capacity, got %v", err)
|
|
}
|
|
}
|
|
|
|
// S02 REVIEW_VLLM_CONFIG: vLLM OpenAI-compatible route + instance fixture
|
|
// verifies that alias/target/provider/endpoint/no-headers/timeout/queue are
|
|
// preserved through config load and mapper/factory contract.
|
|
// Provider pool schema validation tests (G06)
|
|
|
|
func TestLoadEdge_ModelCatalogBasicHappyPath(t *testing.T) {
|
|
// Happy path: models[].providers values must match nodes[].providers[].models.
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
display_name: "Qwen 3.6 35B"
|
|
context_window_tokens: 262144
|
|
default_max_tokens: 32768
|
|
min_max_tokens: 32768
|
|
default_thinking_token_budget: 8192
|
|
providers:
|
|
vllm-gpu: "nvidia/Qwen3.6-35B-A3B-NVFP4"
|
|
ollama-local: "qwen3.6:35b"
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
alias: "gpu-node"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B-A3B-NVFP4"
|
|
capacity: 4
|
|
- id: "ollama-local"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models:
|
|
- "qwen3.6:35b"
|
|
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 catalog entry, got %d", len(cfg.Models))
|
|
}
|
|
m := cfg.Models[0]
|
|
if m.ID != "qwen3.6:35b" {
|
|
t.Errorf("models[0].ID = %q, want %q", m.ID, "qwen3.6:35b")
|
|
}
|
|
if m.DisplayName != "Qwen 3.6 35B" {
|
|
t.Errorf("models[0].DisplayName = %q, want %q", m.DisplayName, "Qwen 3.6 35B")
|
|
}
|
|
if m.ContextWindowTokens != 262144 {
|
|
t.Errorf("models[0].ContextWindowTokens = %d, want 262144", m.ContextWindowTokens)
|
|
}
|
|
if m.DefaultMaxTokens != 32768 {
|
|
t.Errorf("models[0].DefaultMaxTokens = %d, want 32768", m.DefaultMaxTokens)
|
|
}
|
|
if m.MinMaxTokens != 32768 {
|
|
t.Errorf("models[0].MinMaxTokens = %d, want 32768", m.MinMaxTokens)
|
|
}
|
|
if m.DefaultThinkingTokenBudget != 8192 {
|
|
t.Errorf("models[0].DefaultThinkingTokenBudget = %d, want 8192", m.DefaultThinkingTokenBudget)
|
|
}
|
|
if len(m.Providers) != 2 {
|
|
t.Fatalf("expected 2 providers, got %d", len(m.Providers))
|
|
}
|
|
if m.Providers["vllm-gpu"] != "nvidia/Qwen3.6-35B-A3B-NVFP4" {
|
|
t.Errorf("providers[vllm-gpu] = %q, want %q", m.Providers["vllm-gpu"], "nvidia/Qwen3.6-35B-A3B-NVFP4")
|
|
}
|
|
if m.Providers["ollama-local"] != "qwen3.6:35b" {
|
|
t.Errorf("providers[ollama-local] = %q, want %q", m.Providers["ollama-local"], "qwen3.6:35b")
|
|
}
|
|
if len(cfg.Nodes) != 1 {
|
|
t.Fatalf("expected 1 node, got %d", len(cfg.Nodes))
|
|
}
|
|
if len(cfg.Nodes[0].Providers) != 2 {
|
|
t.Fatalf("expected 2 node providers, got %d", len(cfg.Nodes[0].Providers))
|
|
}
|
|
// Check category resolution.
|
|
for _, p := range cfg.Nodes[0].Providers {
|
|
if p.ID == "vllm-gpu" && p.Category != config.CategoryAPI {
|
|
t.Errorf("providers[vllm-gpu].Category = %q, want %q", p.Category, config.CategoryAPI)
|
|
}
|
|
if p.ID == "ollama-local" && p.Category != config.CategoryLocalInference {
|
|
t.Errorf("providers[ollama-local].Category = %q, want %q", p.Category, config.CategoryLocalInference)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_SeulgivibeProviderFirstAliases(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
provider_auth:
|
|
enabled: true
|
|
models:
|
|
- id: "claude-sonnet-4-5"
|
|
providers:
|
|
seulgivibe-claude: "claude-sonnet-4-5"
|
|
- id: "claude-opus-4-8"
|
|
providers:
|
|
seulgivibe-claude: "claude-opus-4-8"
|
|
- id: "claude-fable-5"
|
|
providers:
|
|
seulgivibe-claude: "claude-fable-5"
|
|
- id: "gpt-5.1"
|
|
providers:
|
|
seulgivibe-openai: "gpt-5.1"
|
|
- id: "gpt-5.5"
|
|
providers:
|
|
seulgivibe-openai: "gpt-5.5"
|
|
nodes:
|
|
- id: "node-seulgivibe"
|
|
alias: "seulgivibe-node"
|
|
providers:
|
|
- id: "seulgivibe-claude"
|
|
type: "seulgivibe_claude"
|
|
category: "api"
|
|
endpoint: "https://seulgivibe.example.invalid/anthropic/v1"
|
|
models:
|
|
- "claude-sonnet-4-5"
|
|
- "claude-opus-4-8"
|
|
- "claude-fable-5"
|
|
capacity: 4
|
|
- id: "seulgivibe-openai"
|
|
type: "seulgivibe_openai"
|
|
category: "api"
|
|
endpoint: "https://seulgivibe.example.invalid/openai/v1"
|
|
models:
|
|
- "gpt-5.1"
|
|
- "gpt-5.5"
|
|
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) != 5 {
|
|
t.Fatalf("expected 5 Seulgivibe model catalog entries, got %d", len(cfg.Models))
|
|
}
|
|
if got := config.NormalizeProviderType(cfg.Nodes[0].Providers[0].Type); got != "openai_compat" {
|
|
t.Fatalf("seulgivibe_claude should normalize to openai_compat, got %q", got)
|
|
}
|
|
if got := config.NormalizeProviderType(cfg.Nodes[0].Providers[1].Type); got != "openai_compat" {
|
|
t.Fatalf("seulgivibe_openai should normalize to openai_compat, got %q", got)
|
|
}
|
|
auth := cfg.OpenAI.ProviderAuth
|
|
if !auth.Enabled || auth.FromHeader != "X-IOP-Provider-Authorization" || auth.TargetHeader != "Authorization" || auth.Scheme != "Bearer" || !auth.Required {
|
|
t.Fatalf("unexpected provider_auth defaults: %+v", auth)
|
|
}
|
|
for _, m := range cfg.Models {
|
|
for providerID, servedModel := range m.Providers {
|
|
if strings.TrimSpace(servedModel) == "" {
|
|
t.Fatalf("model %q provider %q has empty served model", m.ID, providerID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogEmptyModelIDRejected(t *testing.T) {
|
|
// No models[] on node, so serve model check is skipped (legacy compat).
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: ""
|
|
providers:
|
|
vllm-gpu: "model-a"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
`
|
|
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 model id")
|
|
}
|
|
if !strings.Contains(err.Error(), "models[0]") || !strings.Contains(err.Error(), "id must not be empty") {
|
|
t.Fatalf("expected error mentioning models[0] and id must not be empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogDuplicateModelIDRejected(t *testing.T) {
|
|
// serve model membership fail → 다른 error로 먼저 실패해야 하지만 duplicate 체크가
|
|
// 먼저 발생하므로 models validation 전에 seen check가 먼저 통과하는 fixture.
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
vllm-gpu: "model-a"
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
ollama: "qwen3.6"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "model-a"
|
|
- id: "ollama"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models:
|
|
- "qwen3.6"
|
|
`
|
|
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 duplicate model id")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate model id") {
|
|
t.Fatalf("expected error mentioning duplicate model id, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogEmptyProvidersRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers: {}
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
`
|
|
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 providers")
|
|
}
|
|
if !strings.Contains(err.Error(), "providers must not be empty") {
|
|
t.Fatalf("expected error mentioning providers must not be empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogContextWindowRejectsNegative(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
context_window_tokens: -1
|
|
providers:
|
|
vllm-gpu: "model-a"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "model-a"
|
|
`
|
|
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 negative context_window_tokens")
|
|
}
|
|
if !strings.Contains(err.Error(), "context_window_tokens must be non-negative") {
|
|
t.Fatalf("expected error mentioning context_window_tokens, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogServedModelMembershipMissing(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
vllm-gpu: "typo-model"
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B-A3B-NVFP4"
|
|
`
|
|
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 served model not in provider's models list")
|
|
}
|
|
if !strings.Contains(err.Error(), "typo-model") || !strings.Contains(err.Error(), "vllm-gpu") {
|
|
t.Fatalf("expected error mentioning typo-model and vllm-gpu, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogUnresolvedProviderRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
non-existent: "model-a"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
`
|
|
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 unresolved provider id")
|
|
}
|
|
if !strings.Contains(err.Error(), "does not resolve") || !strings.Contains(err.Error(), "non-existent") {
|
|
t.Fatalf("expected error mentioning does not resolve and non-existent, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogEmptyProviderIDRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
"": "model-a"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
`
|
|
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 provider id in models[].providers")
|
|
}
|
|
if !strings.Contains(err.Error(), "provider id must not be empty") {
|
|
t.Fatalf("expected error mentioning provider id must not be empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ModelCatalogEmptyServedModelRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
vllm-gpu: ""
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
`
|
|
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 served model name")
|
|
}
|
|
if !strings.Contains(err.Error(), "served model name must not be empty") {
|
|
t.Fatalf("expected error mentioning served model name must not be empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderConfEmptyIDRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: ""
|
|
type: "vllm"
|
|
category: "api"
|
|
`
|
|
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 node provider id")
|
|
}
|
|
if !strings.Contains(err.Error(), "providers[].id must not be empty") {
|
|
t.Fatalf("expected error mentioning providers[].id must not be empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderConfEmptyTypeRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "p1"
|
|
type: ""
|
|
category: "api"
|
|
`
|
|
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 provider type")
|
|
}
|
|
if !strings.Contains(err.Error(), "type must not be empty") {
|
|
t.Fatalf("expected error mentioning type must not be empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderConfInvalidCategoryRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "p1"
|
|
type: "vllm"
|
|
category: "invalid_category"
|
|
`
|
|
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 invalid provider category")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid provider category") {
|
|
t.Fatalf("expected error mentioning invalid provider category, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderConfDuplicateIDRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "p1"
|
|
type: "vllm"
|
|
category: "api"
|
|
- id: "p1"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
`
|
|
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 duplicate provider id within a node")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate provider id") {
|
|
t.Fatalf("expected error mentioning duplicate provider id, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderConfNegativeCapacityRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "p1"
|
|
type: "vllm"
|
|
category: "api"
|
|
capacity: -1
|
|
`
|
|
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 negative provider capacity")
|
|
}
|
|
if !strings.Contains(err.Error(), "capacity must be non-negative") {
|
|
t.Fatalf("expected error mentioning capacity must be non-negative, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_NodeProviderConfEmptyServedModelRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "p1"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- ""
|
|
- "real-model"
|
|
`
|
|
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 served model in provider models list")
|
|
}
|
|
if !strings.Contains(err.Error(), "served model name must not be empty") {
|
|
t.Fatalf("expected error mentioning served model name must not be empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestModelCatalogEntry_Validate(t *testing.T) {
|
|
providerIDs := map[string]struct{}{
|
|
"vllm-gpu": {},
|
|
"ollama-loc": {},
|
|
}
|
|
|
|
// Happy path with serveModels
|
|
serveModels := map[string]map[string]struct{}{
|
|
"vllm-gpu": {"nvidia/Qwen3.6-35B": {}},
|
|
"ollama-loc": {"qwen3.6:35b": {}},
|
|
}
|
|
e := config.ModelCatalogEntry{
|
|
ID: "qwen3.6:35b",
|
|
DisplayName: "Qwen 3.6 35B",
|
|
ContextWindowTokens: 262144,
|
|
DefaultMaxTokens: 32768,
|
|
MinMaxTokens: 32768,
|
|
DefaultThinkingTokenBudget: 8192,
|
|
Providers: map[string]string{
|
|
"vllm-gpu": "nvidia/Qwen3.6-35B",
|
|
"ollama-loc": "qwen3.6:35b",
|
|
},
|
|
}
|
|
if err := e.Validate(providerIDs, serveModels); err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
// Happy path without serveModels (legacy behavior)
|
|
e5 := config.ModelCatalogEntry{
|
|
ID: "test",
|
|
Providers: map[string]string{"vllm-gpu": "m"},
|
|
}
|
|
if err := e5.Validate(providerIDs, nil); err != nil {
|
|
t.Fatalf("expected no error with nil serveModels, got: %v", err)
|
|
}
|
|
|
|
// Trimmed empty ID
|
|
e2 := config.ModelCatalogEntry{
|
|
ID: " ",
|
|
Providers: map[string]string{"vllm-gpu": "m"},
|
|
}
|
|
if err := e2.Validate(providerIDs, serveModels); err == nil {
|
|
t.Fatal("expected error for trimmed empty ID")
|
|
}
|
|
|
|
// Empty providers map
|
|
e3 := config.ModelCatalogEntry{
|
|
ID: "test",
|
|
Providers: map[string]string{},
|
|
}
|
|
if err := e3.Validate(providerIDs, serveModels); err == nil {
|
|
t.Fatal("expected error for empty providers map")
|
|
}
|
|
|
|
// Unresolved provider
|
|
e4 := config.ModelCatalogEntry{
|
|
ID: "test",
|
|
Providers: map[string]string{
|
|
"unknown-provider": "m",
|
|
},
|
|
}
|
|
if err := e4.Validate(providerIDs, serveModels); err == nil {
|
|
t.Fatal("expected error for unresolved provider")
|
|
}
|
|
|
|
// Negative and inconsistent generation policy
|
|
for _, tc := range []struct {
|
|
name string
|
|
entry config.ModelCatalogEntry
|
|
}{
|
|
{
|
|
name: "negative context window tokens",
|
|
entry: config.ModelCatalogEntry{
|
|
ID: "test",
|
|
ContextWindowTokens: -1,
|
|
Providers: map[string]string{"vllm-gpu": "m"},
|
|
},
|
|
},
|
|
{
|
|
name: "negative default max tokens",
|
|
entry: config.ModelCatalogEntry{
|
|
ID: "test",
|
|
DefaultMaxTokens: -1,
|
|
Providers: map[string]string{"vllm-gpu": "m"},
|
|
},
|
|
},
|
|
{
|
|
name: "negative min max tokens",
|
|
entry: config.ModelCatalogEntry{
|
|
ID: "test",
|
|
MinMaxTokens: -1,
|
|
Providers: map[string]string{"vllm-gpu": "m"},
|
|
},
|
|
},
|
|
{
|
|
name: "negative thinking budget",
|
|
entry: config.ModelCatalogEntry{
|
|
ID: "test",
|
|
DefaultThinkingTokenBudget: -1,
|
|
Providers: map[string]string{"vllm-gpu": "m"},
|
|
},
|
|
},
|
|
{
|
|
name: "default below min",
|
|
entry: config.ModelCatalogEntry{
|
|
ID: "test",
|
|
DefaultMaxTokens: 8,
|
|
MinMaxTokens: 16,
|
|
Providers: map[string]string{"vllm-gpu": "m"},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if err := tc.entry.Validate(providerIDs, nil); err == nil {
|
|
t.Fatal("expected error for invalid generation policy")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNodeProviderConf_Validate(t *testing.T) {
|
|
// Happy path
|
|
p := config.NodeProviderConf{
|
|
ID: "p1",
|
|
Type: "vllm",
|
|
Category: config.CategoryAPI,
|
|
Models: []string{"model-a", "model-b"},
|
|
Capacity: 4,
|
|
}
|
|
if err := p.Validate(); err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
// Empty ID
|
|
p2 := config.NodeProviderConf{ID: "", Type: "vllm", Category: config.CategoryAPI}
|
|
if err := p2.Validate(); err == nil {
|
|
t.Fatal("expected error for empty ID")
|
|
}
|
|
|
|
// Empty Type
|
|
p3 := config.NodeProviderConf{ID: "p1", Type: "", Category: config.CategoryAPI}
|
|
if err := p3.Validate(); err == nil {
|
|
t.Fatal("expected error for empty Type")
|
|
}
|
|
|
|
// Invalid Category
|
|
p4 := config.NodeProviderConf{ID: "p1", Type: "vllm", Category: "invalid"}
|
|
if err := p4.Validate(); err == nil {
|
|
t.Fatal("expected error for invalid category")
|
|
}
|
|
|
|
// Empty model in models list
|
|
p5 := config.NodeProviderConf{
|
|
ID: "p1",
|
|
Type: "vllm",
|
|
Category: config.CategoryAPI,
|
|
Models: []string{"", "real"},
|
|
}
|
|
if err := p5.Validate(); err == nil {
|
|
t.Fatal("expected error for empty model in list")
|
|
}
|
|
|
|
// Negative capacity
|
|
p6 := config.NodeProviderConf{
|
|
ID: "p1",
|
|
Type: "vllm",
|
|
Category: config.CategoryAPI,
|
|
Capacity: -1,
|
|
}
|
|
if err := p6.Validate(); err == nil {
|
|
t.Fatal("expected error for negative capacity")
|
|
}
|
|
|
|
// Negative max_queue
|
|
p7 := config.NodeProviderConf{
|
|
ID: "p1",
|
|
Type: "vllm",
|
|
Category: config.CategoryAPI,
|
|
MaxQueue: -1,
|
|
}
|
|
if err := p7.Validate(); err == nil {
|
|
t.Fatal("expected error for negative max_queue")
|
|
}
|
|
|
|
// Negative queue_timeout_ms
|
|
p8 := config.NodeProviderConf{
|
|
ID: "p1",
|
|
Type: "vllm",
|
|
Category: config.CategoryAPI,
|
|
QueueTimeoutMS: -1,
|
|
}
|
|
if err := p8.Validate(); err == nil {
|
|
t.Fatal("expected error for negative queue_timeout_ms")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ProviderPoolLegacyCompatibility(t *testing.T) {
|
|
// Legacy openai.model_routes must still work alongside the new models[]/providers[].
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
model_routes:
|
|
- model: "codex"
|
|
adapter: "cli"
|
|
target: "codex"
|
|
workspace_required: true
|
|
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"
|
|
`
|
|
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.OpenAI.ModelRoutes) != 1 {
|
|
t.Fatalf("expected 1 legacy route, got %d", len(cfg.OpenAI.ModelRoutes))
|
|
}
|
|
if len(cfg.Models) != 1 {
|
|
t.Fatalf("expected 1 models[] entry, got %d", len(cfg.Models))
|
|
}
|
|
}
|
|
|
|
// S02 REVIEW_VLLM_CONFIG: vLLM OpenAI-compatible route + instance fixture
|
|
// verifies that alias/target/provider/endpoint/no-headers/timeout/queue are
|
|
// preserved through config load and mapper/factory contract.
|
|
func TestLoadEdge_VLLMOpenAIRouteAndInstance(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
model_routes:
|
|
- model: "qwen3.6:35b"
|
|
adapter: "openai_compat"
|
|
target: "nvidia/Qwen3.6-35B-A3B-NVFP4"
|
|
node: "node-vllm-01"
|
|
max_queue: 10
|
|
queue_timeout_ms: 30000
|
|
- model: "codex-agent"
|
|
adapter: "cli"
|
|
target: "codex"
|
|
workspace_required: true
|
|
nodes:
|
|
- id: "node-vllm-01"
|
|
alias: "vllm-gpu-node"
|
|
token: "<node-token>"
|
|
adapters:
|
|
openai_compat_instances:
|
|
- name: "vllm-gpu"
|
|
enabled: true
|
|
provider: "vllm"
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
capacity: 4
|
|
max_queue: 16
|
|
queue_timeout_ms: 30000
|
|
request_timeout_ms: 120000
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
codex:
|
|
command: "codex"
|
|
mode: "codex-exec"
|
|
`
|
|
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)
|
|
}
|
|
|
|
// Verify model_routes: vLLM route has no provider field (only adapter/target).
|
|
if len(cfg.OpenAI.ModelRoutes) != 2 {
|
|
t.Fatalf("expected 2 model_routes, got %d", len(cfg.OpenAI.ModelRoutes))
|
|
}
|
|
vllmRoute := cfg.OpenAI.ModelRoutes[0]
|
|
if vllmRoute.Model != "qwen3.6:35b" {
|
|
t.Errorf("model: got %q, want %q", vllmRoute.Model, "qwen3.6:35b")
|
|
}
|
|
if vllmRoute.Adapter != "openai_compat" {
|
|
t.Errorf("adapter: got %q, want %q", vllmRoute.Adapter, "openai_compat")
|
|
}
|
|
if vllmRoute.Target != "nvidia/Qwen3.6-35B-A3B-NVFP4" {
|
|
t.Errorf("target: got %q, want %q", vllmRoute.Target, "nvidia/Qwen3.6-35B-A3B-NVFP4")
|
|
}
|
|
if vllmRoute.NodeRef != "node-vllm-01" {
|
|
t.Errorf("node: got %q, want %q", vllmRoute.NodeRef, "node-vllm-01")
|
|
}
|
|
if vllmRoute.MaxQueue != 10 || vllmRoute.QueueTimeoutMS != 30000 {
|
|
t.Errorf("queue policy mismatch: max_queue=%d, queue_timeout_ms=%d", vllmRoute.MaxQueue, vllmRoute.QueueTimeoutMS)
|
|
}
|
|
// Codex agent route: workspace_required=true.
|
|
agentRoute := cfg.OpenAI.ModelRoutes[1]
|
|
if agentRoute.Model != "codex-agent" || agentRoute.Adapter != "cli" || agentRoute.Target != "codex" {
|
|
t.Errorf("agent route mismatch: %+v", agentRoute)
|
|
}
|
|
if !agentRoute.WorkspaceRequired {
|
|
t.Error("agent route workspace_required should be true")
|
|
}
|
|
|
|
// Verify openai_compat_instances: provider="vllm", endpoint, no headers, queue policy.
|
|
nodes := cfg.Nodes
|
|
if len(nodes) != 1 {
|
|
t.Fatalf("expected 1 node, got %d", len(nodes))
|
|
}
|
|
insts := nodes[0].Adapters.OpenAICompatInstances
|
|
if len(insts) != 1 {
|
|
t.Fatalf("expected 1 openai_compat instance, got %d", len(insts))
|
|
}
|
|
inst := insts[0]
|
|
if inst.Name != "vllm-gpu" {
|
|
t.Errorf("instance name: got %q, want %q", inst.Name, "vllm-gpu")
|
|
}
|
|
if inst.Provider != "vllm" {
|
|
t.Errorf("provider: got %q, want %q", inst.Provider, "vllm")
|
|
}
|
|
if inst.Endpoint != "http://127.0.0.1:8000/v1" {
|
|
t.Errorf("endpoint: got %q, want %q", inst.Endpoint, "http://127.0.0.1:8000/v1")
|
|
}
|
|
if len(inst.Headers) != 0 {
|
|
t.Errorf("expected no headers for vLLM, got %+v", inst.Headers)
|
|
}
|
|
if inst.Capacity != 4 || inst.MaxQueue != 16 || inst.QueueTimeoutMS != 30000 || inst.RequestTimeoutMS != 120000 {
|
|
t.Errorf("queue config mismatch: %+v", inst)
|
|
}
|
|
}
|
|
|
|
func TestLoad_NodeReconnectDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "node.yaml")
|
|
if err := os.WriteFile(f, []byte("transport:\n edge_addr: \"localhost:9090\"\n token: \"tok\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.Load(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Reconnect.IntervalSec != 10 {
|
|
t.Fatalf("expected reconnect.interval_sec=10, got %d", cfg.Reconnect.IntervalSec)
|
|
}
|
|
if cfg.Reconnect.MaxAttempts != 10 {
|
|
t.Fatalf("expected reconnect.max_attempts=10, got %d", cfg.Reconnect.MaxAttempts)
|
|
}
|
|
}
|
|
|
|
func TestLoad_NodeReconnectOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "node.yaml")
|
|
yaml := "transport:\n edge_addr: \"localhost:9090\"\n token: \"tok\"\nreconnect:\n interval_sec: 30\n max_attempts: 5\n"
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.Load(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Reconnect.IntervalSec != 30 {
|
|
t.Fatalf("expected reconnect.interval_sec=30, got %d", cfg.Reconnect.IntervalSec)
|
|
}
|
|
if cfg.Reconnect.MaxAttempts != 5 {
|
|
t.Fatalf("expected reconnect.max_attempts=5, got %d", cfg.Reconnect.MaxAttempts)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ProviderFirstNodeProvidersHappyPath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "gpu-node"
|
|
providers:
|
|
- id: "openai-provider"
|
|
type: "openai_compat"
|
|
category: "api"
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
headers:
|
|
Authorization: "Bearer test"
|
|
capacity: 5
|
|
- id: "ollama-provider"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
base_url: "http://127.0.0.1:11434"
|
|
context_size: 4096
|
|
capacity: 2
|
|
- id: "cli-provider"
|
|
type: "cli"
|
|
category: "cli"
|
|
command: "python"
|
|
args: ["-m", "cli"]
|
|
capacity: 1
|
|
`
|
|
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.Nodes) != 1 {
|
|
t.Fatalf("expected 1 node, got %d", len(cfg.Nodes))
|
|
}
|
|
providers := cfg.Nodes[0].Providers
|
|
if len(providers) != 3 {
|
|
t.Fatalf("expected 3 providers, got %d", len(providers))
|
|
}
|
|
|
|
// Verify openai-provider
|
|
p1 := providers[0]
|
|
if p1.ID != "openai-provider" || p1.Type != "openai_compat" || p1.Endpoint != "http://127.0.0.1:8000/v1" {
|
|
t.Errorf("unexpected openai-provider fields: %+v", p1)
|
|
}
|
|
if p1.Headers["Authorization"] != "Bearer test" && p1.Headers["authorization"] != "Bearer test" {
|
|
t.Errorf("expected Authorization header, got %v", p1.Headers)
|
|
}
|
|
|
|
// Verify ollama-provider
|
|
p2 := providers[1]
|
|
if p2.ID != "ollama-provider" || p2.Type != "ollama" || p2.BaseURL != "http://127.0.0.1:11434" || p2.ContextSize != 4096 {
|
|
t.Errorf("unexpected ollama-provider fields: %+v", p2)
|
|
}
|
|
|
|
// Verify cli-provider
|
|
p3 := providers[2]
|
|
if p3.ID != "cli-provider" || p3.Type != "cli" || p3.Command != "python" || len(p3.Args) != 2 {
|
|
t.Errorf("unexpected cli-provider fields: %+v", p3)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_RejectWhitespaceProviderType(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
providers:
|
|
- id: "whitespace-provider"
|
|
type: " "
|
|
category: "api"
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
`
|
|
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 whitespace provider type, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "type must not be empty") {
|
|
t.Fatalf("expected error containing 'type must not be empty', got %v", err)
|
|
}
|
|
}
|
|
|
|
// REVIEW_REVIEW_COMPAT-1: TestLoadEdge_LegacyOnlyConfigRegression
|
|
// verifies that a pure legacy-only config (adapters only, no providers)
|
|
// still loads successfully and the node has an empty Providers list.
|
|
func TestLoadEdge_LegacyOnlyConfigRegression(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
`
|
|
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("LoadEdge should succeed for pure legacy-only config: %v", err)
|
|
}
|
|
if len(cfg.Nodes) != 1 {
|
|
t.Fatalf("expected 1 node, got %d", len(cfg.Nodes))
|
|
}
|
|
if len(cfg.Nodes[0].Providers) != 0 {
|
|
t.Fatalf("expected empty Providers list for legacy-only config, got %d providers", len(cfg.Nodes[0].Providers))
|
|
}
|
|
}
|
|
|
|
// COMPAT-1: TestLoadEdge_ProviderFirstLegacyIdenticalMixedAllowed verifies that
|
|
// provider-first resources and legacy adapters with matching fields coexist
|
|
// without errors when the key (provider id == adapter instance name) is shared.
|
|
func TestLoadEdge_ProviderFirstLegacyIdenticalMixedAllowed(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
context_size: 4096
|
|
capacity: 2
|
|
providers:
|
|
- id: "ollama"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
base_url: "http://127.0.0.1:11434"
|
|
models:
|
|
- "llama3:8b"
|
|
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("LoadEdge should succeed for identical mixed config: %v", err)
|
|
}
|
|
if len(cfg.Nodes) != 1 || len(cfg.Nodes[0].Providers) != 1 {
|
|
t.Fatalf("expected 1 node with 1 provider, got %d nodes", len(cfg.Nodes))
|
|
}
|
|
}
|
|
|
|
// COMPAT-1: TestLoadEdge_ProviderFirstLegacyConflictRejected verifies that
|
|
// provider-first resources and legacy adapters with different field values
|
|
// for the same key produce a validation error.
|
|
func TestLoadEdge_ProviderFirstLegacyConflictRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
capacity: 2
|
|
providers:
|
|
- id: "ollama"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
base_url: "http://other-host:11434"
|
|
models:
|
|
- "llama3:8b"
|
|
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 conflicting provider-first and legacy ollama config")
|
|
}
|
|
if !strings.Contains(err.Error(), "conflicts with adapters") {
|
|
t.Fatalf("expected error containing 'conflicts with adapters', got %v", err)
|
|
}
|
|
}
|
|
|
|
// REVIEW_COMPAT-1: TestLoadEdge_ProviderFirstLegacyCapacityMismatchZeroProvider
|
|
// verifies that same-key provider-first/legacy mixed config is rejected when
|
|
// provider capacity is 0 (not-dispatchable) and legacy capacity is > 0.
|
|
func TestLoadEdge_ProviderFirstLegacyCapacityMismatchZeroProvider(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
capacity: 2
|
|
providers:
|
|
- id: "ollama"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
base_url: "http://127.0.0.1:11434"
|
|
models:
|
|
- "llama3:8b"
|
|
capacity: 0
|
|
`
|
|
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 provider capacity 0 / legacy capacity 2 conflict")
|
|
}
|
|
if !strings.Contains(err.Error(), "conflicts with adapters") || !strings.Contains(err.Error(), "capacity mismatch") {
|
|
t.Fatalf("expected error containing 'conflicts with adapters' and 'capacity mismatch', got %v", err)
|
|
}
|
|
}
|
|
|
|
// REVIEW_COMPAT-1: TestLoadEdge_ProviderFirstLegacyCapacityMismatchOmittedProvider
|
|
// verifies that same-key provider-first/legacy mixed config is rejected when
|
|
// provider capacity is omitted (defaults to 0) and legacy capacity is > 0.
|
|
func TestLoadEdge_ProviderFirstLegacyCapacityMismatchOmittedProvider(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://127.0.0.1:11434"
|
|
capacity: 3
|
|
providers:
|
|
- id: "ollama"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
base_url: "http://127.0.0.1:11434"
|
|
models:
|
|
- "llama3:8b"
|
|
`
|
|
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 omitted provider capacity / legacy capacity 3 conflict")
|
|
}
|
|
if !strings.Contains(err.Error(), "conflicts with adapters") || !strings.Contains(err.Error(), "capacity mismatch") {
|
|
t.Fatalf("expected error containing 'conflicts with adapters' and 'capacity mismatch', got %v", err)
|
|
}
|
|
}
|
|
|
|
// REVIEW_COMPAT-1: TestLoadEdge_ProviderFirstLegacyCapacityMismatchVllm
|
|
// verifies capacity mismatch rejection for vllm/openai_compat provider type.
|
|
func TestLoadEdge_ProviderFirstLegacyCapacityMismatchVllm(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
openai_compat_instances:
|
|
- name: "vllm-gpu"
|
|
enabled: true
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
capacity: 4
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
models:
|
|
- "model-a"
|
|
capacity: 0
|
|
`
|
|
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 vllm provider capacity 0 / legacy capacity 4 conflict")
|
|
}
|
|
if !strings.Contains(err.Error(), "conflicts with adapters") || !strings.Contains(err.Error(), "capacity mismatch") {
|
|
t.Fatalf("expected error containing 'conflicts with adapters' and 'capacity mismatch', got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadEdge_NodeProviderPriorityOmittedDefaultsZero verifies that when
|
|
// priority is omitted from a provider entry, it defaults to zero.
|
|
func TestLoadEdge_NodeProviderPriorityOmittedDefaultsZero(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-p"
|
|
alias: "n-p"
|
|
token: "tok-p"
|
|
providers:
|
|
- id: "prov-p"
|
|
type: "vllm"
|
|
category: "api"
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
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.Nodes) != 1 {
|
|
t.Fatalf("expected 1 node, got %d", len(cfg.Nodes))
|
|
}
|
|
if len(cfg.Nodes[0].Providers) != 1 {
|
|
t.Fatalf("expected 1 provider, got %d", len(cfg.Nodes[0].Providers))
|
|
}
|
|
if cfg.Nodes[0].Providers[0].Priority != 0 {
|
|
t.Fatalf("expected default priority=0, got %d", cfg.Nodes[0].Providers[0].Priority)
|
|
}
|
|
}
|
|
|
|
// TestLoadEdge_NodeProviderPriorityExplicitLoads verifies that an explicit
|
|
// priority value is loaded correctly.
|
|
func TestLoadEdge_NodeProviderPriorityExplicitLoads(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-p2"
|
|
alias: "n-p2"
|
|
token: "tok-p2"
|
|
providers:
|
|
- id: "prov-p2"
|
|
type: "vllm"
|
|
category: "api"
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
models:
|
|
- "model-a"
|
|
capacity: 2
|
|
priority: 5
|
|
`
|
|
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 cfg.Nodes[0].Providers[0].Priority != 5 {
|
|
t.Fatalf("expected priority=5, got %d", cfg.Nodes[0].Providers[0].Priority)
|
|
}
|
|
}
|
|
|
|
// TestLoadEdge_NodeProviderNegativePriorityRejected verifies that a negative
|
|
// priority value is rejected during config load.
|
|
func TestLoadEdge_NodeProviderNegativePriorityRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-p3"
|
|
alias: "n-p3"
|
|
token: "tok-p3"
|
|
providers:
|
|
- id: "prov-p3"
|
|
type: "vllm"
|
|
category: "api"
|
|
endpoint: "http://127.0.0.1:8000/v1"
|
|
models:
|
|
- "model-a"
|
|
capacity: 2
|
|
priority: -1
|
|
`
|
|
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 negative priority")
|
|
}
|
|
if !strings.Contains(err.Error(), "priority must be non-negative") {
|
|
t.Fatalf("expected error mentioning priority non-negative, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadEdgeProviderLongContextCapacityValidation covers the provider-level
|
|
// long-context fields: a valid config loads the values, and the field-level
|
|
// invariants (non-negative, and total>0 when long_context_capacity>0) are enforced.
|
|
func TestLoadEdgeProviderLongContextCapacityValidation(t *testing.T) {
|
|
validYAML := `
|
|
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"
|
|
alias: "gpu-node"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B"
|
|
capacity: 4
|
|
total_context_tokens: 524288
|
|
long_context_capacity: 2
|
|
`
|
|
t.Run("valid", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte(validYAML), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
prov := cfg.Nodes[0].Providers[0]
|
|
if prov.TotalContextTokens != 524288 {
|
|
t.Errorf("total_context_tokens = %d, want 524288", prov.TotalContextTokens)
|
|
}
|
|
if prov.LongContextCapacity != 2 {
|
|
t.Errorf("long_context_capacity = %d, want 2", prov.LongContextCapacity)
|
|
}
|
|
})
|
|
|
|
cases := []struct {
|
|
name string
|
|
total string
|
|
long string
|
|
wantMsg string
|
|
}{
|
|
{"negative total", "total_context_tokens: -1", "long_context_capacity: 0", "total_context_tokens must be non-negative"},
|
|
{"negative long", "total_context_tokens: 524288", "long_context_capacity: -1", "long_context_capacity must be non-negative"},
|
|
{"long without total", "total_context_tokens: 0", "long_context_capacity: 2", "total_context_tokens must be positive when long_context_capacity"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := fmt.Sprintf(`
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
alias: "gpu-node"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B"
|
|
capacity: 4
|
|
%s
|
|
%s
|
|
`, tc.total, tc.long)
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatalf("expected error for %s", tc.name)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantMsg) {
|
|
t.Fatalf("expected error mentioning %q, got %v", tc.wantMsg, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLoadEdgeRejectsProviderLongContextBudgetBelowModelWindow verifies the
|
|
// cross-entry budget rule: a provider referenced by a model group must have
|
|
// total_context_tokens >= context_window_tokens * long_context_capacity.
|
|
func TestLoadEdgeRejectsProviderLongContextBudgetBelowModelWindow(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
// 262144 * 2 = 524288 required, but total is only 262144.
|
|
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"
|
|
alias: "gpu-node"
|
|
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 total context budget")
|
|
}
|
|
if !strings.Contains(err.Error(), "total_context_tokens") || !strings.Contains(err.Error(), "long_context_capacity") {
|
|
t.Fatalf("expected budget error mentioning total_context_tokens and long_context_capacity, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_MockConfigExplicitEnabled(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "test-node"
|
|
token: "token-test"
|
|
adapters:
|
|
mock:
|
|
enabled: true
|
|
`
|
|
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 !cfg.Nodes[0].Adapters.Mock.Enabled {
|
|
t.Fatal("expected mock.enabled=true")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_MockConfigOmittedDefaultsFalse(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "test-node"
|
|
token: "token-test"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
default:
|
|
command: "echo"
|
|
`
|
|
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 cfg.Nodes[0].Adapters.Mock.Enabled {
|
|
t.Fatal("expected mock.enabled=false when omitted")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_MockConfigExplicitDisabled(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "test-node"
|
|
token: "token-test"
|
|
adapters:
|
|
mock:
|
|
enabled: false
|
|
`
|
|
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 cfg.Nodes[0].Adapters.Mock.Enabled {
|
|
t.Fatal("expected mock.enabled=false when explicitly disabled")
|
|
}
|
|
}
|