Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions go/cmd/compass/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//go:build unix

package main

import (
"context"
"fmt"
"io"
"strings"

"connectrpc.com/connect"
"github.com/spf13/cobra"

compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1"
"github.com/RigelBuild/compass/go/gen/compass/v1/compassv1connect"
)

// newAgentCmd builds the agent noun: the agent-session inspection operator
// surface. It carries no logic of its own; each verb is a child that dials the
// Server and drives one CompassService RPC. It is distinct from the agent-config
// noun (fleet config bundles) — a separate Cobra command, no name clash.
func newAgentCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "agent",
Short: "Inspect agent sessions (status)",
}
cmd.AddCommand(newAgentStatusCmd())
return cmd
}

// newAgentStatusCmd builds `agent status [--session <id>]`: GetAgentStatus and
// render each session's id and state. An empty --session returns every live
// session; a set --session returns just that one (queryable even when terminal,
// per the by-id contract at internal/server/service.go:343-358).
func newAgentStatusCmd() *cobra.Command {
var session string
cmd := &cobra.Command{
Use: "status",
Short: "Show live agent-session state (all sessions, or one with --session)",
RunE: func(cmd *cobra.Command, _ []string) error {
client, err := dialClient(cmd)
if err != nil {
return err
}
return runAgentStatus(cmd.Context(), client, session, cmd.OutOrStdout())
},
}
cmd.Flags().StringVar(&session, "session", "",
"Restrict to one session id (default: every live session).")
return cmd
}

// runAgentStatus calls GetAgentStatus with the resolved session filter and
// renders the returned statuses. An empty result renders a clear message, not an
// error — no live sessions (or an unknown id) is a valid answer, not a failure.
func runAgentStatus(ctx context.Context, client compassv1connect.CompassServiceClient, session string, out io.Writer) error {
ctx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := client.GetAgentStatus(ctx,
connect.NewRequest(&compassv1.GetAgentStatusRequest{SessionId: session}))
if err != nil {
return fmt.Errorf("getting agent status: %w", err)
}
statuses := resp.Msg.GetStatuses()
if len(statuses) == 0 {
_, err = fmt.Fprintln(out, "no live agent sessions")
return err
}
return renderAgentStatuses(out, statuses)
}

// renderAgentStatuses prints a session-id + state column for each status. The
// state renders as the short operator-facing token (the enum name minus the
// AGENT_SESSION_STATE_ prefix, lowercased), so a WORKING session reads "working".
func renderAgentStatuses(out io.Writer, statuses []*compassv1.AgentSessionStatus) error {
if _, err := fmt.Fprintf(out, "%-40s %s\n", "SESSION", "STATE"); err != nil {
return err
}
for _, s := range statuses {
if _, err := fmt.Fprintf(out, "%-40s %s\n", s.GetSessionId(), stateLabel(s.GetState())); err != nil {
return err
}
}
return nil
}

// stateLabel renders an AgentSessionState as the short operator-facing token:
// the generated enum name with the AGENT_SESSION_STATE_ prefix stripped and
// lowercased (WORKING → "working"). An unknown value falls back to "unspecified".
func stateLabel(state compassv1.AgentSessionState) string {
name, ok := compassv1.AgentSessionState_name[int32(state)]
if !ok {
return unspecifiedLabel
}
return strings.ToLower(strings.TrimPrefix(name, "AGENT_SESSION_STATE_"))
}
117 changes: 117 additions & 0 deletions go/cmd/compass/agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//go:build unix

package main

import (
"context"
"strings"
"testing"

"github.com/spf13/cobra"

compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1"
)

// The agent subcommand drives GetAgentStatus against the shared fakeCompass /
// startFakeServer harness in cli_test.go (its statuses/gotStatus fields record
// the request and canned response), so the wiring is tested without a live
// Server or Postgres.

// TestRunAgentStatusAll asserts an empty --session sends an empty session id
// (every live session) and renders each returned session's id and short state
// label, stamping the bearer token.
func TestRunAgentStatusAll(t *testing.T) {
fake := &fakeCompass{statuses: []*compassv1.AgentSessionStatus{
{SessionId: "s1", State: compassv1.AgentSessionState_AGENT_SESSION_STATE_WORKING},
{SessionId: "s2", State: compassv1.AgentSessionState_AGENT_SESSION_STATE_DISCONNECTED},
}}
client := startFakeServer(t, fake)

var out strings.Builder
if err := runAgentStatus(context.Background(), client, "", &out); err != nil {
t.Fatalf("runAgentStatus: %v", err)
}
if fake.gotStatus.GetSessionId() != "" {
t.Errorf("GetAgentStatus session_id = %q, want empty (all sessions)", fake.gotStatus.GetSessionId())
}
got := out.String()
for _, want := range []string{"s1", "working", "s2", "disconnected", "SESSION", "STATE"} {
if !strings.Contains(got, want) {
t.Errorf("status output %q missing %q", got, want)
}
}
if fake.gotAuth != "Bearer test-token" {
t.Errorf("Authorization = %q, want Bearer test-token", fake.gotAuth)
}
}

// TestRunAgentStatusByID asserts a set --session maps straight onto the request
// session_id (the by-id filter) and renders that one session.
func TestRunAgentStatusByID(t *testing.T) {
fake := &fakeCompass{statuses: []*compassv1.AgentSessionStatus{
{SessionId: "s1", State: compassv1.AgentSessionState_AGENT_SESSION_STATE_STOPPED},
}}
client := startFakeServer(t, fake)

var out strings.Builder
if err := runAgentStatus(context.Background(), client, "s1", &out); err != nil {
t.Fatalf("runAgentStatus: %v", err)
}
if fake.gotStatus.GetSessionId() != "s1" {
t.Errorf("GetAgentStatus session_id = %q, want s1", fake.gotStatus.GetSessionId())
}
if got := out.String(); !strings.Contains(got, "s1") || !strings.Contains(got, "stopped") {
t.Errorf("status output %q missing s1/stopped", got)
}
}

