1926 lines
47 KiB
Go
1926 lines
47 KiB
Go
package config_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
// TestLoadEdgeExecutionPresetCatalog verifies that valid direct and light preset
|
|
// shapes decode, normalize, and survive LoadEdge alongside existing provider-
|
|
// only fixtures.
|
|
func TestLoadEdgeExecutionPresetCatalog(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
|
|
// Direct preset: no downstream stages, no options.
|
|
directYAML := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "direct-default"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
t.Run("direct preset loads", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(directYAML), 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.ExecutionPresets) != 1 {
|
|
t.Fatalf("expected 1 preset, got %d", len(cfg.ExecutionPresets))
|
|
}
|
|
p := cfg.ExecutionPresets[0]
|
|
if p.ID != "direct-default" {
|
|
t.Errorf("preset id = %q, want %q", p.ID, "direct-default")
|
|
}
|
|
if p.Selector.Model != "model-a" {
|
|
t.Errorf("selector model = %q, want %q", p.Selector.Model, "model-a")
|
|
}
|
|
if len(p.AllowedModes) != 1 || p.AllowedModes[0] != "direct" {
|
|
t.Errorf("allowed_modes = %v, want [direct]", p.AllowedModes)
|
|
}
|
|
if len(p.Routes["direct"].Stages) != 0 {
|
|
t.Errorf("expected 0 route stages for direct, got %d", len(p.Routes["direct"].Stages))
|
|
}
|
|
})
|
|
|
|
// Route key with surrounding whitespace normalizes.
|
|
whitespaceRouteYAML := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "whitespace-route"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
" direct ":
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
t.Run("route key with surrounding whitespace normalizes", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(whitespaceRouteYAML), 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.ExecutionPresets) != 1 {
|
|
t.Fatalf("expected 1 preset, got %d", len(cfg.ExecutionPresets))
|
|
}
|
|
p := cfg.ExecutionPresets[0]
|
|
if _, ok := p.Routes["direct"]; !ok {
|
|
t.Errorf("expected route key 'direct' after normalization, got routes %v", p.Routes)
|
|
}
|
|
if _, rawExists := p.Routes[" direct "]; rawExists {
|
|
t.Errorf("raw un-trimmed route key ' direct ' should not remain in routes")
|
|
}
|
|
})
|
|
|
|
// Hybrid multi-mode preset (direct and light).
|
|
hybridYAML := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "hybrid-preset"
|
|
selector:
|
|
model: "model-a"
|
|
options:
|
|
temperature: 0.2
|
|
allowed_modes:
|
|
- "direct"
|
|
- "light"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
options:
|
|
timeout_ms: "30000"
|
|
- role: "review"
|
|
model: "model-b"
|
|
options:
|
|
max_retries: "2"
|
|
workspace_tools:
|
|
- name: "standard-fs"
|
|
operations:
|
|
" prepare ":
|
|
tool_name: "mkdir_p"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
read:
|
|
tool_name: "read_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "write_file"
|
|
creates_parents: false
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "delete_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
capacity: 2
|
|
`
|
|
t.Run("hybrid multi-mode preset loads with workspace tools", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(hybridYAML), 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.ExecutionPresets) != 1 {
|
|
t.Fatalf("expected 1 preset, got %d", len(cfg.ExecutionPresets))
|
|
}
|
|
p := cfg.ExecutionPresets[0]
|
|
if p.ID != "hybrid-preset" {
|
|
t.Errorf("preset id = %q, want %q", p.ID, "hybrid-preset")
|
|
}
|
|
if len(p.AllowedModes) != 2 || p.AllowedModes[0] != "direct" || p.AllowedModes[1] != "light" {
|
|
t.Errorf("allowed_modes = %v, want [direct, light]", p.AllowedModes)
|
|
}
|
|
if len(p.Routes["light"].Stages) != 2 {
|
|
t.Fatalf("expected 2 route stages for light, got %d", len(p.Routes["light"].Stages))
|
|
}
|
|
if p.Routes["light"].Stages[0].Role != "local" || p.Routes["light"].Stages[0].Model != "model-a" {
|
|
t.Errorf("light stage 0 = %+v", p.Routes["light"].Stages[0])
|
|
}
|
|
if p.Routes["light"].Stages[1].Role != "review" || p.Routes["light"].Stages[1].Model != "model-b" {
|
|
t.Errorf("light stage 1 = %+v", p.Routes["light"].Stages[1])
|
|
}
|
|
if len(p.WorkspaceTools) != 1 {
|
|
t.Fatalf("expected 1 workspace tool alternative, got %d", len(p.WorkspaceTools))
|
|
}
|
|
wt := p.WorkspaceTools[0]
|
|
if wt.Name != "standard-fs" {
|
|
t.Errorf("workspace tool name = %q, want standard-fs", wt.Name)
|
|
}
|
|
if wt.Operations["write"].ToolName != "write_file" {
|
|
t.Errorf("write operation tool_name = %q, want write_file", wt.Operations["write"].ToolName)
|
|
}
|
|
prepOp, hasPrep := wt.Operations["prepare"]
|
|
if !hasPrep || prepOp.ToolName != "mkdir_p" {
|
|
t.Errorf("prepare operation failed normalized key lookup, got %+v", prepOp)
|
|
}
|
|
if prepOp.SchemaMatcher == nil || prepOp.ArgumentMap == nil || prepOp.ResultMatcher == nil {
|
|
t.Errorf("prepare operation missing matchers/mappings, got %+v", prepOp)
|
|
}
|
|
})
|
|
|
|
// Multiple presets with mixed modes.
|
|
multiYAML := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "fast-path"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
- id: "review-path"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
capacity: 2
|
|
`
|
|
t.Run("multiple presets with mixed modes", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(multiYAML), 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.ExecutionPresets) != 2 {
|
|
t.Fatalf("expected 2 presets, got %d", len(cfg.ExecutionPresets))
|
|
}
|
|
byID := map[string]config.ExecutionPreset{}
|
|
for _, p := range cfg.ExecutionPresets {
|
|
byID[p.ID] = p
|
|
}
|
|
if _, ok := byID["fast-path"]; !ok {
|
|
t.Fatal("expected fast-path preset")
|
|
}
|
|
if _, ok := byID["review-path"]; !ok {
|
|
t.Fatal("expected review-path preset")
|
|
}
|
|
})
|
|
|
|
// Empty execution_presets should load fine.
|
|
emptyYAML := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
t.Run("no presets defined loads fine", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(emptyYAML), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
})
|
|
|
|
// Existing provider-only fixtures must remain compatible.
|
|
providerOnlyYAML := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
providers:
|
|
vllm-gpu: "nvidia/Qwen3.6-35B"
|
|
nodes:
|
|
- id: "node-gpu-01"
|
|
providers:
|
|
- id: "vllm-gpu"
|
|
type: "vllm"
|
|
category: "api"
|
|
models:
|
|
- "nvidia/Qwen3.6-35B"
|
|
capacity: 4
|
|
`
|
|
t.Run("provider-only config remains compatible", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(providerOnlyYAML), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestLoadEdgeExecutionPresetRejectsInvalidShape verifies that invalid ids,
|
|
// routes, options, binding shapes, dangling references, and unsupported handlers fail closed.
|
|
func TestLoadEdgeExecutionPresetRejectsInvalidShape(t *testing.T) {
|
|
t.Run("approved top-level list required map shape rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
execution_presets:
|
|
presets:
|
|
- id: "bad-shape"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for map shape execution_presets")
|
|
}
|
|
})
|
|
|
|
t.Run("unknown preset field rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "unknown-field-preset"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
unsupported_spelling: "bad"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown preset field")
|
|
}
|
|
if !strings.Contains(err.Error(), "unsupported_spelling") && !strings.Contains(err.Error(), "unused") {
|
|
t.Fatalf("expected error mentioning unused/unknown field, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty preset id rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: ""
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty preset id")
|
|
}
|
|
if !strings.Contains(err.Error(), "id must not be empty") {
|
|
t.Fatalf("expected error mentioning id must not be empty, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate preset id rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "dup"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
- id: "dup"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for duplicate preset id")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate preset id") {
|
|
t.Fatalf("expected error mentioning duplicate preset id, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("dangling selector model rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "dangling-selector"
|
|
selector:
|
|
model: "non-existent-model"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for dangling selector model")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found in models catalog") {
|
|
t.Fatalf("expected error mentioning not found in models catalog, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty allowed_modes rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "no-modes"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes: []
|
|
routes: {}
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty allowed_modes")
|
|
}
|
|
if !strings.Contains(err.Error(), "allowed_modes must not be empty") {
|
|
t.Fatalf("expected error mentioning allowed_modes must not be empty, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unsupported mode heavy rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "heavy-path"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "heavy"
|
|
routes:
|
|
heavy:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for unsupported mode 'heavy'")
|
|
}
|
|
if !strings.Contains(err.Error(), "not a registered mode descriptor") {
|
|
t.Fatalf("expected error mentioning not a registered mode descriptor, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing route key for allowed mode rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "missing-route"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
- "light"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing route key for light mode")
|
|
}
|
|
if !strings.Contains(err.Error(), "missing route for allowed mode") {
|
|
t.Fatalf("expected error mentioning missing route for allowed mode, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("extra route key not in allowed_modes rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "extra-route"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-a"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for extra route key")
|
|
}
|
|
if !strings.Contains(err.Error(), "is not in allowed_modes") {
|
|
t.Fatalf("expected error mentioning is not in allowed_modes, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate route key after normalization rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "dup-route-key"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
" direct ":
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for duplicate normalized route key")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate route key") {
|
|
t.Fatalf("expected error mentioning duplicate route key, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("direct mode with downstream stages rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "direct-with-stages"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for direct mode with downstream stages")
|
|
}
|
|
if !strings.Contains(err.Error(), "declares no downstream stages") {
|
|
t.Fatalf("expected error mentioning declares no downstream stages, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("required stage option overflow rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "option-overflow"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
options:
|
|
opt1: "v1"
|
|
opt2: "v2"
|
|
opt3: "v3"
|
|
opt4: "v4"
|
|
opt5: "v5"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 stage option overflow on required stage")
|
|
}
|
|
if !strings.Contains(err.Error(), "allows at most 4 options") {
|
|
t.Fatalf("expected error mentioning allows at most 4 options, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("dangling stage model rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "dangling-stage-model"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "non-existent-review-model"
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for dangling stage model")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found in models catalog") {
|
|
t.Fatalf("expected error mentioning not found in models catalog, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing prepare when write does not create parents rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "missing-prepare"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "no-prepare-ws"
|
|
operations:
|
|
read:
|
|
tool_name: "read_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "write_file"
|
|
creates_parents: false
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "delete_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 missing prepare when write creates_parents=false")
|
|
}
|
|
if !strings.Contains(err.Error(), "prepare") && !strings.Contains(err.Error(), "does not create parents") {
|
|
t.Fatalf("expected error mentioning prepare/creates_parents, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate allowed mode rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "dup-mode"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for duplicate allowed mode")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate allowed mode") {
|
|
t.Fatalf("expected error mentioning duplicate allowed mode, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate workspace alternative name rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "dup-alt"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "ws-dup"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
- name: "ws-dup"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 duplicate workspace alternative name")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate workspace_tools alternative name") {
|
|
t.Fatalf("expected error mentioning duplicate workspace_tools alternative name, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("light wrong stage order rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "wrong-order"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "review"
|
|
model: "model-b"
|
|
- role: "local"
|
|
model: "model-a"
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 light wrong stage order")
|
|
}
|
|
if !strings.Contains(err.Error(), "stage[0] role is") || !strings.Contains(err.Error(), "want") {
|
|
t.Fatalf("expected error mentioning stage role mismatch, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("light wrong stage count rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "wrong-count"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for light wrong stage count")
|
|
}
|
|
if !strings.Contains(err.Error(), "requires stages") || !strings.Contains(err.Error(), "got 1 stages") {
|
|
t.Fatalf("expected error mentioning required stages count, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("light missing read operation rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "missing-read"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "no-read-ws"
|
|
operations:
|
|
write:
|
|
tool_name: "write_file"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "delete_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 missing read operation")
|
|
}
|
|
if !strings.Contains(err.Error(), "requires operation \"read\"") {
|
|
t.Fatalf("expected error mentioning missing read operation, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("light missing write operation rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "missing-write"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "no-write-ws"
|
|
operations:
|
|
read:
|
|
tool_name: "read_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
delete:
|
|
tool_name: "delete_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 missing write operation")
|
|
}
|
|
if !strings.Contains(err.Error(), "requires operation \"write\"") {
|
|
t.Fatalf("expected error mentioning missing write operation, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("light missing delete operation rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "missing-delete"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "no-delete-ws"
|
|
operations:
|
|
read:
|
|
tool_name: "read_file"
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
write:
|
|
tool_name: "write_file"
|
|
creates_parents: true
|
|
schema_matcher:
|
|
type: "object"
|
|
argument_map:
|
|
path: "path"
|
|
result_matcher:
|
|
status: "ok"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 missing delete operation")
|
|
}
|
|
if !strings.Contains(err.Error(), "requires operation \"delete\"") {
|
|
t.Fatalf("expected error mentioning missing delete operation, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("custom unregistered mode rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "custom-mode"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "fast"
|
|
routes:
|
|
fast:
|
|
stages: []
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for custom unregistered mode")
|
|
}
|
|
if !strings.Contains(err.Error(), "not a registered mode descriptor") {
|
|
t.Fatalf("expected error mentioning not a registered mode descriptor, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty model catalog with selector reference rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
execution_presets:
|
|
- id: "empty-catalog-selector"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
`
|
|
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 selector model reference when models catalog is empty")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found in models catalog") {
|
|
t.Fatalf("expected error mentioning not found in models catalog, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty model catalog with stage reference rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "empty-catalog-stage"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher: { type: "object" }
|
|
argument_map: { path: "path" }
|
|
result_matcher: { status: "ok" }
|
|
write:
|
|
tool_name: "tee"
|
|
creates_parents: true
|
|
schema_matcher: { type: "object" }
|
|
argument_map: { path: "path" }
|
|
result_matcher: { status: "ok" }
|
|
delete:
|
|
tool_name: "rm"
|
|
schema_matcher: { type: "object" }
|
|
argument_map: { path: "path" }
|
|
result_matcher: { status: "ok" }
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for stage model reference not in catalog")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found in models catalog") {
|
|
t.Fatalf("expected error mentioning not found in models catalog, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("light mode with zero workspace_tools alternatives rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-b"
|
|
providers:
|
|
prov-a: "model-b"
|
|
execution_presets:
|
|
- id: "no-workspace-tools"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "local"
|
|
model: "model-a"
|
|
- role: "review"
|
|
model: "model-b"
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-b"]
|
|
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 light mode with no workspace_tools alternatives")
|
|
}
|
|
if !strings.Contains(err.Error(), "requires at least one workspace_tools alternative") {
|
|
t.Fatalf("expected error mentioning requires at least one workspace_tools alternative, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("workspace operation missing schema_matcher rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "missing-schema-matcher"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
argument_map: { path: "path" }
|
|
result_matcher: { status: "ok" }
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing schema_matcher")
|
|
}
|
|
if !strings.Contains(err.Error(), "schema_matcher must not be empty") {
|
|
t.Fatalf("expected error mentioning schema_matcher must not be empty, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("workspace operation missing argument_map rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "missing-argument-map"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher: { type: "object" }
|
|
result_matcher: { status: "ok" }
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing argument_map")
|
|
}
|
|
if !strings.Contains(err.Error(), "argument_map must not be empty") {
|
|
t.Fatalf("expected error mentioning argument_map must not be empty, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("workspace operation missing result_matcher rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "missing-result-matcher"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher: { type: "object" }
|
|
argument_map: { path: "path" }
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing result_matcher")
|
|
}
|
|
if !strings.Contains(err.Error(), "result_matcher must not be empty") {
|
|
t.Fatalf("expected error mentioning result_matcher must not be empty, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("workspace operation duplicate key after normalization rejected", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
execution_presets:
|
|
- id: "dup-op-key"
|
|
selector:
|
|
model: "model-a"
|
|
allowed_modes:
|
|
- "direct"
|
|
routes:
|
|
direct:
|
|
stages: []
|
|
workspace_tools:
|
|
- name: "ws1"
|
|
operations:
|
|
read:
|
|
tool_name: "cat"
|
|
schema_matcher: { type: "object" }
|
|
argument_map: { path: "path" }
|
|
result_matcher: { status: "ok" }
|
|
" read ":
|
|
tool_name: "cat2"
|
|
schema_matcher: { type: "object" }
|
|
argument_map: { path: "path" }
|
|
result_matcher: { status: "ok" }
|
|
nodes:
|
|
- id: "node-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
_, err := config.LoadEdge(f)
|
|
if err == nil {
|
|
t.Fatal("expected error for duplicate operation key after normalization")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate operation \"read\"") {
|
|
t.Fatalf("expected error mentioning duplicate operation read, got %v", err)
|
|
}
|
|
})
|
|
}
|