package opsconsole import ( "bytes" "context" "net" "strings" "testing" "time" toki "git.toki-labs.com/toki/proto-socket/go" "google.golang.org/protobuf/proto" edgeevents "iop/apps/edge/internal/events" edgenode "iop/apps/edge/internal/node" edgeservice "iop/apps/edge/internal/service" "iop/packages/go/config" iop "iop/proto/gen/iop" ) func TestParseCommand(t *testing.T) { cases := []struct { name string input string want Command }{ {"empty", "", Command{Kind: CommandEmpty}}, {"whitespace", " ", Command{Kind: CommandEmpty}}, {"slash exit", "/exit", Command{Kind: CommandExit}}, {"slash quit", "/quit", Command{Kind: CommandExit}}, {"bare exit", "exit", Command{Kind: CommandExit}}, {"bare quit upper", "QUIT", Command{Kind: CommandExit}}, {"nodes", "/nodes", Command{Kind: CommandNodes}}, {"terminate", "/terminate-session", Command{Kind: CommandTerminateSession}}, {"status", "/status", Command{Kind: CommandStatus}}, {"capabilities", "/capabilities", Command{Kind: CommandCapabilities}}, {"sessions", "/sessions", Command{Kind: CommandSessionList}}, {"transport", "/transport", Command{Kind: CommandTransportStatus}}, {"node with arg", "/node alias-1", Command{Kind: CommandSetNode, Arg: "alias-1"}}, {"node missing arg", "/node ", Command{Kind: CommandSetNode, UsageErr: "usage: /node "}}, {"session with arg", "/session work-1", Command{Kind: CommandSetSession, Arg: "work-1"}}, {"session missing arg", "/session ", Command{Kind: CommandSetSession, UsageErr: "usage: /session "}}, {"background on", "/background on", Command{Kind: CommandSetBackground, Arg: "on"}}, {"background off", "/background OFF", Command{Kind: CommandSetBackground, Arg: "off"}}, {"background invalid", "/background maybe", Command{Kind: CommandSetBackground, UsageErr: "usage: /background on|off"}}, {"background missing", "/background ", Command{Kind: CommandSetBackground, UsageErr: "usage: /background on|off"}}, {"prompt", "hello world", Command{Kind: CommandPrompt, Arg: "hello world"}}, {"prompt slash unknown is prompt? no it stays prompt", "/unknown stuff", Command{Kind: CommandPrompt, Arg: "/unknown stuff"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := ParseCommand(tc.input) if got != tc.want { t.Fatalf("ParseCommand(%q) = %+v, want %+v", tc.input, got, tc.want) } }) } } func TestConsoleStateSessionBackgroundNode(t *testing.T) { state := &TargetState{ Adapter: "cli", Target: "codex", SessionID: "default", Background: false, TimeoutSec: 30, } apply := func(line string) { t.Helper() cmd := ParseCommand(line) switch cmd.Kind { case CommandSetNode: if cmd.UsageErr != "" { t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr) } state.NodeRef = cmd.Arg case CommandSetSession: if cmd.UsageErr != "" { t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr) } state.SessionID = cmd.Arg case CommandSetBackground: if cmd.UsageErr != "" { t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr) } state.Background = cmd.Arg == "on" default: t.Fatalf("unexpected command kind %v for %q", cmd.Kind, line) } } apply("/node node-7") apply("/session worker-1") apply("/background on") if state.NodeRef != "node-7" { t.Errorf("NodeRef: got %q want node-7", state.NodeRef) } if state.SessionID != "worker-1" { t.Errorf("SessionID: got %q want worker-1", state.SessionID) } if !state.Background { t.Error("Background: expected true") } apply("/background off") if state.Background { t.Error("Background: expected false after /background off") } } func TestNormalizeSessionID(t *testing.T) { if got := NormalizeSessionID(""); got != "default" { t.Errorf("empty: got %q want %q", got, "default") } if got := NormalizeSessionID("my-session"); got != "my-session" { t.Errorf("non-empty: got %q want %q", got, "my-session") } } func TestPrintNodes_ShowsSelectedNode(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"}) reg.Register(&edgenode.NodeEntry{NodeID: "node-2", Alias: "alias-2"}) svc := edgeservice.New(reg, nil) var out bytes.Buffer PrintNodes(&out, svc.ListNodeSnapshots(), "alias-1") got := out.String() if !strings.Contains(got, "* node0 = node-1 (alias-1)") { t.Errorf("expected selected node-1 to have marker, got:\n%s", got) } if !strings.Contains(got, " node1 = node-2 (alias-2)") { t.Errorf("expected non-selected node-2 to have no marker, got:\n%s", got) } } func TestPrintNodes_AcceptsSnapshotDTO(t *testing.T) { var out bytes.Buffer PrintNodes(&out, []edgeservice.NodeSnapshot{ {NodeID: "node-a", Alias: "alpha", Label: "node0"}, }, "alpha") got := out.String() if !strings.Contains(got, "* node0 = node-a (alpha)") { t.Errorf("expected DTO render to mark selected, got:\n%s", got) } } func TestSendRun_SubmitRunRequest_MetadataSource(t *testing.T) { // Create net.Pipe for client-server communication edgeConn, nodeConn := net.Pipe() defer edgeConn.Close() defer nodeConn.Close() parserMap := toki.ParserMap{ toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) { m := &iop.RunRequest{} return m, proto.Unmarshal(b, m) }, } edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap) nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap) reqReceived := make(chan *iop.RunRequest, 1) toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) { reqReceived <- req }) reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{ NodeID: "node-console-test", Alias: "alpha", AgentKind: "node", Client: edgeClient, }) // Enable queue manager in service by passing a new Bus bus := edgeevents.NewBus() svc := edgeservice.New(reg, bus) // Configure NodeStore to fail for queued paths by setting CLI.Enabled = false // while presenting the "codex" profile. This will cause resolveAdapterForNode // to return supported = false if the queued path (which resolves candidate nodes) // is evaluated. store := edgenode.NewNodeStore() store.Add(&edgenode.NodeRecord{ ID: "node-console-test", Adapters: config.AdaptersConf{ CLI: config.CLIConf{ Enabled: false, Profiles: map[string]config.CLIProfileConf{ "codex": {}, }, }, }, }) svc.SetNodeStore(store) target := &TargetState{ NodeRef: "alpha", Adapter: "cli", Target: "codex", SessionID: "sess-1", Background: true, TimeoutSec: 10, } var out bytes.Buffer err := SendRun(context.Background(), svc, nil, &out, target, "test prompt") if err != nil { t.Fatalf("SendRun failed: %v", err) } select { case req := <-reqReceived: if req.GetAdapter() != "cli" || req.GetTarget() != "codex" { t.Errorf("unexpected run request: %+v", req) } if req.GetMetadata()["source"] != "edge-ops-console" { t.Errorf("expected source edge-ops-console, got %q", req.GetMetadata()["source"]) } // If SendRun had set a non-empty ModelGroupKey, it would have routed through the queue. // Since we configured CLI.Enabled = false, resolveQueueCandidates would return an error // ("node ... does not support adapter ..."), causing SendRun to fail. // Successful delivery confirms that SendRun maintains ModelGroupKey == "" (direct path). case <-time.After(2 * time.Second): t.Fatal("timeout waiting for RunRequest at node") } } func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) { view := edgeservice.NodeCommandView{ NodeLabel: "node0", Adapter: "cli@local", Target: "codex", SessionID: "default", Result: map[string]string{ "targets": "v1,v2", "version": "1.0", }, } var out bytes.Buffer FormatNodeCommandView(&out, "capabilities", view) got := out.String() if !strings.Contains(got, "[node0-capabilities] adapter=cli@local target=codex session=default") { t.Errorf("missing header: %q", got) } // keys must appear in sorted order: targets before version tIdx := strings.Index(got, "targets = v1,v2") vIdx := strings.Index(got, "version = 1.0") if tIdx < 0 || vIdx < 0 || tIdx > vIdx { t.Errorf("expected sorted keys, got: %q", got) } } func TestFormatNodeCommandView_EmptyResult(t *testing.T) { view := edgeservice.NodeCommandView{NodeLabel: "alpha", Target: "codex", SessionID: "default"} var out bytes.Buffer FormatNodeCommandView(&out, "sessions", view) if !strings.Contains(out.String(), "no sessions payload returned") { t.Errorf("expected empty-payload notice, got: %q", out.String()) } } func TestFormatNodeCommandView_RendersStructuredSessions(t *testing.T) { view := edgeservice.NodeCommandView{ NodeLabel: "node0", Adapter: "cli@local", Target: "claude", SessionID: "default", Result: map[string]string{ "count": "2", "sessions": "persistent:claude/default,opencode-sse:opencode/work", "session.0.mode": "persistent", "session.0.target": "claude", "session.0.session_id": "default", "session.1.mode": "opencode-sse", "session.1.target": "opencode", "session.1.session_id": "work", }, } var out bytes.Buffer FormatNodeCommandView(&out, "sessions", view) got := out.String() if !strings.Contains(got, "[node0-sessions] adapter=cli@local target=claude session=default") { t.Errorf("missing header: %q", got) } if !strings.Contains(got, "sessions: 2") { t.Errorf("expected count line, got: %q", got) } if !strings.Contains(got, "[0] mode=persistent target=claude session=default") { t.Errorf("expected session 0 group, got: %q", got) } if !strings.Contains(got, "[1] mode=opencode-sse target=opencode session=work") { t.Errorf("expected session 1 group, got: %q", got) } } func TestFormatNodeCommandView_RendersTransportStatusKeys(t *testing.T) { view := edgeservice.NodeCommandView{ NodeLabel: "node0", Adapter: "cli@local", Target: "claude", SessionID: "default", Result: map[string]string{ "connected": "true", "state": "connected", "node_id": "test-node", "session_id": "default", "adapter": "cli", "target": "claude", }, } var out bytes.Buffer FormatNodeCommandView(&out, "transport", view) got := out.String() if !strings.Contains(got, "[node0-transport] adapter=cli@local target=claude session=default") { t.Errorf("missing header: %q", got) } for _, want := range []string{"connected = true", "state = connected", "node_id = test-node", "session_id = default"} { if !strings.Contains(got, want) { t.Errorf("expected %q in transport output, got:\n%s", want, got) } } } func TestHandleTerminateSession_OutputFormat(t *testing.T) { oldFunc := SendTerminateSessionFunc defer func() { SendTerminateSessionFunc = oldFunc }() SendTerminateSessionFunc = func(ctx context.Context, edgeSvc *edgeservice.Service, target *TargetState) (string, error) { return "mock-alias", nil } target := &TargetState{SessionID: "s-test"} var out bytes.Buffer HandleTerminateSession(context.Background(), nil, &out, target) got := out.String() want := "terminated session s-test node=mock-alias\n" if got != want { t.Errorf("OutputFormat (alias): got %q, want %q", got, want) } SendTerminateSessionFunc = func(ctx context.Context, edgeSvc *edgeservice.Service, target *TargetState) (string, error) { return "node-raw-id", nil } out.Reset() HandleTerminateSession(context.Background(), nil, &out, target) got2 := out.String() want2 := "terminated session s-test node=node-raw-id\n" if got2 != want2 { t.Errorf("OutputFormat (node_id): got %q, want %q", got2, want2) } }