승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
381 lines
11 KiB
Go
381 lines
11 KiB
Go
package node_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
edgenode "iop/apps/edge/internal/node"
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
func TestLoadFromConfig_WorkspaceRefValidation(t *testing.T) {
|
|
workspace := func(ref string) config.WorkspaceDefinition {
|
|
return config.WorkspaceDefinition{Ref: ref}
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
defs []config.NodeDefinition
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "empty ref rejected",
|
|
defs: []config.NodeDefinition{{
|
|
Alias: "alpha", Token: "token-alpha", Workspaces: []config.WorkspaceDefinition{workspace(" ")},
|
|
}},
|
|
wantErr: "ref must not be empty after trim",
|
|
},
|
|
{
|
|
name: "duplicate ref within node rejected",
|
|
defs: []config.NodeDefinition{{
|
|
Alias: "alpha", Token: "token-alpha", Workspaces: []config.WorkspaceDefinition{workspace("ws-a"), workspace("ws-a")},
|
|
}},
|
|
wantErr: "duplicate workspace ref",
|
|
},
|
|
{
|
|
name: "whitespace canonical duplicate within node rejected",
|
|
defs: []config.NodeDefinition{{
|
|
Alias: "alpha", Token: "token-alpha", Workspaces: []config.WorkspaceDefinition{workspace(" ws-a "), workspace("ws-a")},
|
|
}},
|
|
wantErr: "duplicate workspace ref",
|
|
},
|
|
{
|
|
name: "duplicate ref across nodes rejected",
|
|
defs: []config.NodeDefinition{
|
|
{Alias: "alpha", Token: "token-alpha", Workspaces: []config.WorkspaceDefinition{workspace("ws-a")}},
|
|
{Alias: "beta", Token: "token-beta", Workspaces: []config.WorkspaceDefinition{workspace("ws-a")}},
|
|
},
|
|
wantErr: "duplicate workspace ref",
|
|
},
|
|
{
|
|
name: "unique refs are trimmed and accepted",
|
|
defs: []config.NodeDefinition{
|
|
{Alias: "alpha", Token: "token-alpha", Workspaces: []config.WorkspaceDefinition{workspace(" ws-a ")}},
|
|
{Alias: "beta", Token: "token-beta", Workspaces: []config.WorkspaceDefinition{workspace("ws-b")}},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig(tc.defs)
|
|
if tc.wantErr != "" {
|
|
if err == nil {
|
|
t.Fatal("expected LoadFromConfig error")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Fatalf("expected error containing %q, got %v", tc.wantErr, err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("LoadFromConfig: %v", err)
|
|
}
|
|
_, resolved, err := store.ResolveWorkspace("ws-a")
|
|
if err != nil {
|
|
t.Fatalf("ResolveWorkspace: %v", err)
|
|
}
|
|
if resolved.Ref != "ws-a" {
|
|
t.Fatalf("resolved ref = %q, want ws-a", resolved.Ref)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNodeStore_ResolveWorkspaceImmutableCopies(t *testing.T) {
|
|
defs := []config.NodeDefinition{{
|
|
ID: "node-alpha",
|
|
Alias: "alpha",
|
|
Token: "token-alpha",
|
|
Workspaces: []config.WorkspaceDefinition{{
|
|
Ref: "ws-alpha",
|
|
Platform: "darwin",
|
|
Root: "/Users/operator/projects/alpha",
|
|
Operations: []config.WorkspaceOperation{config.WorkspaceOpRead, config.WorkspaceOpCommand},
|
|
Commands: []config.WorkspaceCommandDefinition{{ID: "read-file", Executable: "/usr/bin/cat", Args: []string{"README.md"}}},
|
|
EnvironmentAllowlist: []string{"HOME"},
|
|
MaxReadBytes: 1024,
|
|
MaxOutputBytes: 2048,
|
|
MaxCommandTimeoutMS: 3000,
|
|
}},
|
|
}}
|
|
store, err := edgenode.LoadFromConfig(defs)
|
|
if err != nil {
|
|
t.Fatalf("LoadFromConfig: %v", err)
|
|
}
|
|
|
|
// Mutate the caller-owned config after loading; NodeStore must retain its snapshot.
|
|
defs[0].Workspaces[0].Ref = "source-mutated"
|
|
defs[0].Workspaces[0].Operations[0] = config.WorkspaceOpWrite
|
|
defs[0].Workspaces[0].Commands[0].Args[0] = "source-mutated.md"
|
|
defs[0].Workspaces[0].EnvironmentAllowlist[0] = "SOURCE_MUTATED"
|
|
|
|
owner, workspace, err := store.ResolveWorkspace("ws-alpha")
|
|
if err != nil {
|
|
t.Fatalf("ResolveWorkspace: %v", err)
|
|
}
|
|
if owner.ID != "node-alpha" {
|
|
t.Fatalf("owner ID = %q, want node-alpha", owner.ID)
|
|
}
|
|
|
|
// Mutate every returned catalog surface, then resolve again to ensure no
|
|
// store-owned workspace data escaped after the read lock was released.
|
|
workspace.Ref = "returned-mutated"
|
|
workspace.Operations[0] = config.WorkspaceOpWrite
|
|
workspace.Commands[0].Args[0] = "returned-mutated.md"
|
|
workspace.EnvironmentAllowlist[0] = "RETURNED_MUTATED"
|
|
owner.Workspaces[0].Ref = "owner-mutated"
|
|
owner.Workspaces[0].Operations[0] = config.WorkspaceOpWrite
|
|
owner.Workspaces[0].Commands[0].Args[0] = "owner-mutated.md"
|
|
owner.Workspaces[0].EnvironmentAllowlist[0] = "OWNER_MUTATED"
|
|
|
|
owner, workspace, err = store.ResolveWorkspace("ws-alpha")
|
|
if err != nil {
|
|
t.Fatalf("ResolveWorkspace after mutation: %v", err)
|
|
}
|
|
if owner.ID != "node-alpha" || owner.Alias != "alpha" {
|
|
t.Fatalf("owner identity changed: %+v", owner)
|
|
}
|
|
if workspace.Ref != "ws-alpha" || owner.Workspaces[0].Ref != "ws-alpha" {
|
|
t.Fatalf("stored ref mutated: workspace=%q owner=%q", workspace.Ref, owner.Workspaces[0].Ref)
|
|
}
|
|
if workspace.Operations[0] != config.WorkspaceOpRead || owner.Workspaces[0].Operations[0] != config.WorkspaceOpRead {
|
|
t.Fatalf("stored operations mutated: workspace=%v owner=%v", workspace.Operations, owner.Workspaces[0].Operations)
|
|
}
|
|
if workspace.Commands[0].Args[0] != "README.md" || owner.Workspaces[0].Commands[0].Args[0] != "README.md" {
|
|
t.Fatalf("stored command args mutated: workspace=%v owner=%v", workspace.Commands[0].Args, owner.Workspaces[0].Commands[0].Args)
|
|
}
|
|
if workspace.EnvironmentAllowlist[0] != "HOME" || owner.Workspaces[0].EnvironmentAllowlist[0] != "HOME" {
|
|
t.Fatalf("stored environment allowlist mutated: workspace=%v owner=%v", workspace.EnvironmentAllowlist, owner.Workspaces[0].EnvironmentAllowlist)
|
|
}
|
|
if _, _, err := store.ResolveWorkspace("missing"); err == nil {
|
|
t.Fatal("expected missing workspace lookup to fail")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_Success(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{Alias: "beta", Token: "token-beta"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load from config: %v", err)
|
|
}
|
|
|
|
rec, ok := store.FindByToken("token-beta")
|
|
if !ok {
|
|
t.Fatal("expected record for token-beta")
|
|
}
|
|
if rec.Alias != "beta" {
|
|
t.Fatalf("expected alias %q, got %q", "beta", rec.Alias)
|
|
}
|
|
if rec.ID == "" {
|
|
t.Fatal("expected non-empty ID")
|
|
}
|
|
|
|
if _, ok := store.FindByToken("missing"); ok {
|
|
t.Fatal("expected missing token lookup to fail")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_DuplicateToken(t *testing.T) {
|
|
_, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{Alias: "alpha", Token: "token"},
|
|
{Alias: "beta", Token: "token"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected duplicate token error")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_DuplicateAlias(t *testing.T) {
|
|
_, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{Alias: "alpha", Token: "token-alpha"},
|
|
{Alias: "alpha", Token: "token-beta"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected duplicate alias error")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_EmptyToken(t *testing.T) {
|
|
_, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{Alias: "alpha"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected empty token error")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_ExplicitID(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{ID: "my-custom-id", Alias: "alpha", Token: "token-alpha"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load from config: %v", err)
|
|
}
|
|
|
|
rec, ok := store.FindByToken("token-alpha")
|
|
if !ok {
|
|
t.Fatal("expected record for token-alpha")
|
|
}
|
|
if rec.ID != "my-custom-id" {
|
|
t.Fatalf("expected id %q, got %q", "my-custom-id", rec.ID)
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_AutoID(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{Alias: "alpha", Token: "token-alpha"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load from config: %v", err)
|
|
}
|
|
|
|
rec, ok := store.FindByToken("token-alpha")
|
|
if !ok {
|
|
t.Fatal("expected record for token-alpha")
|
|
}
|
|
if rec.ID == "" {
|
|
t.Fatal("expected auto-generated non-empty ID")
|
|
}
|
|
if rec.ID == "node-alpha" {
|
|
t.Fatal("expected UUID, not alias-derived ID")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_DuplicateID(t *testing.T) {
|
|
_, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{ID: "node-dup", Alias: "alpha", Token: "token-alpha"},
|
|
{ID: "node-dup", Alias: "beta", Token: "token-beta"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected duplicate id error")
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_LegacyOllamaInPayload(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{
|
|
ID: "node-ollama",
|
|
Alias: "ollama-node",
|
|
Token: "token-ollama",
|
|
Adapters: config.AdaptersConf{
|
|
Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://127.0.0.1:11434"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load from config: %v", err)
|
|
}
|
|
rec, ok := store.FindByToken("token-ollama")
|
|
if !ok {
|
|
t.Fatal("expected record for token-ollama")
|
|
}
|
|
payload, err := edgenode.BuildConfigPayload(rec)
|
|
if err != nil {
|
|
t.Fatalf("build config payload: %v", err)
|
|
}
|
|
var found bool
|
|
for _, a := range payload.GetAdapters() {
|
|
if a.GetType() == "ollama" && a.GetOllama().GetBaseUrl() == "http://127.0.0.1:11434" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected ollama adapter in payload with correct base_url, got %v", payload.GetAdapters())
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_LegacyVllmInPayload(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{
|
|
ID: "node-vllm",
|
|
Alias: "vllm-node",
|
|
Token: "token-vllm",
|
|
Adapters: config.AdaptersConf{
|
|
Vllm: config.VllmConf{Enabled: true, Endpoint: "http://127.0.0.1:8000"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load from config: %v", err)
|
|
}
|
|
rec, ok := store.FindByToken("token-vllm")
|
|
if !ok {
|
|
t.Fatal("expected record for token-vllm")
|
|
}
|
|
payload, err := edgenode.BuildConfigPayload(rec)
|
|
if err != nil {
|
|
t.Fatalf("build config payload: %v", err)
|
|
}
|
|
var found bool
|
|
for _, a := range payload.GetAdapters() {
|
|
if a.GetType() == "vllm" && a.GetVllm().GetEndpoint() == "http://127.0.0.1:8000" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected vllm adapter in payload with correct endpoint, got %v", payload.GetAdapters())
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_AutoIDUnique(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{Alias: "alpha", Token: "token-alpha"},
|
|
{Alias: "beta", Token: "token-beta"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load from config: %v", err)
|
|
}
|
|
|
|
recA, ok := store.FindByToken("token-alpha")
|
|
if !ok {
|
|
t.Fatal("expected record for token-alpha")
|
|
}
|
|
recB, ok := store.FindByToken("token-beta")
|
|
if !ok {
|
|
t.Fatal("expected record for token-beta")
|
|
}
|
|
if recA.ID == recB.ID {
|
|
t.Fatalf("expected unique IDs, both are %q", recA.ID)
|
|
}
|
|
}
|
|
|
|
func TestLoadFromConfig_LegacyOpenAICompatInPayload(t *testing.T) {
|
|
store, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{
|
|
ID: "node-oai",
|
|
Alias: "oai-node",
|
|
Token: "token-oai",
|
|
Adapters: config.AdaptersConf{
|
|
OpenAICompat: config.OpenAICompatConf{
|
|
Enabled: true,
|
|
Provider: "lemonade",
|
|
Endpoint: "http://127.0.0.1:13305",
|
|
Headers: map[string]string{"authorization": "Bearer token"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load from config: %v", err)
|
|
}
|
|
rec, ok := store.FindByToken("token-oai")
|
|
if !ok {
|
|
t.Fatal("expected record for token-oai")
|
|
}
|
|
payload, err := edgenode.BuildConfigPayload(rec)
|
|
if err != nil {
|
|
t.Fatalf("build config payload: %v", err)
|
|
}
|
|
var found bool
|
|
for _, a := range payload.GetAdapters() {
|
|
if a.GetType() == "openai_compat" {
|
|
oai := a.GetOpenaiCompat()
|
|
if oai != nil && oai.GetProvider() == "lemonade" && oai.GetEndpoint() == "http://127.0.0.1:13305" && oai.GetHeaders()["authorization"] == "Bearer token" {
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected openai_compat adapter in payload with correct config, got %v", payload.GetAdapters())
|
|
}
|
|
}
|