- NodeReadyRequest/Response: 노드→엣지 연결 준비 확인 핸드셰이크 추가 - ReconnectConf: interval_sec, max_attempts 명시적 설정 지원 - node_config_test: 재연결 설정 로드 검증
495 lines
13 KiB
Go
495 lines
13 KiB
Go
package config_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
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_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 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)
|
|
}
|
|
}
|
|
|
|
// TestLoadReconnectPolicy verifies that config load distinguishes an omitted
|
|
// max_attempts (default 10), an explicit 0 (unlimited), and a positive finite
|
|
// limit, and preserves an explicit interval alongside each. SDD S16/S17.
|
|
func TestLoadReconnectPolicy(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
reconnectYAML string
|
|
wantInterval int
|
|
wantMax int
|
|
}{
|
|
{
|
|
name: "omitted keeps defaults",
|
|
reconnectYAML: "",
|
|
wantInterval: 10,
|
|
wantMax: 10,
|
|
},
|
|
{
|
|
name: "explicit zero max is unlimited with default interval",
|
|
reconnectYAML: "reconnect:\n max_attempts: 0\n",
|
|
wantInterval: 10,
|
|
wantMax: 0,
|
|
},
|
|
{
|
|
name: "explicit zero max with positive interval stays unlimited",
|
|
reconnectYAML: "reconnect:\n max_attempts: 0\n interval_sec: 5\n",
|
|
wantInterval: 5,
|
|
wantMax: 0,
|
|
},
|
|
{
|
|
name: "positive finite limit is preserved",
|
|
reconnectYAML: "reconnect:\n max_attempts: 3\n interval_sec: 30\n",
|
|
wantInterval: 30,
|
|
wantMax: 3,
|
|
},
|
|
{
|
|
name: "positive finite limit allows zero interval",
|
|
reconnectYAML: "reconnect:\n max_attempts: 3\n interval_sec: 0\n",
|
|
wantInterval: 0,
|
|
wantMax: 3,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "node.yaml")
|
|
yaml := "transport:\n edge_addr: \"localhost:9090\"\n token: \"tok\"\n" + tc.reconnectYAML
|
|
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 != tc.wantInterval {
|
|
t.Errorf("interval_sec: got %d want %d", cfg.Reconnect.IntervalSec, tc.wantInterval)
|
|
}
|
|
if cfg.Reconnect.MaxAttempts != tc.wantMax {
|
|
t.Errorf("max_attempts: got %d want %d", cfg.Reconnect.MaxAttempts, tc.wantMax)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLoadRejectsInvalidReconnectPolicy verifies that negative attempts/interval
|
|
// and an unlimited policy with a non-positive interval are rejected at load with
|
|
// a clear config error. SDD S17.
|
|
func TestLoadRejectsInvalidReconnectPolicy(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
reconnectYAML string
|
|
wantSubstr string
|
|
}{
|
|
{
|
|
name: "negative max_attempts",
|
|
reconnectYAML: "reconnect:\n max_attempts: -1\n",
|
|
wantSubstr: "reconnect.max_attempts must not be negative",
|
|
},
|
|
{
|
|
name: "negative interval_sec",
|
|
reconnectYAML: "reconnect:\n max_attempts: 3\n interval_sec: -1\n",
|
|
wantSubstr: "reconnect.interval_sec must not be negative",
|
|
},
|
|
{
|
|
name: "unlimited with zero interval",
|
|
reconnectYAML: "reconnect:\n max_attempts: 0\n interval_sec: 0\n",
|
|
wantSubstr: "reconnect.interval_sec must be positive",
|
|
},
|
|
{
|
|
name: "unlimited with negative interval",
|
|
reconnectYAML: "reconnect:\n max_attempts: 0\n interval_sec: -5\n",
|
|
wantSubstr: "reconnect.interval_sec must not be negative",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "node.yaml")
|
|
yaml := "transport:\n edge_addr: \"localhost:9090\"\n token: \"tok\"\n" + tc.reconnectYAML
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.Load(f)
|
|
if err == nil {
|
|
t.Fatalf("expected error for %s", tc.name)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantSubstr) {
|
|
t.Fatalf("expected error containing %q, got %v", tc.wantSubstr, 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)
|
|
}
|
|
}
|