package service_test import ( "testing" "time" edgenode "iop/apps/edge/internal/node" edgeservice "iop/apps/edge/internal/service" 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) } }