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) } } }