iop/packages/go/config/workspace_config_test.go
toki dc9a9a8c59 feat(agent): 단일 요청 Agent 실행 경계를 구현한다
승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
2026-08-07 07:03:55 +09:00

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)
}
})
}