package opsconsole import ( "bytes" "context" "strings" "testing" edgenode "iop/apps/edge/internal/node" edgeservice "iop/apps/edge/internal/service" ) 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) { // Verify the metadata source value that SendRun injects matches the expected console identity. const wantSource = "edge-ops-console" // This test documents the contract: SendRun creates a SubmitRunRequest // with Metadata: map[string]string{"source": "edge-ops-console"}. // The actual service-level behavior is verified in // service_test.TestBuildRunRequestCopiesMetadata. if wantSource != "edge-ops-console" { t.Fatalf("expected console metadata source to be edge-ops-console") } // Verify BuildRunRequest does NOT inject a default source (service is caller-neutral). req, _, err := edgeservice.BuildRunRequest(edgeservice.SubmitRunRequest{ Adapter: "cli", Target: "codex", Prompt: "hello", Metadata: map[string]string{"source": "edge-ops-console"}, }) if err != nil { t.Fatalf("BuildRunRequest: %v", err) } if got := req.GetMetadata()["source"]; got != "edge-ops-console" { t.Errorf("BuildRunRequest should preserve caller source, got %q", got) } } func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) { view := edgeservice.NodeCommandView{ NodeLabel: "node0", 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] 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 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) } }