// TestRunAgentStatusEmpty asserts an empty result renders a clear message, not
// an error — no live sessions (or an unknown id) is a valid answer.
func TestRunAgentStatusEmpty(t *testing.T) {
fake := &fakeCompass{}
client := startFakeServer(t, fake)

var out strings.Builder
if err := runAgentStatus(context.Background(), client, "", &out); err != nil {
t.Fatalf("runAgentStatus: %v", err)
}
if got := out.String(); !strings.Contains(got, "no live agent sessions") {
t.Errorf("empty output %q, want the no-sessions message", got)
}
}

// TestAgentStatusFlagParsing asserts the status verb parses --session off argv
// into the request (flag wiring, not just the run function).
func TestAgentStatusFlagParsing(t *testing.T) {
fake := &fakeCompass{}
client := startFakeServer(t, fake)

cmd := newAgentStatusCmd()
cmd.RunE = func(cmd *cobra.Command, _ []string) error {
return runAgentStatus(cmd.Context(), client, cmd.Flag("session").Value.String(), cmd.OutOrStdout())
}
cmd.SetArgs([]string{"--session", "abc"})
cmd.SetOut(&strings.Builder{})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute: %v", err)
}
if fake.gotStatus.GetSessionId() != "abc" {
t.Errorf("parsed session_id = %q, want abc", fake.gotStatus.GetSessionId())
}
}

// TestStateLabel asserts the enum-name → short-token rendering, including the
// unspecified fallback for an out-of-range value.
func TestStateLabel(t *testing.T) {
cases := map[compassv1.AgentSessionState]string{
compassv1.AgentSessionState_AGENT_SESSION_STATE_WORKING: "working",
compassv1.AgentSessionState_AGENT_SESSION_STATE_READY: "ready",
compassv1.AgentSessionState_AGENT_SESSION_STATE_DISCONNECTED: "disconnected",
compassv1.AgentSessionState(9999): "unspecified",
}
for state, want := range cases {
if got := stateLabel(state); got != want {
t.Errorf("stateLabel(%v) = %q, want %q", state, got, want)
}
}
}
8 changes: 8 additions & 0 deletions go/cmd/compass/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ type fakeCompass struct {
info *compassv1.GetAgentConfigInfoResponse
deleteCalls int
gotToken string
gotStatus *compassv1.GetAgentStatusRequest
statuses []*compassv1.AgentSessionStatus
gotAuth string
}

Expand Down Expand Up @@ -290,6 +292,12 @@ func (f *fakeCompass) RevokeToken(_ context.Context, req *connect.Request[compas
return connect.NewResponse(&compassv1.RevokeTokenResponse{}), nil
}

func (f *fakeCompass) GetAgentStatus(_ context.Context, req *connect.Request[compassv1.GetAgentStatusRequest]) (*connect.Response[compassv1.GetAgentStatusResponse], error) {
f.gotStatus = req.Msg
f.gotAuth = req.Header().Get("Authorization")
return connect.NewResponse(&compassv1.GetAgentStatusResponse{Statuses: f.statuses}), nil
}

// startFakeServer stands up the fake CompassService over a plain-HTTP httptest
// server and returns a client wired to it with the bearer interceptor.
func startFakeServer(t *testing.T, fake *fakeCompass) compassv1connect.CompassServiceClient {
Expand Down
23 changes: 23 additions & 0 deletions go/cmd/compass/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,26 @@ func dialSecretsClient(cmd *cobra.Command) (compassv1connect.SecretsServiceClien
}
return newSecretsClient(cfg)
}

// newCommsClient constructs the CommsService client for the resolved
// connection, reusing the same httpClient + bearer interceptor as newClient.
func newCommsClient(cfg connConfig) (compassv1connect.CommsServiceClient, error) {
httpClient, err := httpClientFor(cfg)
if err != nil {
return nil, err
}
return compassv1connect.NewCommsServiceClient(
httpClient, cfg.serverAddr,
connect.WithInterceptors(&bearerToken{token: cfg.token}),
), nil
}

// dialCommsClient resolves the connection config and builds the CommsService
// client in one step — the shared prelude every message subcommand runs.
func dialCommsClient(cmd *cobra.Command) (compassv1connect.CommsServiceClient, error) {
cfg, err := resolveConn(cmd)
if err != nil {
return nil, err
}
return newCommsClient(cfg)
}
7 changes: 7 additions & 0 deletions go/cmd/compass/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import (
// "-X main.version=<v>".
var version = "0.1.0"

// unspecifiedLabel is the operator-facing token for an enum value the CLI does
// not recognize — the zero/unknown fallback shared by every enum renderer
// (deliveryLabel, kindLabel, stateLabel), so an unknown value reads uniformly.
const unspecifiedLabel = "unspecified"

func main() {
if err := newRootCmd().Execute(); err != nil {
fmt.Fprintln(os.Stderr, "compass:", err)
Expand All @@ -45,7 +50,9 @@ func newRootCmd() *cobra.Command {
}
addConnFlags(root.PersistentFlags())

root.AddCommand(newAgentCmd())
root.AddCommand(newAgentConfigCmd())
root.AddCommand(newMessageCmd())
root.AddCommand(newSecretCmd())
root.AddCommand(newTokenCmd())
return root
Expand Down
Loading
Loading