- edge bootstrap runtime 개선 - edge 설정 및 config 업데이트 - hostsetup 테스트 및 템플릿 수정 - Makefile 업데이트 - E2E 테스트 스크립트 추가 - 아키텍처 태스크 아카이브
876 lines
24 KiB
Go
876 lines
24 KiB
Go
package config_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/config"
|
|
)
|
|
|
|
func TestLoadEdge_EdgeIdentity(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nedge:\n id: \"edge-dgx-group\"\n name: \"DGX Group\"\n"
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Edge.ID != "edge-dgx-group" {
|
|
t.Fatalf("expected edge.id=%q, got %q", "edge-dgx-group", cfg.Edge.ID)
|
|
}
|
|
if cfg.Edge.Name != "DGX Group" {
|
|
t.Fatalf("expected edge.name=%q, got %q", "DGX Group", cfg.Edge.Name)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_EdgeIdentityEmptyByDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Edge.ID != "" || cfg.Edge.Name != "" {
|
|
t.Fatalf("expected empty edge identity by default, got id=%q name=%q", cfg.Edge.ID, cfg.Edge.Name)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTimeoutDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.TimeoutSec != 120 {
|
|
t.Fatalf("expected default timeout_sec=120, got %d", cfg.Console.TimeoutSec)
|
|
}
|
|
if cfg.Console.Adapter != "cli" {
|
|
t.Fatalf("expected default console.adapter=%q, got %q", "cli", cfg.Console.Adapter)
|
|
}
|
|
if cfg.Console.ResolveTarget() != "claude" {
|
|
t.Fatalf("expected default console.target=%q, got %q", "claude", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.OpenAI.Enabled {
|
|
t.Fatal("expected openai.enabled=false by default")
|
|
}
|
|
if cfg.OpenAI.Listen != "0.0.0.0:8080" {
|
|
t.Fatalf("expected openai.listen default, got %q", cfg.OpenAI.Listen)
|
|
}
|
|
if cfg.OpenAI.Adapter != "ollama" {
|
|
t.Fatalf("expected openai.adapter=%q, got %q", "ollama", cfg.OpenAI.Adapter)
|
|
}
|
|
if cfg.OpenAI.SessionID != "openai" {
|
|
t.Fatalf("expected openai.session_id=%q, got %q", "openai", cfg.OpenAI.SessionID)
|
|
}
|
|
if cfg.OpenAI.TimeoutSec != 120 {
|
|
t.Fatalf("expected openai.timeout_sec=120, got %d", cfg.OpenAI.TimeoutSec)
|
|
}
|
|
if !cfg.OpenAI.StrictOutput {
|
|
t.Fatal("expected openai.strict_output=true")
|
|
}
|
|
if cfg.OpenAI.StrictStreamBuffer {
|
|
t.Fatal("expected openai.strict_stream_buffer=false")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpenAIOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
listen: "127.0.0.1:8088"
|
|
node: "node0"
|
|
adapter: "ollama"
|
|
target: "llama-test"
|
|
models:
|
|
- "llama-test"
|
|
session_id: "cline"
|
|
timeout_sec: 45
|
|
strict_output: false
|
|
strict_stream_buffer: true
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if !cfg.OpenAI.Enabled {
|
|
t.Fatal("expected openai.enabled=true")
|
|
}
|
|
if cfg.OpenAI.Listen != "127.0.0.1:8088" || cfg.OpenAI.NodeRef != "node0" {
|
|
t.Fatalf("unexpected openai routing config: %+v", cfg.OpenAI)
|
|
}
|
|
if cfg.OpenAI.Target != "llama-test" || len(cfg.OpenAI.Models) != 1 || cfg.OpenAI.Models[0] != "llama-test" {
|
|
t.Fatalf("unexpected openai model config: %+v", cfg.OpenAI)
|
|
}
|
|
if cfg.OpenAI.SessionID != "cline" || cfg.OpenAI.TimeoutSec != 45 {
|
|
t.Fatalf("unexpected openai execution config: %+v", cfg.OpenAI)
|
|
}
|
|
if cfg.OpenAI.StrictOutput {
|
|
t.Fatalf("unexpected openai strict output config: %+v", cfg.OpenAI)
|
|
}
|
|
if !cfg.OpenAI.StrictStreamBuffer {
|
|
t.Fatalf("unexpected openai strict stream buffer config: %+v", cfg.OpenAI)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OllamaContextSize(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://192.168.0.91:11434"
|
|
context_size: 262144
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
ollama := cfg.Nodes[0].Adapters.Ollama
|
|
if !ollama.Enabled {
|
|
t.Fatal("expected ollama.enabled=true")
|
|
}
|
|
if ollama.BaseURL != "http://192.168.0.91:11434" || ollama.ContextSize != 262144 {
|
|
t.Fatalf("unexpected ollama config: %+v", ollama)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleSessionDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.SessionID != "default" {
|
|
t.Fatalf("expected default session_id=%q, got %q", "default", cfg.Console.SessionID)
|
|
}
|
|
if cfg.Console.Background {
|
|
t.Fatal("expected default background=false")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleSessionOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n session_id: \"worker-1\"\n background: true\n"
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.SessionID != "worker-1" {
|
|
t.Fatalf("expected session_id=%q, got %q", "worker-1", cfg.Console.SessionID)
|
|
}
|
|
if !cfg.Console.Background {
|
|
t.Fatal("expected background=true")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTimeoutOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n timeout_sec: 45\n"
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.TimeoutSec != 45 {
|
|
t.Fatalf("expected timeout_sec=45, got %d", cfg.Console.TimeoutSec)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTargetOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n target: \"codex\"\n"
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.ResolveTarget() != "codex" {
|
|
t.Fatalf("expected console.target to resolve to %q, got %q", "codex", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTargetFallbackFromLegacyAgent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n agent: \"codex\"\n"
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.ResolveTarget() != "codex" {
|
|
t.Fatalf("expected legacy console.agent fallback to resolve to %q, got %q", "codex", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ConsoleTargetFallbackFromLegacyModel(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := "server:\n listen: \"0.0.0.0:9090\"\nconsole:\n model: \"codex\"\n"
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Console.ResolveTarget() != "codex" {
|
|
t.Fatalf("expected legacy console.model fallback to resolve to %q, got %q", "codex", cfg.Console.ResolveTarget())
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_CodexProfile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
codex:
|
|
command: "codex"
|
|
output_format: "codex-json"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
if len(cfg.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
codex, ok := cfg.Nodes[0].Adapters.CLI.Profiles["codex"]
|
|
if !ok {
|
|
t.Fatal("expected codex profile")
|
|
}
|
|
if codex.Command != "codex" || codex.OutputFormat != "codex-json" {
|
|
t.Errorf("unexpected codex profile: %+v", codex)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_AntigravityProfile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
antigravity:
|
|
command: "agy"
|
|
args:
|
|
- "--dangerously-skip-permissions"
|
|
- "--print-timeout"
|
|
- "10m"
|
|
- "--print"
|
|
resume_args:
|
|
- "--dangerously-skip-permissions"
|
|
- "--print-timeout"
|
|
- "10m"
|
|
- "--conversation"
|
|
mode: "antigravity-print"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
if len(cfg.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
agy, ok := cfg.Nodes[0].Adapters.CLI.Profiles["antigravity"]
|
|
if !ok {
|
|
t.Fatal("expected antigravity profile")
|
|
}
|
|
if agy.Command != "agy" {
|
|
t.Errorf("expected command agy, got %q", agy.Command)
|
|
}
|
|
expectedArgs := []string{"--dangerously-skip-permissions", "--print-timeout", "10m", "--print"}
|
|
if len(agy.Args) != len(expectedArgs) {
|
|
t.Fatalf("expected %d args, got %d: %v", len(expectedArgs), len(agy.Args), agy.Args)
|
|
}
|
|
for i, v := range expectedArgs {
|
|
if agy.Args[i] != v {
|
|
t.Errorf("args[%d]: expected %q, got %q", i, v, agy.Args[i])
|
|
}
|
|
}
|
|
if agy.OutputFormat != "" {
|
|
t.Errorf("expected empty output format, got %q", agy.OutputFormat)
|
|
}
|
|
if agy.Mode != "antigravity-print" {
|
|
t.Errorf("expected mode antigravity-print, got %q", agy.Mode)
|
|
}
|
|
expectedResume := []string{"--dangerously-skip-permissions", "--print-timeout", "10m", "--conversation"}
|
|
if len(agy.ResumeArgs) != len(expectedResume) {
|
|
t.Fatalf("expected %d resume_args, got %d: %v", len(expectedResume), len(agy.ResumeArgs), agy.ResumeArgs)
|
|
}
|
|
for i, v := range expectedResume {
|
|
if agy.ResumeArgs[i] != v {
|
|
t.Errorf("resume_args[%d]: expected %q, got %q", i, v, agy.ResumeArgs[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLIProfileConf_CompletionMarkerUnmarshal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
marker-test:
|
|
command: "mycli"
|
|
completion_marker:
|
|
line: "<<END_OF_RESPONSE>>"
|
|
regex: "^DONE \\d+$"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
if len(cfg.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["marker-test"]
|
|
if !ok {
|
|
t.Fatal("expected marker-test profile")
|
|
}
|
|
if profile.CompletionMarker.Line != "<<END_OF_RESPONSE>>" {
|
|
t.Errorf("expected Line=%q, got %q", "<<END_OF_RESPONSE>>", profile.CompletionMarker.Line)
|
|
}
|
|
if profile.CompletionMarker.Regex != "^DONE \\d+$" {
|
|
t.Errorf("expected Regex=%q, got %q", "^DONE \\d+$", profile.CompletionMarker.Regex)
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_Empty(t *testing.T) {
|
|
var empty config.CompletionMarkerConf
|
|
if !empty.Empty() {
|
|
t.Fatal("expected Empty() == true for zero value")
|
|
}
|
|
|
|
lineOnly := config.CompletionMarkerConf{Line: "<<END>>"}
|
|
if lineOnly.Empty() {
|
|
t.Fatal("expected Empty() == false when Line is set")
|
|
}
|
|
|
|
regexOnly := config.CompletionMarkerConf{Regex: "^END$"}
|
|
if regexOnly.Empty() {
|
|
t.Fatal("expected Empty() == false when Regex is set")
|
|
}
|
|
|
|
bothSet := config.CompletionMarkerConf{Line: "<<END>>", Regex: "^END$"}
|
|
if bothSet.Empty() {
|
|
t.Fatal("expected Empty() == false when both are set")
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_LineOnlyUnmarshal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
line-only:
|
|
command: "mycli"
|
|
completion_marker:
|
|
line: "<<END>>"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["line-only"]
|
|
if !ok {
|
|
t.Fatal("expected line-only profile")
|
|
}
|
|
if profile.CompletionMarker.Line != "<<END>>" {
|
|
t.Errorf("expected Line=%q, got %q", "<<END>>", profile.CompletionMarker.Line)
|
|
}
|
|
if profile.CompletionMarker.Regex != "" {
|
|
t.Errorf("expected empty Regex, got %q", profile.CompletionMarker.Regex)
|
|
}
|
|
if profile.CompletionMarker.Empty() {
|
|
t.Fatal("expected Empty() == false when only Line is set")
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_RegexOnlyUnmarshal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
regex-only:
|
|
command: "mycli"
|
|
completion_marker:
|
|
regex: "^DONE \\d+$"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["regex-only"]
|
|
if !ok {
|
|
t.Fatal("expected regex-only profile")
|
|
}
|
|
if profile.CompletionMarker.Regex != "^DONE \\d+$" {
|
|
t.Errorf("expected Regex=%q, got %q", "^DONE \\d+$", profile.CompletionMarker.Regex)
|
|
}
|
|
if profile.CompletionMarker.Line != "" {
|
|
t.Errorf("expected empty Line, got %q", profile.CompletionMarker.Line)
|
|
}
|
|
if profile.CompletionMarker.Empty() {
|
|
t.Fatal("expected Empty() == false when only Regex is set")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_CLIProfileMode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
codex:
|
|
command: "codex"
|
|
args:
|
|
- "exec"
|
|
- "--json"
|
|
resume_args:
|
|
- "exec"
|
|
- "resume"
|
|
- "--json"
|
|
mode: "codex-exec"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
if len(cfg.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["codex"]
|
|
if !ok {
|
|
t.Fatal("expected codex profile")
|
|
}
|
|
if profile.Mode != "codex-exec" {
|
|
t.Errorf("expected mode=%q, got %q", "codex-exec", profile.Mode)
|
|
}
|
|
expectedResume := []string{"exec", "resume", "--json"}
|
|
if len(profile.ResumeArgs) != len(expectedResume) {
|
|
t.Fatalf("expected %d resume_args, got %d: %v", len(expectedResume), len(profile.ResumeArgs), profile.ResumeArgs)
|
|
}
|
|
for i, v := range expectedResume {
|
|
if profile.ResumeArgs[i] != v {
|
|
t.Errorf("resume_args[%d]: expected %q, got %q", i, v, profile.ResumeArgs[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_OpencodeSSEProfile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
opencode:
|
|
command: "/usr/local/bin/opencode"
|
|
args:
|
|
- "--title"
|
|
- "untitle"
|
|
- "--model"
|
|
- "ollama-dgx/qwen3.6:35b-a3b-bf16"
|
|
- "--dangerously-skip-permissions"
|
|
mode: "opencode-sse"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(cfg.Nodes) == 0 {
|
|
t.Fatal("expected 1 node")
|
|
}
|
|
prof, ok := cfg.Nodes[0].Adapters.CLI.Profiles["opencode"]
|
|
if !ok {
|
|
t.Fatal("expected opencode profile")
|
|
}
|
|
if prof.Mode != "opencode-sse" {
|
|
t.Errorf("mode: got %q", prof.Mode)
|
|
}
|
|
expectedArgs := []string{"--title", "untitle", "--model", "ollama-dgx/qwen3.6:35b-a3b-bf16", "--dangerously-skip-permissions"}
|
|
if len(prof.Args) != len(expectedArgs) {
|
|
t.Fatalf("args len: got %d, want %d", len(prof.Args), len(expectedArgs))
|
|
}
|
|
for i, want := range expectedArgs {
|
|
if prof.Args[i] != want {
|
|
t.Errorf("args[%d]: got %q, want %q", i, prof.Args[i], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLIProfileConf_CompletionMarkerEmpty(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- alias: "local"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
no-marker:
|
|
command: "mycli"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
profile, ok := cfg.Nodes[0].Adapters.CLI.Profiles["no-marker"]
|
|
if !ok {
|
|
t.Fatal("expected no-marker profile")
|
|
}
|
|
if !profile.CompletionMarker.Empty() {
|
|
t.Fatal("expected Empty() == true when completion_marker is not specified")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_AdvertiseHostAndArtifactDefaultsEmpty(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Server.AdvertiseHost != "" {
|
|
t.Errorf("expected empty server.advertise_host by default, got %q", cfg.Server.AdvertiseHost)
|
|
}
|
|
if cfg.Bootstrap.ArtifactBaseURL != "" {
|
|
t.Errorf("expected empty bootstrap.artifact_base_url by default, got %q", cfg.Bootstrap.ArtifactBaseURL)
|
|
}
|
|
if cfg.Logging.Path != "" {
|
|
t.Errorf("expected empty logging.path by default, got %q", cfg.Logging.Path)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_AdvertiseHostAndArtifactExplicit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `server:
|
|
listen: "0.0.0.0:9090"
|
|
advertise_host: "edge.example.test"
|
|
bootstrap:
|
|
artifact_base_url: "http://edge.example.test:18080"
|
|
logging:
|
|
path: "/var/log/iop/edge.log"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.Server.AdvertiseHost != "edge.example.test" {
|
|
t.Errorf("server.advertise_host=%q", cfg.Server.AdvertiseHost)
|
|
}
|
|
if cfg.Bootstrap.ArtifactBaseURL != "http://edge.example.test:18080" {
|
|
t.Errorf("bootstrap.artifact_base_url=%q", cfg.Bootstrap.ArtifactBaseURL)
|
|
}
|
|
if cfg.Logging.Path != "/var/log/iop/edge.log" {
|
|
t.Errorf("logging.path=%q", cfg.Logging.Path)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_A2ADefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.A2A.Enabled {
|
|
t.Fatal("expected a2a.enabled=false by default")
|
|
}
|
|
if cfg.A2A.Listen != "0.0.0.0:8081" {
|
|
t.Fatalf("expected a2a.listen default, got %q", cfg.A2A.Listen)
|
|
}
|
|
if cfg.A2A.Path != "/a2a" {
|
|
t.Fatalf("expected a2a.path=%q, got %q", "/a2a", cfg.A2A.Path)
|
|
}
|
|
if cfg.A2A.Adapter != "cli" {
|
|
t.Fatalf("expected a2a.adapter=%q, got %q", "cli", cfg.A2A.Adapter)
|
|
}
|
|
if cfg.A2A.SessionID != "a2a" {
|
|
t.Fatalf("expected a2a.session_id=%q, got %q", "a2a", cfg.A2A.SessionID)
|
|
}
|
|
if cfg.A2A.TimeoutSec != 120 {
|
|
t.Fatalf("expected a2a.timeout_sec=120, got %d", cfg.A2A.TimeoutSec)
|
|
}
|
|
if cfg.A2A.BearerToken != "" {
|
|
t.Fatalf("expected a2a.bearer_token empty by default, got %q", cfg.A2A.BearerToken)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_A2AOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
a2a:
|
|
enabled: true
|
|
listen: "127.0.0.1:8082"
|
|
path: "/agent"
|
|
node: "node0"
|
|
adapter: "cli"
|
|
target: "claude"
|
|
session_id: "nomad"
|
|
timeout_sec: 60
|
|
bearer_token: "secret-token"
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if !cfg.A2A.Enabled {
|
|
t.Fatal("expected a2a.enabled=true")
|
|
}
|
|
if cfg.A2A.Listen != "127.0.0.1:8082" {
|
|
t.Fatalf("expected a2a.listen=%q, got %q", "127.0.0.1:8082", cfg.A2A.Listen)
|
|
}
|
|
if cfg.A2A.Path != "/agent" {
|
|
t.Fatalf("expected a2a.path=%q, got %q", "/agent", cfg.A2A.Path)
|
|
}
|
|
if cfg.A2A.NodeRef != "node0" {
|
|
t.Fatalf("expected a2a.node=%q, got %q", "node0", cfg.A2A.NodeRef)
|
|
}
|
|
if cfg.A2A.Target != "claude" {
|
|
t.Fatalf("expected a2a.target=%q, got %q", "claude", cfg.A2A.Target)
|
|
}
|
|
if cfg.A2A.SessionID != "nomad" {
|
|
t.Fatalf("expected a2a.session_id=%q, got %q", "nomad", cfg.A2A.SessionID)
|
|
}
|
|
if cfg.A2A.TimeoutSec != 60 {
|
|
t.Fatalf("expected a2a.timeout_sec=60, got %d", cfg.A2A.TimeoutSec)
|
|
}
|
|
if cfg.A2A.BearerToken != "secret-token" {
|
|
t.Fatalf("expected a2a.bearer_token=%q, got %q", "secret-token", cfg.A2A.BearerToken)
|
|
}
|
|
}
|
|
|
|
func TestCompletionMarkerConf_RegexAndLineUsage(t *testing.T) {
|
|
lineOnly := config.CompletionMarkerConf{Line: "<<END>>"}
|
|
if !strings.Contains(lineOnly.Line, "<<END>>") {
|
|
t.Errorf("expected Line to contain <<END>>, got %q", lineOnly.Line)
|
|
}
|
|
|
|
regexOnly := config.CompletionMarkerConf{Regex: "^DONE \\d+$"}
|
|
if !strings.Contains(regexOnly.Regex, "DONE") {
|
|
t.Errorf("expected Regex to contain DONE, got %q", regexOnly.Regex)
|
|
}
|
|
|
|
both := config.CompletionMarkerConf{Line: "<<END>>", Regex: "^DONE \\d+$"}
|
|
if both.Line != "<<END>>" || both.Regex != "^DONE \\d+$" {
|
|
t.Errorf("expected both fields set, got %+v", both)
|
|
}
|
|
if both.Empty() {
|
|
t.Fatal("expected Empty() == false when both are set")
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ControlPlaneDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.ControlPlane.Enabled {
|
|
t.Fatal("expected control_plane.enabled=false by default")
|
|
}
|
|
if cfg.ControlPlane.WireAddr != "" {
|
|
t.Fatalf("expected control_plane.wire_addr empty by default, got %q", cfg.ControlPlane.WireAddr)
|
|
}
|
|
if cfg.ControlPlane.ReconnectIntervalSec != 5 {
|
|
t.Fatalf("expected control_plane.reconnect_interval_sec=5, got %d", cfg.ControlPlane.ReconnectIntervalSec)
|
|
}
|
|
}
|
|
|
|
func TestLoadEdge_ControlPlaneOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "edge.yaml")
|
|
yaml := `
|
|
server:
|
|
listen: "0.0.0.0:9090"
|
|
control_plane:
|
|
enabled: true
|
|
wire_addr: "cp.example.test:7070"
|
|
reconnect_interval_sec: 10
|
|
`
|
|
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
cfg, err := config.LoadEdge(f)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if !cfg.ControlPlane.Enabled {
|
|
t.Fatal("expected control_plane.enabled=true")
|
|
}
|
|
if cfg.ControlPlane.WireAddr != "cp.example.test:7070" {
|
|
t.Fatalf("expected control_plane.wire_addr=%q, got %q", "cp.example.test:7070", cfg.ControlPlane.WireAddr)
|
|
}
|
|
if cfg.ControlPlane.ReconnectIntervalSec != 10 {
|
|
t.Fatalf("expected control_plane.reconnect_interval_sec=10, got %d", cfg.ControlPlane.ReconnectIntervalSec)
|
|
}
|
|
}
|