- Migrate config contract files to archive (01_config_contract) - Update edge config.go for new configuration structure - Refactor node/mapper.go and mapper_test.go for field mapping - Update node/store_test.go tests - Add new fields to configs/edge.yaml - Extend packages/go/config with new configuration options - Update protobuf definitions in runtime.proto and generated code
365 lines
10 KiB
Go
365 lines
10 KiB
Go
package edgecmd
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
const minimalEdgeYAML = "server:\n listen: \"0.0.0.0:9090\"\n"
|
|
|
|
func writeMinimalEdgeYAML(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(minimalEdgeYAML), 0o600); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func TestEnvCmdMarksAutoAdvertiseHostAndOfflineStateForEmptyConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
writeMinimalEdgeYAML(t, cfgPath)
|
|
|
|
root := NewRoot(Options{})
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"env", "--config", cfgPath})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
|
|
got := out.String()
|
|
if !strings.Contains(got, "advertise_host: ") || !strings.Contains(got, " (auto)") {
|
|
t.Errorf("expected advertise_host with (auto) source, got:\n%s", got)
|
|
}
|
|
expectedArtifact := "artifact_base_url: " + defaultArtifactBaseURL(resolveAdvertiseHost("").Host) + " (derived)"
|
|
if !strings.Contains(got, expectedArtifact) {
|
|
t.Errorf("expected derived artifact_base_url %q, got:\n%s", expectedArtifact, got)
|
|
}
|
|
expectedBootstrap := "node_bootstrap: " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host))
|
|
if !strings.Contains(got, expectedBootstrap) {
|
|
t.Errorf("expected node_bootstrap %q, got:\n%s", expectedBootstrap, got)
|
|
}
|
|
if !strings.Contains(got, "openai_url: (disabled)") {
|
|
t.Errorf("expected openai_url (disabled), got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "configured_nodes: 0 (none)") {
|
|
t.Errorf("expected configured_nodes 0 (none), got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "connected_nodes: offline") {
|
|
t.Errorf("expected connected_nodes offline placeholder, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveEdgeLogPathFallsBackToBinaryDirLogsEdgeLog(t *testing.T) {
|
|
got, err := ResolveEdgeLogPath("")
|
|
if err != nil {
|
|
t.Fatalf("ResolveEdgeLogPath: %v", err)
|
|
}
|
|
want := filepath.Join(binaryDir(), "logs", "edge.log")
|
|
if got != want {
|
|
t.Fatalf("ResolveEdgeLogPath empty: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestResolveEdgeLogPathRespectsExplicitPath(t *testing.T) {
|
|
explicit := filepath.Join(t.TempDir(), "custom.log")
|
|
got, err := ResolveEdgeLogPath(explicit)
|
|
if err != nil {
|
|
t.Fatalf("ResolveEdgeLogPath: %v", err)
|
|
}
|
|
if got != explicit {
|
|
t.Fatalf("ResolveEdgeLogPath explicit: got %q, want %q", got, explicit)
|
|
}
|
|
}
|
|
|
|
func TestResolveAdvertiseHostPrefersExplicit(t *testing.T) {
|
|
got := resolveAdvertiseHost("explicit.example.test")
|
|
if got.Host != "explicit.example.test" || got.Source != "explicit" {
|
|
t.Fatalf("explicit advertise: got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveAdvertiseHostFallsBackToAuto(t *testing.T) {
|
|
got := resolveAdvertiseHost("")
|
|
if got.Host == "" {
|
|
t.Fatal("auto advertise host empty")
|
|
}
|
|
if got.Source != "auto" {
|
|
t.Fatalf("expected auto source, got %q", got.Source)
|
|
}
|
|
}
|
|
|
|
func TestAddressForCombinesAdvertiseHostWithListenPort(t *testing.T) {
|
|
if got := addressFor("10.0.0.1", "0.0.0.0:9090"); got != "10.0.0.1:9090" {
|
|
t.Errorf("addressFor: got %q", got)
|
|
}
|
|
if got := addressFor("host", ""); got != "" {
|
|
t.Errorf("addressFor empty listen: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
|
|
origGen := tokenGenerator
|
|
defer func() { tokenGenerator = origGen }()
|
|
tokenGenerator = func() string {
|
|
return "mocked-token-1234"
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
writeMinimalEdgeYAML(t, cfgPath)
|
|
|
|
root := NewRoot(Options{})
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--alias", "alias-a"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute register node-a: %v\n%s", err, out.String())
|
|
}
|
|
|
|
output := out.String()
|
|
expectedCmd := "curl -fsSL " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host)) + " | bash -s mocked-token-1234"
|
|
if strings.TrimSpace(output) != expectedCmd {
|
|
t.Errorf("expected only bootstrap command %q, got %q", expectedCmd, output)
|
|
}
|
|
|
|
data, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("read persisted config: %v", err)
|
|
}
|
|
var cfg config.EdgeConfig
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatalf("unmarshal config: %v", err)
|
|
}
|
|
|
|
if len(cfg.Nodes) != 1 {
|
|
t.Fatalf("expected exactly 1 node, got %d", len(cfg.Nodes))
|
|
}
|
|
if cfg.Nodes[0].ID != "node-a" || cfg.Nodes[0].Alias != "alias-a" || cfg.Nodes[0].Token != "mocked-token-1234" {
|
|
t.Fatalf("unexpected node state in YAML: %+v", cfg.Nodes[0])
|
|
}
|
|
|
|
root = NewRoot(Options{})
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--token", "explicit-token-abc", "--adapter", "ollama", "--ollama-base-url", "http://ollama-host:11434"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute update node-a: %v\n%s", err, out.String())
|
|
}
|
|
|
|
output = out.String()
|
|
expectedCmd2 := "curl -fsSL " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host)) + " | bash -s explicit-token-abc"
|
|
if strings.TrimSpace(output) != expectedCmd2 {
|
|
t.Errorf("expected only bootstrap command %q, got %q", expectedCmd2, output)
|
|
}
|
|
|
|
data, err = os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("read persisted config: %v", err)
|
|
}
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatalf("unmarshal config: %v", err)
|
|
}
|
|
|
|
if len(cfg.Nodes) != 1 {
|
|
t.Fatalf("expected exactly 1 node, got %d", len(cfg.Nodes))
|
|
}
|
|
if cfg.Nodes[0].Token != "explicit-token-abc" || !cfg.Nodes[0].Adapters.Ollama.Enabled || cfg.Nodes[0].Adapters.Ollama.BaseURL != "http://ollama-host:11434" {
|
|
t.Fatalf("unexpected node state after explicit token update: %+v", cfg.Nodes[0])
|
|
}
|
|
}
|
|
|
|
func TestPatchYAMLPreservesArtifactBootstrapField(t *testing.T) {
|
|
origYAML := `server:
|
|
listen: "0.0.0.0:9090"
|
|
bootstrap:
|
|
artifact_base_url: "http://old.example.com/artifacts"
|
|
`
|
|
cfg := &config.EdgeConfig{
|
|
Bootstrap: config.EdgeBootstrapConf{
|
|
ArtifactBaseURL: "http://example.com/artifacts",
|
|
},
|
|
}
|
|
|
|
patched, err := patchYAML([]byte(origYAML), cfg)
|
|
if err != nil {
|
|
t.Fatalf("patchYAML: %v", err)
|
|
}
|
|
|
|
var doc struct {
|
|
Server struct {
|
|
Listen string `yaml:"listen"`
|
|
} `yaml:"server"`
|
|
Bootstrap struct {
|
|
ArtifactBaseURL string `yaml:"artifact_base_url"`
|
|
} `yaml:"bootstrap"`
|
|
}
|
|
|
|
if err := yaml.Unmarshal(patched, &doc); err != nil {
|
|
t.Fatalf("unmarshal patched: %v", err)
|
|
}
|
|
|
|
if doc.Server.Listen != "0.0.0.0:9090" {
|
|
t.Errorf("server.listen was modified: %q", doc.Server.Listen)
|
|
}
|
|
if doc.Bootstrap.ArtifactBaseURL != "http://example.com/artifacts" {
|
|
t.Errorf("artifact_base_url = %q", doc.Bootstrap.ArtifactBaseURL)
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeConfig_VllmLegacyEmptyEndpointRejected(t *testing.T) {
|
|
cfg := &config.EdgeConfig{
|
|
Nodes: []config.NodeDefinition{
|
|
{
|
|
ID: "node-1",
|
|
Alias: "test",
|
|
Token: "tok",
|
|
Adapters: config.AdaptersConf{
|
|
Vllm: config.VllmConf{Enabled: true, Endpoint: ""},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := validateEdgeConfig(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error for enabled vllm with empty endpoint")
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeConfig_VllmInstanceEmptyEndpointRejected(t *testing.T) {
|
|
cfg := &config.EdgeConfig{
|
|
Nodes: []config.NodeDefinition{
|
|
{
|
|
ID: "node-1",
|
|
Alias: "test",
|
|
Token: "tok",
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "a100", Enabled: true, Endpoint: ""},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := validateEdgeConfig(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error for enabled vllm instance with empty endpoint")
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeConfig_AllAdaptersDisabledRejected(t *testing.T) {
|
|
cfg := &config.EdgeConfig{
|
|
Nodes: []config.NodeDefinition{
|
|
{
|
|
ID: "node-1",
|
|
Alias: "test",
|
|
Token: "tok",
|
|
Adapters: config.AdaptersConf{
|
|
Ollama: config.OllamaConf{Enabled: false},
|
|
Vllm: config.VllmConf{Enabled: false},
|
|
CLI: config.CLIConf{Enabled: false},
|
|
OllamaInstances: []config.OllamaInstanceConf{
|
|
{Name: "off", Enabled: false, BaseURL: "http://127.0.0.1:11434"},
|
|
},
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "off", Enabled: false, Endpoint: "http://10.0.0.5:8000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := validateEdgeConfig(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error when all adapters are disabled")
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeConfig_OllamaInstanceEnabledAccepted(t *testing.T) {
|
|
cfg := &config.EdgeConfig{
|
|
Nodes: []config.NodeDefinition{
|
|
{
|
|
ID: "node-1",
|
|
Alias: "test",
|
|
Token: "tok",
|
|
Adapters: config.AdaptersConf{
|
|
OllamaInstances: []config.OllamaInstanceConf{
|
|
{Name: "local", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := validateEdgeConfig(cfg); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeConfig_OpenAICompatInstanceEnabled(t *testing.T) {
|
|
cfg := &config.EdgeConfig{
|
|
Nodes: []config.NodeDefinition{
|
|
{
|
|
ID: "node-1",
|
|
Alias: "test",
|
|
Token: "tok",
|
|
Adapters: config.AdaptersConf{
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "lemonade", Enabled: true, Endpoint: "http://127.0.0.1:13305"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := validateEdgeConfig(cfg); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeConfig_OpenAICompatInstanceEmptyEndpointRejected(t *testing.T) {
|
|
cfg := &config.EdgeConfig{
|
|
Nodes: []config.NodeDefinition{
|
|
{
|
|
ID: "node-1",
|
|
Alias: "test",
|
|
Token: "tok",
|
|
Adapters: config.AdaptersConf{
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "lemonade", Enabled: true, Endpoint: ""},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := validateEdgeConfig(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error for enabled openai_compat instance with empty endpoint")
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeConfig_OpenAICompatLegacyEmptyEndpointRejected(t *testing.T) {
|
|
cfg := &config.EdgeConfig{
|
|
Nodes: []config.NodeDefinition{
|
|
{
|
|
ID: "node-1",
|
|
Alias: "test",
|
|
Token: "tok",
|
|
Adapters: config.AdaptersConf{
|
|
OpenAICompat: config.OpenAICompatConf{Enabled: true, Endpoint: ""},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := validateEdgeConfig(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error for enabled legacy openai_compat with empty endpoint")
|
|
}
|
|
}
|