iop/apps/edge/internal/service/service_test.go
toki 7008ff888f feat: edge node registry and service updates, roadmap sync
- Update edge node registry with snapshot tracking
- Fix mapper and store tests for registry changes
- Update transport server integration tests
- Add config support for new edge settings
- Sync roadmap with current phase progress
- Archive completed task groups
2026-06-03 17:53:44 +09:00

385 lines
12 KiB
Go

package service_test
import (
"testing"
"time"
edgenode "iop/apps/edge/internal/node"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
func TestBuildRunRequestNormalizesSessionAndTimeout(t *testing.T) {
req, runID, err := edgeservice.BuildRunRequest(edgeservice.SubmitRunRequest{
Adapter: "cli",
Target: "codex",
Prompt: "hello",
})
if err != nil {
t.Fatalf("BuildRunRequest: %v", err)
}
if runID == "" || req.GetRunId() != runID {
t.Fatalf("run id mismatch: runID=%q req=%q", runID, req.GetRunId())
}
if req.GetSessionId() != edgeservice.DefaultSessionID {
t.Fatalf("SessionId: got %q want %q", req.GetSessionId(), edgeservice.DefaultSessionID)
}
if req.GetTimeoutSec() != edgeservice.DefaultTimeoutSec {
t.Fatalf("TimeoutSec: got %d want %d", req.GetTimeoutSec(), edgeservice.DefaultTimeoutSec)
}
if req.GetSessionMode() != iop.RunSessionMode_RUN_SESSION_MODE_CREATE_IF_MISSING {
t.Fatalf("SessionMode: got %v", req.GetSessionMode())
}
}
func TestBuildNodeCommandRequest_NewTypes(t *testing.T) {
cases := []struct {
name string
cmdType iop.NodeCommandType
prefix string
}{
{"capabilities", iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES, "caps"},
{"sessions", iop.NodeCommandType_NODE_COMMAND_TYPE_SESSION_LIST, "sessions"},
{"transport", iop.NodeCommandType_NODE_COMMAND_TYPE_TRANSPORT_STATUS, "transport"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := edgeservice.BuildNodeCommandRequest(tc.cmdType, tc.prefix, "cli", "codex", "", 0)
if req.GetType() != tc.cmdType {
t.Errorf("Type: got %v want %v", req.GetType(), tc.cmdType)
}
if req.GetAdapter() != "cli" {
t.Errorf("Adapter: got %q want cli", req.GetAdapter())
}
if req.GetTarget() != "codex" {
t.Errorf("Target: got %q want codex", req.GetTarget())
}
if req.GetSessionId() != edgeservice.DefaultSessionID {
t.Errorf("SessionId: got %q want default", req.GetSessionId())
}
if req.GetTimeoutSec() != edgeservice.DefaultTimeoutSec {
t.Errorf("TimeoutSec: got %d want default", req.GetTimeoutSec())
}
if req.GetRequestId() == "" {
t.Error("RequestId: empty")
}
})
}
}
func TestBuildUsageStatusRequestAndWaitTimeout(t *testing.T) {
req := edgeservice.BuildUsageStatusRequest("cli", "antigravity", "", 0)
if req.GetSessionId() != edgeservice.DefaultSessionID {
t.Fatalf("SessionId: got %q want %q", req.GetSessionId(), edgeservice.DefaultSessionID)
}
if req.GetTimeoutSec() != edgeservice.DefaultTimeoutSec {
t.Fatalf("TimeoutSec: got %d want %d", req.GetTimeoutSec(), edgeservice.DefaultTimeoutSec)
}
if got, want := edgeservice.StatusWaitTimeout(req), 35*time.Second; got != want {
t.Fatalf("StatusWaitTimeout: got %v want %v", got, want)
}
}
func TestBuildRunRequest_SessionAndBackground(t *testing.T) {
req, runID, err := edgeservice.BuildRunRequest(edgeservice.SubmitRunRequest{
Adapter: "cli",
Target: "codex",
SessionID: "session-a",
Background: true,
TimeoutSec: 30,
Prompt: "hello",
})
if err != nil {
t.Fatalf("BuildRunRequest: %v", err)
}
if runID == "" {
t.Fatal("expected non-empty runID")
}
if req.GetSessionId() != "session-a" {
t.Errorf("SessionId: got %q want %q", req.GetSessionId(), "session-a")
}
if !req.GetBackground() {
t.Error("Background: expected true")
}
}
func TestResolveNode_RequiresExplicitSelectionForMultipleNodes(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-1"})
reg.Register(&edgenode.NodeEntry{NodeID: "node-2"})
svc := edgeservice.New(reg, nil)
if _, err := svc.ResolveNode(""); err == nil {
t.Error("expected error for implicit resolve with multiple nodes")
}
if e, err := svc.ResolveNode("node-1"); err != nil || e.NodeID != "node-1" {
t.Errorf("failed explicit resolve: %v", err)
}
}
func TestBuildRunRequestDoesNotInjectConsoleSource(t *testing.T) {
req, _, err := edgeservice.BuildRunRequest(edgeservice.SubmitRunRequest{
Adapter: "cli",
Target: "codex",
Prompt: "hello",
Metadata: nil,
})
if err != nil {
t.Fatalf("BuildRunRequest: %v", err)
}
if req.GetMetadata() == nil {
t.Fatal("expected non-nil metadata map")
}
if v, ok := req.GetMetadata()["source"]; ok {
t.Errorf("BuildRunRequest should not inject source, got source=%q", v)
}
}
func TestBuildRunRequestCopiesMetadata(t *testing.T) {
req, _, err := edgeservice.BuildRunRequest(edgeservice.SubmitRunRequest{
Adapter: "cli",
Target: "codex",
Prompt: "hello",
Metadata: map[string]string{"source": "edge-ops-console", "x-custom": "value"},
})
if err != nil {
t.Fatalf("BuildRunRequest: %v", err)
}
if got := req.GetMetadata()["source"]; got != "edge-ops-console" {
t.Errorf("source: got %q, want %q", got, "edge-ops-console")
}
if got := req.GetMetadata()["x-custom"]; got != "value" {
t.Errorf("x-custom: got %q, want %q", got, "value")
}
}
func TestListNodesReturnsSnapshots(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alpha"})
reg.Register(&edgenode.NodeEntry{NodeID: "node-2"})
svc := edgeservice.New(reg, nil)
snaps := svc.ListNodeSnapshots()
if len(snaps) != 2 {
t.Fatalf("ListNodeSnapshots: got %d want 2", len(snaps))
}
byID := map[string]edgeservice.NodeSnapshot{}
for _, s := range snaps {
byID[s.NodeID] = s
}
if got := byID["node-1"]; got.Alias != "alpha" || got.Label != "node0" {
t.Errorf("node-1 snapshot: %+v, want Label=node0", got)
}
if got := byID["node-2"]; got.Alias != "" || got.Label != "node1" {
t.Errorf("node-2 snapshot: %+v, want Label=node1", got)
}
}
func TestResolveNodeSnapshotReturnsDTO(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alpha"})
svc := edgeservice.New(reg, nil)
snap, err := svc.ResolveNodeSnapshot("alpha")
if err != nil {
t.Fatalf("ResolveNodeSnapshot: %v", err)
}
if snap.NodeID != "node-1" || snap.Label != "node0" {
t.Errorf("snapshot: %+v", snap)
}
}
func TestSubmitRunReturnsDispatchMetadata(t *testing.T) {
dispatch := edgeservice.RunDispatch{
RunID: "run-x",
NodeID: "node-1",
NodeLabel: "alpha",
Adapter: "cli",
Target: "codex",
SessionID: "session-a",
Background: true,
TimeoutSec: 30,
}
handle := &edgeservice.RunHandle{RunDispatch: dispatch}
if handle.RunID != dispatch.RunID || handle.NodeLabel != dispatch.NodeLabel {
t.Fatalf("RunHandle does not expose embedded RunDispatch fields: %+v", handle)
}
if handle.Background != true || handle.TimeoutSec != 30 {
t.Errorf("RunHandle dispatch fields wrong: %+v", handle)
}
}
func TestBuildCancelRunRequest(t *testing.T) {
cases := []struct {
name string
req edgeservice.CancelRunRequest
wantSessionID string
}{
{
name: "explicit session",
req: edgeservice.CancelRunRequest{
RunID: "run-123",
Adapter: "cli",
Target: "claude",
SessionID: "session-a",
},
wantSessionID: "session-a",
},
{
name: "empty session normalizes to default",
req: edgeservice.CancelRunRequest{
RunID: "run-456",
Adapter: "ollama",
Target: "llama3",
SessionID: "",
},
wantSessionID: edgeservice.DefaultSessionID,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
proto := edgeservice.BuildCancelRunRequest(tc.req)
if proto.GetRunId() != tc.req.RunID {
t.Errorf("RunId: got %q want %q", proto.GetRunId(), tc.req.RunID)
}
if proto.GetAdapter() != tc.req.Adapter {
t.Errorf("Adapter: got %q want %q", proto.GetAdapter(), tc.req.Adapter)
}
if proto.GetTarget() != tc.req.Target {
t.Errorf("Target: got %q want %q", proto.GetTarget(), tc.req.Target)
}
if proto.GetSessionId() != tc.wantSessionID {
t.Errorf("SessionId: got %q want %q", proto.GetSessionId(), tc.wantSessionID)
}
if proto.GetAction() != iop.CancelAction_CANCEL_ACTION_CANCEL_RUN {
t.Errorf("Action: got %v want CANCEL_ACTION_CANCEL_RUN", proto.GetAction())
}
})
}
}
func TestResolveNode_AllowsSingleNodeFallback(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-1"})
svc := edgeservice.New(reg, nil)
if e, err := svc.ResolveNode(""); err != nil || e.NodeID != "node-1" {
t.Errorf("expected single node fallback, got error: %v", err)
}
}
func TestListNodeSnapshotsWithConfig(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alpha"})
store := edgenode.NewNodeStore()
store.Add(&edgenode.NodeRecord{
ID: "node-1",
Alias: "alpha",
Token: "tok-1",
Runtime: config.RuntimeConf{
Concurrency: 3,
WorkspaceRoot: "/workspace",
},
Adapters: config.AdaptersConf{
Ollama: config.OllamaConf{
Enabled: true,
BaseURL: "http://localhost:11434",
ContextSize: 2048,
},
},
})
svc := edgeservice.New(reg, nil)
svc.SetNodeStore(store)
snaps := svc.ListNodeSnapshots()
if len(snaps) != 1 {
t.Fatalf("ListNodeSnapshots: got %d want 1", len(snaps))
}
snap := snaps[0]
if snap.NodeID != "node-1" || snap.Label != "node0" {
t.Errorf("snapshot basic info wrong: %+v", snap)
}
if snap.Config == nil {
t.Fatal("expected snap.Config to be populated")
}
if snap.Config.Runtime == nil || snap.Config.Runtime.Concurrency != 3 {
t.Errorf("unexpected runtime config: %+v", snap.Config.Runtime)
}
var ollama *iop.AdapterConfig
for _, a := range snap.Config.Adapters {
if a.Type == "ollama" {
ollama = a
break
}
}
if ollama == nil {
t.Fatal("expected ollama adapter config")
}
if !ollama.Enabled || ollama.GetOllama().GetBaseUrl() != "http://localhost:11434" {
t.Errorf("unexpected ollama config: %+v", ollama)
}
}
func TestListNodeSnapshotsIncludesAgentKind(t *testing.T) {
reg := edgenode.NewRegistry()
// generic node: kind/lifecycle defaults are applied by Register.
reg.Register(&edgenode.NodeEntry{NodeID: "node-generic"})
// OTO agent: kind and a non-default lifecycle must pass through unchanged.
reg.Register(&edgenode.NodeEntry{
NodeID: "node-oto",
AgentKind: config.AgentKindOTOAgent,
LifecycleState: "registering",
})
svc := edgeservice.New(reg, nil)
byID := map[string]edgeservice.NodeSnapshot{}
for _, s := range svc.ListNodeSnapshots() {
byID[s.NodeID] = s
}
generic, ok := byID["node-generic"]
if !ok {
t.Fatal("node-generic snapshot missing")
}
if generic.AgentKind != config.AgentKindGenericNode {
t.Errorf("generic AgentKind: got %q want %q", generic.AgentKind, config.AgentKindGenericNode)
}
if generic.LifecycleState != edgenode.LifecycleConnected {
t.Errorf("generic LifecycleState: got %q want %q", generic.LifecycleState, edgenode.LifecycleConnected)
}
oto, ok := byID["node-oto"]
if !ok {
t.Fatal("node-oto snapshot missing")
}
if oto.AgentKind != config.AgentKindOTOAgent {
t.Errorf("oto AgentKind: got %q want %q", oto.AgentKind, config.AgentKindOTOAgent)
}
if oto.LifecycleState != "registering" {
t.Errorf("oto LifecycleState: got %q want %q", oto.LifecycleState, "registering")
}
}
func TestResolveNodeSnapshotCarriesAgentKind(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "node-oto",
Alias: "oto-1",
AgentKind: config.AgentKindOTOAgent,
})
svc := edgeservice.New(reg, nil)
snap, err := svc.ResolveNodeSnapshot("oto-1")
if err != nil {
t.Fatalf("ResolveNodeSnapshot: %v", err)
}
if snap.AgentKind != config.AgentKindOTOAgent {
t.Errorf("AgentKind: got %q want %q", snap.AgentKind, config.AgentKindOTOAgent)
}
if snap.LifecycleState != edgenode.LifecycleConnected {
t.Errorf("LifecycleState: got %q want %q", snap.LifecycleState, edgenode.LifecycleConnected)
}
}