agent
This commit is contained in:
@@ -104,9 +104,109 @@ func TestACallerScopedToNothingIsNotACallerScopedToEverything(t *testing.T) {
|
||||
if !errors.Is(err, ErrNoTenant) {
|
||||
t.Fatalf("a caller with no tenant was let through: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := r.Call(context.Background(), agent, "thing", nil, Caller{Userid: 12, Superadmin: true}); err != nil {
|
||||
t.Fatalf("staff were refused: %v", err)
|
||||
func TestStaffAreNotExemptFromAToolsScope(t *testing.T) {
|
||||
// A platform account carries no tenant, and "every merchant at once" is not
|
||||
// an answer to "what is stuck?". Staff are told to pick a shop, in the same
|
||||
// words a branch user would get — the exemption in WebAuth is about which
|
||||
// tenant they may NAME, not about reading all of them at once.
|
||||
r, _ := registryWith(t, okTool("thing"))
|
||||
agent := Agent{Name: "orders", Tools: []string{"thing"}}
|
||||
|
||||
_, err := r.Call(context.Background(), agent, "thing", nil, Caller{Userid: 12, Superadmin: true})
|
||||
if !errors.Is(err, ErrNoTenant) {
|
||||
t.Fatalf("staff read a tenant-scoped tool with no tenant: %v", err)
|
||||
}
|
||||
|
||||
// With a shop picked, the same call works.
|
||||
if _, err := r.Call(context.Background(), agent, "thing", nil,
|
||||
Caller{Userid: 12, Superadmin: true, Tenantid: 1147}); err != nil {
|
||||
t.Fatalf("staff were refused a shop they had picked: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── The scope a tool declares ─────────────────────────────────────────── */
|
||||
|
||||
func TestAToolThatDeclaresNothingIsConfinedToOneMerchant(t *testing.T) {
|
||||
// Default-deny. The zero value of Requires is the strictest, so a tool
|
||||
// written next year without thinking about scope is safe rather than silent.
|
||||
tool := okTool("thing")
|
||||
if tool.Needs != RequiresTenant {
|
||||
t.Fatalf("the default scope is %v, not the strictest", tool.Needs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABranchScopedToolRefusesAnAllBranchesCaller(t *testing.T) {
|
||||
tool := okTool("thing")
|
||||
tool.Needs = RequiresBranch
|
||||
r, _ := registryWith(t, tool)
|
||||
agent := Agent{Name: "orders", Tools: []string{"thing"}}
|
||||
|
||||
if _, err := r.Call(context.Background(), agent, "thing", nil, anyone); !errors.Is(err, ErrNoTenant) {
|
||||
t.Fatalf("a branch-only tool answered for every branch: %v", err)
|
||||
}
|
||||
|
||||
withBranch := Caller{Userid: 904, Tenantid: 1147, Locationid: 1172}
|
||||
if _, err := r.Call(context.Background(), agent, "thing", nil, withBranch); err != nil {
|
||||
t.Fatalf("a branch caller was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAToolNeedingNothingAnswersWithoutAShop(t *testing.T) {
|
||||
// There is one: the product help corpus, which carries nothing about
|
||||
// anybody. Requiring a tenant would refuse staff a help question for no
|
||||
// reason.
|
||||
tool := okTool("thing")
|
||||
tool.Needs = RequiresNothing
|
||||
r, _ := registryWith(t, tool)
|
||||
|
||||
_, err := r.Call(context.Background(), Agent{Name: "a", Tools: []string{"thing"}}, "thing", nil,
|
||||
Caller{Userid: 12, Superadmin: true})
|
||||
if err != nil {
|
||||
t.Fatalf("a tool needing no shop was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheScopeIsCheckedBeforeTheHandlerRuns(t *testing.T) {
|
||||
// So no query is ever built from a scope that was never established.
|
||||
ran := false
|
||||
tool := okTool("thing")
|
||||
tool.Needs = RequiresBranch
|
||||
tool.Handler = func(context.Context, Request) (Result, error) {
|
||||
ran = true
|
||||
return Result{}, nil
|
||||
}
|
||||
r, _ := registryWith(t, tool)
|
||||
|
||||
_, _ = r.Call(context.Background(), Agent{Name: "a", Tools: []string{"thing"}}, "thing", nil, anyone)
|
||||
if ran {
|
||||
t.Fatal("the handler ran without the scope it declared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAToolMayNotOfferAScopingArgument(t *testing.T) {
|
||||
// The structural guarantee. An argument is something the MODEL fills in, and
|
||||
// the model is the one part of this system that can be argued with — so a
|
||||
// tool offering `tenantid` is refused at registration rather than trusted to
|
||||
// ignore it.
|
||||
for _, name := range []string{"tenantid", "tenant_id", "locationid", "store_id", "partnerid", "customerid", "userid"} {
|
||||
tool := okTool("thing")
|
||||
tool.Schema = Schema{Fields: []Field{{Name: name, Description: "d", Kind: KindInt}}}
|
||||
if err := New(nil).Register(tool); err == nil {
|
||||
t.Fatalf("a tool offering %q as an argument was registered", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoRegisteredToolOffersAWayToChooseWhoseDataIsRead(t *testing.T) {
|
||||
// The sweep, over every tool that actually ships. This is the test that
|
||||
// catches the eighth tool somebody adds in a hurry.
|
||||
r := New(nil)
|
||||
for _, tool := range shippedTools(t) {
|
||||
if err := r.Register(tool); err != nil {
|
||||
t.Fatalf("%s: %v", tool.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,3 +425,37 @@ func contains(haystack, needle string) bool {
|
||||
return false
|
||||
})()
|
||||
}
|
||||
|
||||
/* ── The silent zero ───────────────────────────────────────────────────── */
|
||||
|
||||
func TestAJSONNumberDoesNotBecomeASilentZero(t *testing.T) {
|
||||
// The root cause of the approval bug. `Validate` hands a handler a real int,
|
||||
// so this only matters on a path that skipped validation — and the cost of
|
||||
// getting it wrong is a zero id, which looks like a plausible argument
|
||||
// rather than a fault. "Request 0 is not waiting for approval" reads like a
|
||||
// stale card, not like a type error.
|
||||
req := Request{Args: map[string]any{"a": 41, "b": float64(41), "c": int64(41)}}
|
||||
|
||||
for _, name := range []string{"a", "b", "c"} {
|
||||
if got := req.Int(name); got != 41 {
|
||||
t.Fatalf("%q read back as %d", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFractionIsNotQuietlyTruncated(t *testing.T) {
|
||||
// Silently rounding would be inventing an answer. Zero is wrong too, but it
|
||||
// is wrong in a way that shows up as "not found" rather than as the wrong
|
||||
// row being changed.
|
||||
req := Request{Args: map[string]any{"a": 41.5}}
|
||||
if got := req.Int("a"); got != 0 {
|
||||
t.Fatalf("41.5 became %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAbsentArgumentIsZero(t *testing.T) {
|
||||
req := Request{Args: map[string]any{}}
|
||||
if got := req.Int("missing"); got != 0 {
|
||||
t.Fatalf("an absent argument read as %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user