Files
llmgw/internal/store/toolgrants_test.go
T
Ilker ManapandClaude Opus 5.5 1948a787ae
CI / test (push) Successful in 9m15s
Fixes from the second review: grants, flags, updates, setup
- Revoking a server's tools takes them from every level beneath too, and a
  level left naming no tool stays closed ([]) while any level above or
  beneath names tools: an organization's revoke left its teams the copies
  their first grants took, and going back to no list opened the level to
  what was above it. A closed level is shown on the page and in the
  console with an action to open it (store.OpenTools). A key's owner's
  list is among the levels above a key.
- The flag check matches short flags as written again (-m, -hf, -mu were
  let through), takes a long flag's beginning for it only for the engines
  that parse with argparse (vLLM, SGLang, ComfyUI: --li is --listen), so
  llama.cpp's own --chat-template is not refused, and refuses llama.cpp's
  short spellings (-hfr, -hff, -mm, -jf, …).
- A stored model with an adapter by Hugging Face id can be changed again:
  what it already had is not asked about anew.
- The catalog reads a Hugging Face id as the agent does (engines.IsHubID).
- The page's form cannot move llmgw:admin to another address.
- server init skips the memory check only where the gateway already runs,
  not wherever a half-finished first run left server.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:31:07 +01:00

150 lines
5.1 KiB
Go

