From 3c4e8e8bb830491cc63e0f3277e4f6e63aae8a7c Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Sun, 3 May 2026 21:27:33 +0200 Subject: [PATCH] feat(hyperguild): tier subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the tier subcommand to the hyperguild CLI. Reuses internal/tier.Detect verbatim, sources probe URLs from ANTHROPIC_PROBE_URL (default https://api.anthropic.com) and LITELLM_BASE_URL (no default — empty triggers airplane). Human-readable output by default; --json emits the same Info struct as the supervisor's tier MCP returns. Tests cover all three tier states via httptest fakes. --- cmd/hyperguild/main.go | 2 +- cmd/hyperguild/main_test.go | 4 +- cmd/hyperguild/tier.go | 42 ++++++++++++++++++++ cmd/hyperguild/tier_test.go | 77 +++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 cmd/hyperguild/tier.go create mode 100644 cmd/hyperguild/tier_test.go diff --git a/cmd/hyperguild/main.go b/cmd/hyperguild/main.go index 222b7cf..cca51f4 100644 --- a/cmd/hyperguild/main.go +++ b/cmd/hyperguild/main.go @@ -25,7 +25,7 @@ func notYet(ctx context.Context, args []string, stdin io.Reader, stdout, stderr func subcommands() map[string]subcommand { return map[string]subcommand{ - "tier": notYet, + "tier": runTier, "brain": notYet, "mode": notYet, } diff --git a/cmd/hyperguild/main_test.go b/cmd/hyperguild/main_test.go index 7c91bd0..6959f20 100644 --- a/cmd/hyperguild/main_test.go +++ b/cmd/hyperguild/main_test.go @@ -35,8 +35,8 @@ func TestDispatch_UnknownSubcommand_ReturnsTwo(t *testing.T) { func TestDispatch_KnownSubcommand_RoutesAndReturnsExitCode(t *testing.T) { var out, errBuf bytes.Buffer - code := dispatch(context.Background(), []string{"tier"}, strings.NewReader(""), &out, &errBuf) - // At Task 1 time, tier returns the not-implemented error → exit 1. + code := dispatch(context.Background(), []string{"brain"}, strings.NewReader(""), &out, &errBuf) + // At this point, brain still returns the not-implemented error → exit 1. assert.Equal(t, 1, code) assert.Contains(t, errBuf.String(), "not implemented") } diff --git a/cmd/hyperguild/tier.go b/cmd/hyperguild/tier.go new file mode 100644 index 0000000..a18f573 --- /dev/null +++ b/cmd/hyperguild/tier.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "os" + + "github.com/mathiasbq/supervisor/internal/tier" +) + +const defaultAnthropicProbe = "https://api.anthropic.com" + +func runTier(ctx context.Context, args []string, _ io.Reader, stdout, stderr io.Writer) error { + fs := flag.NewFlagSet("tier", flag.ContinueOnError) + fs.SetOutput(stderr) + asJSON := fs.Bool("json", false, "output JSON instead of human-readable") + if err := fs.Parse(args); err != nil { + return fmt.Errorf("parse flags: %w", err) + } + + anthropicURL := os.Getenv("ANTHROPIC_PROBE_URL") + if anthropicURL == "" { + anthropicURL = defaultAnthropicProbe + } + liteLLMURL := os.Getenv("LITELLM_BASE_URL") // empty → tier falls through to airplane + + info := tier.Detect(ctx, anthropicURL, liteLLMURL) + + if *asJSON { + enc := json.NewEncoder(stdout) + enc.SetIndent("", " ") + if err := enc.Encode(info); err != nil { + return fmt.Errorf("encode json: %w", err) + } + return nil + } + fmt.Fprintf(stdout, "tier %d (%s) managed_agents=%t\n", int(info.Tier), info.Label, info.ManagedAgents) //nolint:errcheck + return nil +} diff --git a/cmd/hyperguild/tier_test.go b/cmd/hyperguild/tier_test.go new file mode 100644 index 0000000..cd9e509 --- /dev/null +++ b/cmd/hyperguild/tier_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func okServer(t *testing.T) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) +} + +func TestRunTier_Full_Human(t *testing.T) { + anthropic := okServer(t) + defer anthropic.Close() + litellm := okServer(t) + defer litellm.Close() + + t.Setenv("ANTHROPIC_PROBE_URL", anthropic.URL) + t.Setenv("LITELLM_BASE_URL", litellm.URL) + + var out, errBuf bytes.Buffer + err := runTier(context.Background(), []string{}, strings.NewReader(""), &out, &errBuf) + require.NoError(t, err) + assert.Contains(t, out.String(), "tier 1") + assert.Contains(t, out.String(), "full-online") + assert.Contains(t, out.String(), "managed_agents=true") +} + +func TestRunTier_LANOnly_JSON(t *testing.T) { + litellm := okServer(t) + defer litellm.Close() + + t.Setenv("ANTHROPIC_PROBE_URL", "http://127.0.0.1:1") // unreachable + t.Setenv("LITELLM_BASE_URL", litellm.URL) + + var out, errBuf bytes.Buffer + err := runTier(context.Background(), []string{"--json"}, strings.NewReader(""), &out, &errBuf) + require.NoError(t, err) + + var got struct { + Tier int `json:"tier"` + Label string `json:"label"` + ManagedAgents bool `json:"managed_agents"` + } + require.NoError(t, json.Unmarshal(out.Bytes(), &got)) + assert.Equal(t, 2, got.Tier) + assert.Equal(t, "lan-only", got.Label) + assert.False(t, got.ManagedAgents) +} + +func TestRunTier_Airplane_NoLiteLLMBaseURL(t *testing.T) { + t.Setenv("ANTHROPIC_PROBE_URL", "http://127.0.0.1:1") + t.Setenv("LITELLM_BASE_URL", "") + + var out, errBuf bytes.Buffer + err := runTier(context.Background(), []string{}, strings.NewReader(""), &out, &errBuf) + require.NoError(t, err) + assert.Contains(t, out.String(), "tier 3") + assert.Contains(t, out.String(), "airplane") +} + +func TestRunTier_UnknownFlag_ReturnsError(t *testing.T) { + var out, errBuf bytes.Buffer + err := runTier(context.Background(), []string{"--bogus"}, strings.NewReader(""), &out, &errBuf) + assert.Error(t, err) +}