승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
1162 lines
32 KiB
Go
1162 lines
32 KiB
Go
package config_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
// validWorkspaceYAML is a baseline valid workspace catalog YAML used by
|
|
// multiple sub-tests. It uses a single node with one workspace defining read
|
|
// and list operations with bounded limits and a safe environment allowlist.
|
|
const validWorkspaceYAML = `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
nodes:
|
|
- id: "node-ws-01"
|
|
alias: "mac-node"
|
|
token: "token-ws-01"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-project-root"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/iop-workspace"
|
|
operations:
|
|
- "read"
|
|
- "list"
|
|
max_read_bytes: 1048576
|
|
max_output_bytes: 4194304
|
|
environment_allowlist:
|
|
- "IOP_ENV"
|
|
`
|
|
|
|
// validWorkspaceWithCommandsYAML is a valid workspace catalog with command
|
|
// operations and command templates.
|
|
const validWorkspaceWithCommandsYAML = `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
nodes:
|
|
- id: "node-ws-cmd"
|
|
alias: "mac-node-cmd"
|
|
token: "token-ws-cmd"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-cmd-workspace"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/cmd-workspace"
|
|
operations:
|
|
- "read"
|
|
- "list"
|
|
- "write"
|
|
- "delete"
|
|
- "command"
|
|
commands:
|
|
- id: "list-files"
|
|
executable: "/usr/bin/find"
|
|
args:
|
|
- "/Users/operator/projects/cmd-workspace"
|
|
- "-name"
|
|
- "*.go"
|
|
- id: "read-file"
|
|
executable: "/usr/bin/cat"
|
|
args: []
|
|
max_read_bytes: 2097152
|
|
max_write_bytes: 1048576
|
|
max_output_bytes: 8388608
|
|
max_command_timeout_ms: 30000
|
|
environment_allowlist:
|
|
- "PATH"
|
|
- "HOME"
|
|
`
|
|
|
|
// TestLoadEdgeWorkspaceCatalog verifies that valid workspace catalogs decode,
|
|
// normalize, and survive LoadEdge alongside ordinary node definitions.
|
|
func TestLoadEdgeWorkspaceCatalog(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
|
|
t.Run("single workspace with read/list operations loads", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(validWorkspaceYAML), 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))
|
|
}
|
|
node := cfg.Nodes[0]
|
|
if len(node.Workspaces) != 1 {
|
|
t.Fatalf("expected 1 workspace, got %d", len(node.Workspaces))
|
|
}
|
|
ws := node.Workspaces[0]
|
|
if ws.Ref != "ws-project-root" {
|
|
t.Errorf("ref = %q, want ws-project-root", ws.Ref)
|
|
}
|
|
if ws.Platform != "darwin" {
|
|
t.Errorf("platform = %q, want darwin", ws.Platform)
|
|
}
|
|
if ws.Root != "/Users/operator/projects/iop-workspace" {
|
|
t.Errorf("root = %q, want /Users/operator/projects/iop-workspace", ws.Root)
|
|
}
|
|
if len(ws.Operations) != 2 {
|
|
t.Fatalf("expected 2 operations, got %d", len(ws.Operations))
|
|
}
|
|
if ws.Operations[0] != config.WorkspaceOpRead {
|
|
t.Errorf("operations[0] = %q, want read", ws.Operations[0])
|
|
}
|
|
if ws.Operations[1] != config.WorkspaceOpList {
|
|
t.Errorf("operations[1] = %q, want list", ws.Operations[1])
|
|
}
|
|
if ws.MaxReadBytes != 1048576 {
|
|
t.Errorf("max_read_bytes = %d, want 1048576", ws.MaxReadBytes)
|
|
}
|
|
if ws.MaxOutputBytes != 4194304 {
|
|
t.Errorf("max_output_bytes = %d, want 4194304", ws.MaxOutputBytes)
|
|
}
|
|
if len(ws.EnvironmentAllowlist) != 1 || ws.EnvironmentAllowlist[0] != "IOP_ENV" {
|
|
t.Errorf("environment_allowlist = %v, want [IOP_ENV]", ws.EnvironmentAllowlist)
|
|
}
|
|
})
|
|
|
|
t.Run("workspace with command operations and templates loads", func(t *testing.T) {
|
|
if err := os.WriteFile(f, []byte(validWorkspaceWithCommandsYAML), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
node := cfg.Nodes[0]
|
|
ws := node.Workspaces[0]
|
|
if ws.Ref != "ws-cmd-workspace" {
|
|
t.Errorf("ref = %q, want ws-cmd-workspace", ws.Ref)
|
|
}
|
|
if len(ws.Operations) != 5 {
|
|
t.Fatalf("expected 5 operations, got %d", len(ws.Operations))
|
|
}
|
|
if len(ws.Commands) != 2 {
|
|
t.Fatalf("expected 2 commands, got %d", len(ws.Commands))
|
|
}
|
|
if ws.Commands[0].ID != "list-files" {
|
|
t.Errorf("commands[0].id = %q, want list-files", ws.Commands[0].ID)
|
|
}
|
|
if ws.Commands[0].Executable != "/usr/bin/find" {
|
|
t.Errorf("commands[0].executable = %q, want /usr/bin/find", ws.Commands[0].Executable)
|
|
}
|
|
if ws.Commands[1].ID != "read-file" {
|
|
t.Errorf("commands[1].id = %q, want read-file", ws.Commands[1].ID)
|
|
}
|
|
if ws.MaxCommandTimeoutMS != 30000 {
|
|
t.Errorf("max_command_timeout_ms = %d, want 30000", ws.MaxCommandTimeoutMS)
|
|
}
|
|
})
|
|
|
|
t.Run("empty workspaces is backward-compatible", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
nodes:
|
|
- id: "node-no-ws"
|
|
alias: "no-workspace-node"
|
|
token: "token-no-ws"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(cfg.Nodes[0].Workspaces) != 0 {
|
|
t.Errorf("expected 0 workspaces, got %d", len(cfg.Nodes[0].Workspaces))
|
|
}
|
|
})
|
|
|
|
t.Run("workspace ref is trimmed", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
nodes:
|
|
- id: "node-ws-trim"
|
|
alias: "trim-node"
|
|
token: "token-trim"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: " ws-trimmed "
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/trimmed"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
`
|
|
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].Workspaces[0].Ref != "ws-trimmed" {
|
|
t.Errorf("ref = %q, want ws-trimmed (trimmed)", cfg.Nodes[0].Workspaces[0].Ref)
|
|
}
|
|
})
|
|
|
|
t.Run("workspace coexists with execution_presets", func(t *testing.T) {
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
- id: "model-plan"
|
|
providers:
|
|
prov-a: "model-plan"
|
|
- id: "model-work"
|
|
providers:
|
|
prov-a: "model-work"
|
|
- id: "model-review"
|
|
providers:
|
|
prov-a: "model-review"
|
|
execution_presets:
|
|
- id: "preset-fixed-light"
|
|
selector:
|
|
model: "model-plan"
|
|
options:
|
|
reasoning_effort: "high"
|
|
allowed_modes:
|
|
- "light"
|
|
routes:
|
|
light:
|
|
stages:
|
|
- role: "plan"
|
|
model: "model-plan"
|
|
options:
|
|
reasoning_effort: "high"
|
|
- role: "work"
|
|
model: "model-work"
|
|
- role: "review"
|
|
model: "model-review"
|
|
options:
|
|
reasoning_effort: "high"
|
|
single_request:
|
|
workspace_ref: "ws-single-request-ref"
|
|
limits:
|
|
wall_clock_ms: 1800000
|
|
timeout_ms: 600000
|
|
max_tool_iterations: 64
|
|
max_output_bytes: 16777216
|
|
stages:
|
|
plan:
|
|
model: "model-plan"
|
|
options:
|
|
reasoning_effort: "high"
|
|
work:
|
|
model: "model-work"
|
|
review:
|
|
model: "model-review"
|
|
options:
|
|
reasoning_effort: "high"
|
|
nodes:
|
|
- id: "node-ws-and-preset"
|
|
alias: "full-node"
|
|
token: "token-full"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a", "model-plan", "model-work", "model-review"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-operator-root"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/iop"
|
|
operations:
|
|
- "read"
|
|
- "list"
|
|
max_read_bytes: 1048576
|
|
max_output_bytes: 4194304
|
|
`
|
|
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].Workspaces) != 1 {
|
|
t.Fatalf("expected 1 workspace, got %d", len(cfg.Nodes[0].Workspaces))
|
|
}
|
|
if len(cfg.ExecutionPresets) != 1 {
|
|
t.Fatalf("expected 1 preset, got %d", len(cfg.ExecutionPresets))
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestLoadEdgeWorkspaceCatalogRejectsInvalid verifies that invalid workspace
|
|
// catalogs fail closed with descriptive errors covering all validation
|
|
// dimensions: ref, platform, root, operations, commands, environment, and
|
|
// numeric limits.
|
|
func TestLoadEdgeWorkspaceCatalogRejectsInvalid(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
|
|
baseNode := `
|
|
models:
|
|
- id: "model-a"
|
|
providers:
|
|
prov-a: "model-a"
|
|
nodes:`
|
|
|
|
t.Run("empty ref rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-empty-ref"
|
|
alias: "empty-ref-node"
|
|
token: "token-empty-ref"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: " "
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
`
|
|
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 ref")
|
|
}
|
|
if !strings.Contains(err.Error(), "ref must not be empty") {
|
|
t.Fatalf("expected ref error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate ref within node rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-dup-ref"
|
|
alias: "dup-ref-node"
|
|
token: "token-dup-ref"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-dup"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test1"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
- ref: "ws-dup"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test2"
|
|
operations:
|
|
- "list"
|
|
max_read_bytes: 2048
|
|
max_output_bytes: 2048
|
|
`
|
|
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 ref within node")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate ref") {
|
|
t.Fatalf("expected duplicate ref error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate ref across nodes rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-dup-a"
|
|
alias: "dup-a-node"
|
|
token: "token-dup-a"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-global-dup"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test-a"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
- id: "node-ws-dup-b"
|
|
alias: "dup-b-node"
|
|
token: "token-dup-b"
|
|
providers:
|
|
- id: "prov-b"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-global-dup"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test-b"
|
|
operations:
|
|
- "list"
|
|
max_read_bytes: 2048
|
|
max_output_bytes: 2048
|
|
`
|
|
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 ref across nodes")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate workspace ref") {
|
|
t.Fatalf("expected cross-node duplicate error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("non-darwin platform rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-bad-platform"
|
|
alias: "bad-platform-node"
|
|
token: "token-bad-platform"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-linux"
|
|
platform: "linux"
|
|
root: "/home/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
`
|
|
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-darwin platform")
|
|
}
|
|
if !strings.Contains(err.Error(), "platform") {
|
|
t.Fatalf("expected platform error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("relative root path rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-rel-root"
|
|
alias: "rel-root-node"
|
|
token: "token-rel-root"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-rel-root"
|
|
platform: "darwin"
|
|
root: "relative/path"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
`
|
|
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 relative root")
|
|
}
|
|
if !strings.Contains(err.Error(), "absolute path") {
|
|
t.Fatalf("expected absolute path error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("root \"/\" rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-root-slash"
|
|
alias: "root-slash-node"
|
|
token: "token-root-slash"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-root-slash"
|
|
platform: "darwin"
|
|
root: "/"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
`
|
|
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 root = /")
|
|
}
|
|
if !strings.Contains(err.Error(), `must not be "/"`) {
|
|
t.Fatalf("expected root / error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unclean root path rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-unclean"
|
|
alias: "unclean-node"
|
|
token: "token-unclean"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-unclean"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/../projects/test"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
`
|
|
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 unclean root")
|
|
}
|
|
if !strings.Contains(err.Error(), "must be clean") {
|
|
t.Fatalf("expected clean path error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty operations rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-empty-ops"
|
|
alias: "empty-ops-node"
|
|
token: "token-empty-ops"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-empty-ops"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations: []
|
|
max_read_bytes: 1024
|
|
`
|
|
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 operations")
|
|
}
|
|
if !strings.Contains(err.Error(), "operations must not be empty") {
|
|
t.Fatalf("expected empty operations error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unknown operation rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-bad-op"
|
|
alias: "bad-op-node"
|
|
token: "token-bad-op"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-bad-op"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "execute"
|
|
max_read_bytes: 1024
|
|
`
|
|
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 operation")
|
|
}
|
|
if !strings.Contains(err.Error(), "unknown operation") {
|
|
t.Fatalf("expected unknown operation error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate operation rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-dup-op"
|
|
alias: "dup-op-node"
|
|
token: "token-dup-op"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-dup-op"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
`
|
|
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")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate operation") {
|
|
t.Fatalf("expected duplicate operation error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("commands present without command operation rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-cmd-without-op"
|
|
alias: "cmd-without-op-node"
|
|
token: "token-cmd-without-op"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-cmd-without-op"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "list"
|
|
commands:
|
|
- id: "list-files"
|
|
executable: "/usr/bin/find"
|
|
args: []
|
|
max_read_bytes: 1024
|
|
`
|
|
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 commands without command operation")
|
|
}
|
|
if !strings.Contains(err.Error(), "commands must be empty") {
|
|
t.Fatalf("expected commands must be empty error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("command operation without commands rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-cmd-op-no-cmds"
|
|
alias: "cmd-op-no-cmds-node"
|
|
token: "token-cmd-op-no-cmds"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-cmd-op-no-cmds"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "command"
|
|
max_read_bytes: 1024
|
|
`
|
|
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 command operation without commands")
|
|
}
|
|
if !strings.Contains(err.Error(), "commands must not be empty") {
|
|
t.Fatalf("expected commands must not be empty error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate command id rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-dup-cmd"
|
|
alias: "dup-cmd-node"
|
|
token: "token-dup-cmd"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-dup-cmd"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "command"
|
|
commands:
|
|
- id: "list-files"
|
|
executable: "/usr/bin/find"
|
|
args: []
|
|
- id: "list-files"
|
|
executable: "/usr/bin/ls"
|
|
args: []
|
|
max_read_bytes: 1024
|
|
`
|
|
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 command id")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate command id") {
|
|
t.Fatalf("expected duplicate command id error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("non-absolute command executable rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-rel-exe"
|
|
alias: "rel-exe-node"
|
|
token: "token-rel-exe"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-rel-exe"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "command"
|
|
commands:
|
|
- id: "list-files"
|
|
executable: "relative/path/to/cmd"
|
|
args: []
|
|
max_read_bytes: 1024
|
|
`
|
|
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 relative command executable")
|
|
}
|
|
if !strings.Contains(err.Error(), "absolute path") {
|
|
t.Fatalf("expected absolute path error for executable, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid environment variable name rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-bad-env"
|
|
alias: "bad-env-node"
|
|
token: "token-bad-env"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-bad-env"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
environment_allowlist:
|
|
- "1INVALID"
|
|
`
|
|
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 env name")
|
|
}
|
|
if !strings.Contains(err.Error(), "portable environment variable name") {
|
|
t.Fatalf("expected portable env name error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate environment variable name rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-dup-env"
|
|
alias: "dup-env-node"
|
|
token: "token-dup-env"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-dup-env"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1024
|
|
environment_allowlist:
|
|
- "PATH"
|
|
- "PATH"
|
|
`
|
|
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 env name")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicate name") {
|
|
t.Fatalf("expected duplicate name error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("max_read_bytes over 1GiB rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-over-read"
|
|
alias: "over-read-node"
|
|
token: "token-over-read"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-over-read"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
max_read_bytes: 1073741825
|
|
`
|
|
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 over-limit max_read_bytes")
|
|
}
|
|
if !strings.Contains(err.Error(), "max_read_bytes must be between") {
|
|
t.Fatalf("expected max_read_bytes range error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("max_command_timeout_ms over 1 hour rejected", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-over-timeout"
|
|
alias: "over-timeout-node"
|
|
token: "token-over-timeout"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-over-timeout"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "command"
|
|
commands:
|
|
- id: "list-files"
|
|
executable: "/usr/bin/find"
|
|
args: []
|
|
max_command_timeout_ms: 3600001
|
|
`
|
|
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 over-limit timeout")
|
|
}
|
|
if !strings.Contains(err.Error(), "max_command_timeout_ms must be between") {
|
|
t.Fatalf("expected timeout range error, got %v", err)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
operations string
|
|
commands string
|
|
limits string
|
|
want string
|
|
}{
|
|
{
|
|
name: "read without max_read_bytes rejected",
|
|
operations: " - \"read\"\n",
|
|
want: "max_read_bytes must be positive",
|
|
},
|
|
{
|
|
name: "list without max_output_bytes rejected",
|
|
operations: " - \"list\"\n",
|
|
want: "max_output_bytes must be positive",
|
|
},
|
|
{
|
|
name: "write without max_write_bytes rejected",
|
|
operations: " - \"write\"\n",
|
|
want: "max_write_bytes must be positive",
|
|
},
|
|
{
|
|
name: "command without max_output_bytes rejected",
|
|
operations: " - \"command\"\n",
|
|
commands: " commands:\n - id: \"list-files\"\n executable: \"/usr/bin/find\"\n args: []\n",
|
|
limits: " max_command_timeout_ms: 1000\n",
|
|
want: "max_output_bytes must be positive",
|
|
},
|
|
{
|
|
name: "command without max_command_timeout_ms rejected",
|
|
operations: " - \"command\"\n",
|
|
commands: " commands:\n - id: \"list-files\"\n executable: \"/usr/bin/find\"\n args: []\n",
|
|
limits: " max_output_bytes: 1024\n",
|
|
want: "max_command_timeout_ms must be positive",
|
|
},
|
|
{
|
|
name: "negative max_read_bytes rejected",
|
|
operations: " - \"read\"\n",
|
|
limits: " max_read_bytes: -1\n",
|
|
want: "max_read_bytes must be between",
|
|
},
|
|
{
|
|
name: "negative max_write_bytes rejected",
|
|
operations: " - \"write\"\n",
|
|
limits: " max_write_bytes: -1\n",
|
|
want: "max_write_bytes must be between",
|
|
},
|
|
{
|
|
name: "negative max_output_bytes rejected",
|
|
operations: " - \"list\"\n",
|
|
limits: " max_output_bytes: -1\n",
|
|
want: "max_output_bytes must be between",
|
|
},
|
|
{
|
|
name: "negative max_command_timeout_ms rejected",
|
|
operations: " - \"command\"\n",
|
|
commands: " commands:\n - id: \"list-files\"\n executable: \"/usr/bin/find\"\n args: []\n",
|
|
limits: " max_output_bytes: 1024\n max_command_timeout_ms: -1\n",
|
|
want: "max_command_timeout_ms must be between",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-effective-bound"
|
|
alias: "effective-bound-node"
|
|
token: "token-effective-bound"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-effective-bound"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
` + tc.operations + tc.commands + tc.limits
|
|
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 effective bound validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("expected error containing %q, got %v", tc.want, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("boundary limit values load", func(t *testing.T) {
|
|
yaml := baseNode + `
|
|
- id: "node-ws-boundary"
|
|
alias: "boundary-node"
|
|
token: "token-boundary"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-boundary"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "write"
|
|
- "delete"
|
|
- "command"
|
|
commands:
|
|
- id: "cmd-boundary"
|
|
executable: "/usr/bin/cat"
|
|
args: []
|
|
max_read_bytes: 1
|
|
max_write_bytes: 1073741824
|
|
max_output_bytes: 1
|
|
max_command_timeout_ms: 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)
|
|
}
|
|
ws := cfg.Nodes[0].Workspaces[0]
|
|
if ws.MaxReadBytes != 1 {
|
|
t.Errorf("max_read_bytes = %d, want 1", ws.MaxReadBytes)
|
|
}
|
|
if ws.MaxWriteBytes != 1073741824 {
|
|
t.Errorf("max_write_bytes = %d, want 1073741824", ws.MaxWriteBytes)
|
|
}
|
|
if ws.MaxOutputBytes != 1 {
|
|
t.Errorf("max_output_bytes = %d, want 1", ws.MaxOutputBytes)
|
|
}
|
|
if ws.MaxCommandTimeoutMS != 1 {
|
|
t.Errorf("max_command_timeout_ms = %d, want 1", ws.MaxCommandTimeoutMS)
|
|
}
|
|
})
|
|
|
|
t.Run("boundary limit values at max load", func(t *testing.T) {
|
|
yaml := fmt.Sprintf(baseNode+`
|
|
- id: "node-ws-boundary-max"
|
|
alias: "boundary-max-node"
|
|
token: "token-boundary-max"
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "ollama"
|
|
category: "local_inference"
|
|
models: ["model-a"]
|
|
capacity: 2
|
|
workspaces:
|
|
- ref: "ws-boundary-max"
|
|
platform: "darwin"
|
|
root: "/Users/operator/projects/test"
|
|
operations:
|
|
- "read"
|
|
- "write"
|
|
- "delete"
|
|
- "command"
|
|
commands:
|
|
- id: "cmd-boundary-max"
|
|
executable: "/usr/bin/cat"
|
|
args: []
|
|
max_read_bytes: %d
|
|
max_write_bytes: %d
|
|
max_output_bytes: %d
|
|
max_command_timeout_ms: 3600000
|
|
`, 1073741824, 1073741824, 1073741824)
|
|
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)
|
|
}
|
|
ws := cfg.Nodes[0].Workspaces[0]
|
|
if ws.MaxReadBytes != 1073741824 {
|
|
t.Errorf("max_read_bytes = %d, want 1073741824", ws.MaxReadBytes)
|
|
}
|
|
if ws.MaxWriteBytes != 1073741824 {
|
|
t.Errorf("max_write_bytes = %d, want 1073741824", ws.MaxWriteBytes)
|
|
}
|
|
if ws.MaxOutputBytes != 1073741824 {
|
|
t.Errorf("max_output_bytes = %d, want 1073741824", ws.MaxOutputBytes)
|
|
}
|
|
if ws.MaxCommandTimeoutMS != 3600000 {
|
|
t.Errorf("max_command_timeout_ms = %d, want 3600000", ws.MaxCommandTimeoutMS)
|
|
}
|
|
})
|
|
}
|