package store
import (
"context"
"path/filepath"
"slices"
"testing"
)
func TestGrantingToolsAtALevelKeepsWhatItSawFromAbove(t *testing.T) {
ctx := context.Background()
s, err := OpenSQLite(ctx, filepath.Join(t.TempDir(), "s.db"))
if err != nil {
t.Fatal(err)
}
defer s.Close()
org, _ := s.CreateOrg(ctx, "Acme")
team, _ := s.CreateTeam(ctx, org, "eng")
tools := func(scope Scope, id string) []string {
l, _ := s.LimitsFor(ctx, scope, id)
return l.AllowedTools
}
sees := func(tool string) bool {
e := Effective{}
for _, l := range []struct {
sc Scope
id string
}{{ScopeOrg, org}, {ScopeTeam, team}} {
if ts := tools(l.sc, l.id); ts != nil {
e.ToolGrants = append(e.ToolGrants, ts)
}
}
return e.AllowsTool(tool)
}
// The organization grants hr's tools; the team names none and sees them.
if _, err := s.GrantTools(ctx, ScopeOrg, org, "hr", []string{"hr.*"}); err != nil {
t.Fatal(err)
}
if !sees("hr.search") {
t.Fatal("the team does not see what its organization grants")
}
// Granting jira's to the team keeps hr's, and says the organization
// still keeps jira's out of reach.
g, err := s.GrantTools(ctx, ScopeTeam, team, "jira", []string{"jira.*"})
if err != nil {
t.Fatal(err)
}
if !sees("hr.search") {
t.Errorf("granting jira's tools to the team took hr's away: %v", tools(ScopeTeam, team))
}
if len(g.BlockedBy) != 1 || g.BlockedBy[0].ID != org {
t.Errorf("the organization, which does not grant jira's tools, should be named: %+v", g)
}
if sees("jira.search") {
t.Error("the team sees jira's tools its organization does not grant")
}
// The organization grants them too: now the team sees both.
_, _ = s.GrantTools(ctx, ScopeOrg, org, "jira", []string{"jira.*"})
if !sees("jira.search") || !sees("hr.get") {
t.Errorf("org %v team %v", tools(ScopeOrg, org), tools(ScopeTeam, team))
}
// Taking jira's back from the team leaves it hr's.
if err := s.RevokeTools(ctx, ScopeTeam, team, "jira"); err != nil {
t.Fatal(err)
}
if sees("jira.search") || !sees("hr.get") {
t.Errorf("after the revoke: team %v", tools(ScopeTeam, team))
}
}
func TestRevokingALevelsLastToolsNeitherClosesNorOpensIt(t *testing.T) {
ctx := context.Background()
s, err := OpenSQLite(ctx, filepath.Join(t.TempDir(), "s.db"))
if err != nil {
t.Fatal(err)
}
defer s.Close()
org, _ := s.CreateOrg(ctx, "Acme")
team, _ := s.CreateTeam(ctx, org, "eng")
// Nothing above names tools: the team's last grant taken leaves it
// naming none at all, not closed to every tool.
_, _ = s.GrantTools(ctx, ScopeTeam, team, "jira", []string{"jira.*"})
_ = s.RevokeTools(ctx, ScopeTeam, team, "jira")
if l, _ := s.LimitsFor(ctx, ScopeTeam, team); l.AllowedTools != nil {
t.Errorf("left %v; want no list", l.AllowedTools)
}
// The organization grants every tool; the team was kept to jira's.
// Taking jira's away must not open it to everything above.
_ = s.PutLimits(ctx, Limits{Scope: ScopeOrg, ScopeID: org, AllowedTools: []string{"*"}})
_ = s.PutLimits(ctx, Limits{Scope: ScopeTeam, ScopeID: team, AllowedTools: []string{"jira.*"}})
_ = s.RevokeTools(ctx, ScopeTeam, team, "jira")
if l, _ := s.LimitsFor(ctx, ScopeTeam, team); l.AllowedTools == nil || len(l.AllowedTools) != 0 {
t.Errorf("left %v; want an empty list, closed", l.AllowedTools)
}
if !slices.Equal(withoutServer([]string{"a.x", "b.*", "a.*"}, "a"), []string{"b.*"}) {
t.Error("withoutServer")
}
}
func TestRevokingFromAnOrganizationTakesItFromEverythingBeneath(t *testing.T) {
ctx := context.Background()
s, err := OpenSQLite(ctx, filepath.Join(t.TempDir(), "s.db"))
if err != nil {
t.Fatal(err)
}
defer s.Close()
org, _ := s.CreateOrg(ctx, "Acme")
team, _ := s.CreateTeam(ctx, org, "eng")
tools := func(scope Scope, id string) []string {
l, _ := s.LimitsFor(ctx, scope, id)
return l.AllowedTools
}
sees := func(tool string) bool {
e := Effective{}
for _, ts := range [][]string{tools(ScopeOrg, org), tools(ScopeTeam, team)} {
if ts != nil {
e.ToolGrants = append(e.ToolGrants, ts)
}
}
return e.AllowsTool(tool)
}
_, _ = s.GrantTools(ctx, ScopeOrg, org, "hr", []string{"hr.*"})
_, _ = s.GrantTools(ctx, ScopeTeam, team, "jira", []string{"jira.*"}) // keeps hr.*, copied
// The organization takes hr's back: the team, which held a copy, loses
// them too; and the organization, naming nothing now, is closed rather
// than opened — its team's jira.* was kept out of reach by it before.
if err := s.RevokeTools(ctx, ScopeOrg, org, "hr"); err != nil {
t.Fatal(err)
}
if sees("hr.search") {
t.Errorf("the team kept hr after its organization's revoke: org %v team %v", tools(ScopeOrg, org), tools(ScopeTeam, team))
}
if sees("jira.search") {
t.Errorf("the revoke opened the organization to jira: org %v", tools(ScopeOrg, org))
}
if got := tools(ScopeOrg, org); got == nil || len(got) != 0 {
t.Errorf("the organization is %v; want closed, []", got)
}
// Opened on purpose, it names none and bounds nothing: the team's own
// list decides.
if err := s.OpenTools(ctx, ScopeOrg, org); err != nil {
t.Fatal(err)
}
if tools(ScopeOrg, org) != nil || !sees("jira.search") {
t.Errorf("opened: org %v team %v", tools(ScopeOrg, org), tools(ScopeTeam, team))
}
}