package cli import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strings" "testing" "time" "github.com/spf13/cobra" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" ) func TestTheServicesHereAreFoundAndAskedAbout(t *testing.T) { old, oldRun := unitDir, serviceRunner unitDir = t.TempDir() t.Cleanup(func() { unitDir, serviceRunner = old, oldRun }) for _, u := range []string{"llmgw-server", "llmgw-agent"} { _ = os.WriteFile(filepath.Join(unitDir, u+".service"), []byte("[Unit]"), 0o644) } var asked []string serviceRunner = func(_ context.Context, args ...string) (string, error) { asked = append(asked, strings.Join(args, " ")) if args[0] == "is-active" { return "active", nil } return "Wed 2026-10-07 12:00:00 UTC", nil } units, err := installedServices(context.Background(), nil) if err != nil || strings.Join(units, ",") != "llmgw-server,llmgw-agent" { t.Fatalf("%v %v", units, err) } if _, err := installedServices(context.Background(), []string{"mirror"}); err == nil || !strings.Contains(err.Error(), "server, agent") { t.Errorf("a service not here: %v", err) } if u, err := installedServices(context.Background(), []string{"agent"}); err != nil || len(u) != 1 { t.Errorf("one named: %v %v", u, err) } if runtime.GOOS != "linux" { return } var out bytes.Buffer c := serviceVerbs()[0] c.SetOut(&out) c.SetContext(context.Background()) if err := c.RunE(c, nil); err != nil { t.Fatal(err) } if !strings.Contains(out.String(), "server") || !strings.Contains(out.String(), "active") { t.Errorf("%s", out.String()) } } func TestStopSetsTheClustersServingStopped(t *testing.T) { var put adminapi.SettingsRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/v1/summary": _ = json.NewEncoder(w).Encode(adminapi.Summary{}) case r.Method == "GET" && r.URL.Path == "/api/v1/settings": _ = json.NewEncoder(w).Encode(adminapi.SettingsView{Version: 3, Body: "router:\n retries: 2\n"}) case r.Method == "PUT" && r.URL.Path == "/api/v1/settings": _ = json.NewDecoder(r.Body).Decode(&put) _ = json.NewEncoder(w).Encode(adminapi.SettingsView{Version: 4, Body: put.Body}) default: http.NotFound(w, r) } })) defer srv.Close() dir := t.TempDir() cfg := filepath.Join(dir, "server.yaml") _ = os.WriteFile(filepath.Join(dir, "token"), []byte("t"), 0o600) _ = os.WriteFile(cfg, []byte("listen: \":8080\"\nagent_insecure: true\nadmin_listen: "+ strings.TrimPrefix(srv.URL, "http://")+"\nadmin_remote: true\nadmin_token_file: "+filepath.Join(dir, "token")+"\n"), 0o600) var stop *cobra.Command for _, c := range newSystemCmds() { if c.Name() == "stop" { stop = c } } var out bytes.Buffer stop.SetOut(&out) stop.SetArgs([]string{"--config", cfg, "--note", "maintenance"}) if err := stop.ExecuteContext(context.Background()); err != nil { t.Fatalf("%v: %s", err, out.String()) } if put.Base != 3 || !strings.Contains(put.Body, "stopped: true") || !strings.Contains(put.Body, "retries: 2") || put.Note != "maintenance" { t.Errorf("sent %+v", put) } if !strings.Contains(out.String(), "stopping") { t.Errorf("%s", out.String()) } } // Started while the control plane has not yet taken the setting up: every // model of a stopped system has the nothing it asks for, which is not // serving. func TestStartWaitsForTheControlPlaneToServe(t *testing.T) { old := servicePoll servicePoll = time.Millisecond t.Cleanup(func() { servicePoll = old }) answers := []adminapi.Summary{ {Stopped: true, Models: adminapi.ModelTotals{Total: 2, Satisfied: 2}}, {Models: adminapi.ModelTotals{Total: 2, Satisfied: 0}}, {Models: adminapi.ModelTotals{Total: 2, Satisfied: 2}}, } asked := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(answers[min(asked, len(answers)-1)]) asked++ })) defer srv.Close() var out bytes.Buffer cmd := &cobra.Command{} cmd.SetOut(&out) cmd.SetContext(context.Background()) c := &ctlClient{base: srv.URL, http: srv.Client()} if err := waitForService(cmd, func() *ctlClient { return c }, false); err != nil { t.Fatal(err) } if asked != 3 { t.Errorf("returned after %d answers: %s", asked, out.String()) } }