Files
backend_fiesta/services/tools/conformance_test.go
2026-09-23 17:26:13 +05:30

273 lines
9.3 KiB
Go

package tools
import (
"context"
"errors"
"testing"
"time"
"nearle/models"
)
// The sweep: every tool that ships, held to the same rules.
//
// Phase 5's measure is that a merchant's question cannot return another
// merchant's row. The per-tool tests each prove it for one tool; this proves it
// for all of them at once, and — more usefully — for the eighth tool somebody
// adds in a hurry next month. A tool that forgets appears here as a failure
// rather than as a support ticket.
// shipped is one real tool plus a way to ask what tenant its service was given.
//
// The recorders differ per tool because the services differ, and writing them
// out is the honest version: a generic mechanism would need every service to
// share an interface they have no reason to share.
type shipped struct {
tool Tool
// askedTenant reports the tenant the underlying service was called with,
// or -1 when the tool never reached its service.
askedTenant func() int
}
func shippedTools(t *testing.T) []Tool {
t.Helper()
out := make([]Tool, 0, 8)
for _, s := range shippedWithRecorders(t) {
out = append(out, s.tool)
}
return out
}
func shippedWithRecorders(t *testing.T) []shipped {
t.Helper()
corpus, err := LoadHelp()
if err != nil {
t.Fatalf("loading help: %v", err)
}
deliveries1 := &fakeDeliveries{}
deliveries2 := &fakeDeliveries{}
branches := &fakeBranches{}
approvals := &fakeApprovals{}
stocks := &fakeStocks{}
tills := &fakeTills{}
revenue := &fakeRevenue{}
counter := &fakeCounter{}
// Carries a pending request so the write resolves rather than refusing —
// the sweep is about scope and shape, not about an empty inbox.
writes := &writingApprovals{}
writes.rows = []models.StockRequest{pendingRequest(41, 12, "Rice", "R Mart")}
fixed := func() time.Time { return time.Date(2026, 9, 23, 12, 0, 0, 0, time.Local) }
return []shipped{
{StuckOrders(deliveries1, fixed), func() int { return deliveries1.last.Tenantid }},
{DeliveryProgress(deliveries2), func() int { return deliveries2.last.Tenantid }},
{BranchPerformance(branches), func() int { return branches.seen }},
{PendingApprovals(approvals, fixed), func() int { return approvals.seenTenant }},
{LowStock(stocks), func() int { return atoiOr(stocks.seenTenant, -1) }},
{TillsNotSyncing(tills), func() int { return -1 }}, // presence is keyed by branch, not tenant
{SalesByChannel(revenue, counter, fixed), func() int { return revenue.seenTid }},
// Help reaches no service and holds no shop data — it is the one tool
// with RequiresNothing, and the sweep checks that separately below.
{Help(corpus), func() int { return -1 }},
// The write. Included so it is held to every rule the reads are, and so
// a second write added later cannot quietly skip the sweep.
{ApproveStockRequest(writes, writes), func() int { return writes.seenTenant }},
}
}
func atoiOr(text string, fallback int) int {
n := 0
if text == "" {
return fallback
}
for _, r := range text {
if r < '0' || r > '9' {
return fallback
}
n = n*10 + int(r-'0')
}
return n
}
/* ── The property phase 5 is measured on ───────────────────────────────── */
func TestNoToolReadsATenantOtherThanTheCallers(t *testing.T) {
t.Setenv("POS_TOKEN_SECRET", cardSecret)
// The model is handed every argument it could possibly want, including the
// one it must never be able to use. Each tool's service must still have been
// asked only about the caller's own shop.
const mine = 1147
const theirs = 916
caller := Caller{Userid: 904, Tenantid: mine, Locationid: 1172}
poison := map[string]any{
"tenantid": theirs, "tenant_id": theirs, "locationid": 9999,
"store_id": 9999, "partnerid": theirs, "customerid": theirs,
"minutes_waiting": 10, "at_or_below": 5, "days": 7,
"question": "how do I add a cashier", "area": "people", "requestid": 41,
}
for _, s := range shippedWithRecorders(t) {
r := New(nil)
if err := r.Register(s.tool); err != nil {
t.Fatalf("registering %s: %v", s.tool.Name, err)
}
agent := Agent{Name: "sweep", Tools: []string{s.tool.Name}}
if _, err := r.Call(context.Background(), agent, s.tool.Name, poison, caller); err != nil {
t.Fatalf("%s refused a legitimate caller: %v", s.tool.Name, err)
}
if asked := s.askedTenant(); asked != -1 && asked != mine {
t.Fatalf("%s read tenant %d for a caller from %d", s.tool.Name, asked, mine)
}
}
}
func TestNoToolAcceptsAnArgumentThatChoosesWhoseDataIsRead(t *testing.T) {
// Structural, not behavioural: the schema must not offer the field at all,
// so there is nothing for a model to be argued into filling in. Register
// enforces it, and this is the sweep over everything that ships.
r := New(nil)
for _, tool := range shippedTools(t) {
if err := r.Register(tool); err != nil {
t.Fatalf("%s: %v", tool.Name, err)
}
}
}
func TestEveryTenantScopedToolRefusesACallerWithNoShop(t *testing.T) {
// Including staff. A platform account carries no tenant, and every one of
// these would otherwise run with a zero tenant and return whatever that
// means to the query underneath.
staff := Caller{Userid: 12, Superadmin: true}
for _, s := range shippedWithRecorders(t) {
if s.tool.Needs == RequiresNothing {
continue
}
r := New(nil)
if err := r.Register(s.tool); err != nil {
t.Fatalf("registering %s: %v", s.tool.Name, err)
}
_, err := r.Call(context.Background(), Agent{Name: "sweep", Tools: []string{s.tool.Name}},
s.tool.Name, map[string]any{"question": "x", "requestid": 41}, staff)
if !errors.Is(err, ErrNoTenant) {
t.Fatalf("%s answered a caller with no shop: %v", s.tool.Name, err)
}
}
}
func TestEveryBranchScopedToolRefusesAnAllBranchesCaller(t *testing.T) {
// A tool that only exists per outlet must say so rather than answering for
// whichever branch a zero happens to mean.
admin := Caller{Userid: 904, Tenantid: 1147}
found := 0
for _, s := range shippedWithRecorders(t) {
if s.tool.Needs != RequiresBranch {
continue
}
found++
r := New(nil)
_ = r.Register(s.tool)
_, err := r.Call(context.Background(), Agent{Name: "sweep", Tools: []string{s.tool.Name}},
s.tool.Name, map[string]any{"days": 7}, admin)
if !errors.Is(err, ErrNoTenant) {
t.Fatalf("%s answered without a branch: %v", s.tool.Name, err)
}
}
if found == 0 {
t.Fatal("no branch-scoped tools were exercised, so this proves nothing")
}
}
/* ── Things every tool owes the model and the reader ───────────────────── */
func TestEveryToolTellsTheModelWhenToUseIt(t *testing.T) {
// A model chooses between tools by their descriptions alone. A vague one
// produces a model that calls the wrong tool and explains the wrong number
// with total confidence.
for _, tool := range shippedTools(t) {
if len(tool.Description) < 60 {
t.Fatalf("%s has a description too thin to choose by: %q", tool.Name, tool.Description)
}
}
}
func TestNoToolWritesWhenTheModelAsksForIt(t *testing.T) {
// Every write must have been built through WriteTool, which is the only way
// to get a propose/execute pair — and Register refuses a ScopeWrite tool
// without one. A write whose Handler wrote would execute on a model's say
// so, with nobody asked.
r := New(nil)
writes := 0
for _, tool := range shippedTools(t) {
if err := r.Register(tool); err != nil {
t.Fatalf("%s: %v", tool.Name, err)
}
if tool.Scope == ScopeWrite {
writes++
if tool.propose == nil || tool.execute == nil {
t.Fatalf("%s is a write with no approval path", tool.Name)
}
}
}
if writes == 0 {
t.Fatal("no write tools were exercised, so this proves nothing")
}
}
func TestTheSweepItselfChangesNothing(t *testing.T) {
t.Setenv("POS_TOKEN_SECRET", cardSecret)
// Every other test in this file calls every tool. If a write ever executed
// on Call, this is the test that notices — and it notices for tools added
// after today.
writes := &writingApprovals{}
writes.rows = []models.StockRequest{pendingRequest(41, 12, "Rice", "R Mart")}
r := New(nil)
tool := ApproveStockRequest(writes, writes)
_ = r.Register(tool)
_, err := r.Call(context.Background(), Agent{Name: "sweep", Tools: []string{tool.Name}},
tool.Name, map[string]any{"requestid": 41}, Caller{Userid: 904, Tenantid: 1147})
if err != nil {
t.Fatalf("proposing: %v", err)
}
if len(writes.wrote) != 0 {
t.Fatalf("calling a write tool wrote to the database: %v", writes.wrote)
}
}
func TestEveryToolNamesWhereItsRowsCanBeChecked(t *testing.T) {
// Buddy states conclusions in sentences. The only honest way to present that
// is beside a link to the page holding the same rows.
caller := Caller{Userid: 904, Tenantid: 1147, Locationid: 1172}
for _, s := range shippedWithRecorders(t) {
if s.tool.Needs == RequiresNothing {
continue // the help corpus is not a page of rows
}
if s.tool.Scope == ScopeWrite {
continue // a write answers with a card, not with rows to check
}
r := New(nil)
_ = r.Register(s.tool)
result, err := r.Call(context.Background(), Agent{Name: "sweep", Tools: []string{s.tool.Name}},
s.tool.Name, map[string]any{"days": 7}, caller)
if err != nil {
t.Fatalf("%s: %v", s.tool.Name, err)
}
if result.Source == "" {
t.Fatalf("%s answers with nowhere to check it", s.tool.Name)
}
if result.Scope == "" {
t.Fatalf("%s does not say what its answer covered", s.tool.Name)
}
}
}