package cli import ( "bytes" "context" "path/filepath" "strings" "testing" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) // run executes the CLI against one store file and returns stdout. func run(t *testing.T, db string, args ...string) (string, error) { t.Helper() cmd := Root() var out, errOut bytes.Buffer cmd.SetOut(&out) cmd.SetErr(&errOut) // --config pointed nowhere, so a server.yaml on the test machine is never read. cmd.SetArgs(append(args, "--store", db, "--config", "")) err := cmd.ExecuteContext(context.Background()) return out.String(), err } func mustRun(t *testing.T, db string, args ...string) string { t.Helper() out, err := run(t, db, args...) if err != nil { t.Fatalf("%v: %v", args, err) } return out } func TestTheTreeIsManagedByNameNotId(t *testing.T) { db := filepath.Join(t.TempDir(), "s.db") mustRun(t, db, "org", "add", "org", "Acme") mustRun(t, db, "org", "add", "org", "Scania") mustRun(t, db, "org", "add", "team", "platform", "--in", "Acme") mustRun(t, db, "org", "add", "team", "platform", "--in", "Scania") // "platform" is in both, so a project needs the path. _, err := run(t, db, "org", "add", "project", "gateway", "--in", "platform") if err == nil || !strings.Contains(err.Error(), "more than one team") { t.Fatalf("an ambiguous name was accepted: %v", err) } mustRun(t, db, "org", "add", "project", "gateway", "--in", "acme/Platform") mustRun(t, db, "org", "add", "project", "research", "--in", "Scania / platform") mustRun(t, db, "users", "create", "ayse@example.com", "--password", "correct horse battery", "--project", "Acme/platform/gateway") mustRun(t, db, "keys", "create", "laptop", "--project", "gateway") out := mustRun(t, db, "org", "tree", "--people") for _, want := range []string{"Acme", "gateway", "1 member, 1 key", "ayse@example.com"} { if !strings.Contains(out, want) { t.Errorf("tree lacks %q:\n%s", want, out) } } out = mustRun(t, db, "users", "join", "ayse@example.com", "--project", "research") if !strings.Contains(out, "Scania / platform / research") { t.Errorf("join said: %s", out) } mustRun(t, db, "users", "leave", "ayse@example.com", "--project", "Acme/platform/gateway") // She now uses the gateway through research alone; a personal key is hers. mustRun(t, db, "keys", "create", "mine", "--for", "ayse@example.com") if out := mustRun(t, db, "keys", "list", "--all"); !strings.Contains(out, "personal") { t.Errorf("a personal key is not listed as one:\n%s", out) } if _, err := run(t, db, "keys", "create", "x", "--for", "ayse@example.com", "--project", "gateway"); err == nil { t.Error("a key was made for her in a project she left") } mustRun(t, db, "org", "rename", "project", "research", "lab") mustRun(t, db, "org", "move", "project", "Acme/platform/gateway", "--to", "Scania/platform") // gateway still has the operator's key, so it stays. if _, err := run(t, db, "org", "remove", "project", "gateway"); err == nil { t.Error("a project with a key was removed") } mustRun(t, db, "org", "remove", "team", "Acme/platform") mustRun(t, db, "org", "remove", "org", "Acme") s, err := store.Open(context.Background(), db) if err != nil { t.Fatal(err) } defer s.Close() paths, _ := s.AllProjects(context.Background()) var got []string for _, p := range paths { got = append(got, p.Path) } if strings.Join(got, "|") != "Scania / platform / gateway|Scania / platform / lab" { t.Errorf("projects = %v", got) } } func TestOneProjectNeedsNoFlag(t *testing.T) { db := filepath.Join(t.TempDir(), "s.db") mustRun(t, db, "org", "add", "org", "Acme") mustRun(t, db, "org", "add", "team", "platform") mustRun(t, db, "org", "add", "project", "gateway") mustRun(t, db, "keys", "create", "laptop") if out := mustRun(t, db, "keys", "list"); !strings.Contains(out, "laptop") { t.Errorf("list: %s", out) } mustRun(t, db, "org", "add", "project", "search", "--in", "platform") if _, err := run(t, db, "keys", "create", "other"); err == nil || !strings.Contains(err.Error(), "which project?") { t.Errorf("two projects and no flag: %v", err) } } func TestKeyBudgetsAreWholeUnits(t *testing.T) { db := filepath.Join(t.TempDir(), "s.db") mustRun(t, db, "org", "add", "org", "Acme") mustRun(t, db, "org", "add", "team", "platform") mustRun(t, db, "org", "add", "project", "gateway") mustRun(t, db, "keys", "limits", "--scope", "project", "--id", "gateway", "--budget", "12.5") s, err := store.Open(context.Background(), db) if err != nil { t.Fatal(err) } defer s.Close() all, _ := s.AllLimits(context.Background()) for _, l := range all { if l.Budget == nil || *l.Budget != 12_500_000 { t.Errorf("budget stored as %v micro-units", l.Budget) } } } // A limit given alone changes that limit alone: granting a tool keeps the // scope's rate. --replace writes the row whole, as the console's form does. func TestALimitGivenAloneLeavesTheOthersAtTheScope(t *testing.T) { db := filepath.Join(t.TempDir(), "s.db") mustRun(t, db, "org", "add", "org", "Acme") mustRun(t, db, "org", "add", "team", "platform") mustRun(t, db, "org", "add", "project", "gateway") mustRun(t, db, "keys", "limits", "--scope", "project", "--id", "gateway", "--rpm", "30") mustRun(t, db, "keys", "limits", "--scope", "project", "--id", "gateway", "--tools", "jira.*") read := func() store.Limits { s, err := store.Open(context.Background(), db) if err != nil { t.Fatal(err) } defer s.Close() all, _ := s.AllLimits(context.Background()) for _, l := range all { return l } return store.Limits{} } if l := read(); l.RPM == nil || *l.RPM != 30 || len(l.AllowedTools) != 1 { t.Errorf("after a tool grant: rpm %v, tools %v", l.RPM, l.AllowedTools) } mustRun(t, db, "keys", "limits", "--replace", "--scope", "project", "--id", "gateway", "--tpm", "1000") if l := read(); l.RPM != nil || l.AllowedTools != nil || l.TPM == nil { t.Errorf("after --replace: %+v", l) } }