CI / test (push) Successful in 5m25s
Setting one limit at a scope wrote the whole row, so a tool grant given alone cleared the scope's rate, budget and models. --replace writes it whole, as the console's form, which shows every field, now asks for. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
164 lines
5.8 KiB
Go
164 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|