agent
This commit is contained in:
199
services/tools/branches_test.go
Normal file
199
services/tools/branches_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"nearle/models"
|
||||
)
|
||||
|
||||
type fakeBranches struct {
|
||||
rows []models.Ordersummarylocation
|
||||
err error
|
||||
seen int
|
||||
}
|
||||
|
||||
func (f *fakeBranches) GetLocationOrderSummary(tenantID int) ([]models.Ordersummarylocation, error) {
|
||||
f.seen = tenantID
|
||||
return f.rows, f.err
|
||||
}
|
||||
|
||||
func runBranches(t *testing.T, rows []models.Ordersummarylocation, caller Caller) (Result, *fakeBranches) {
|
||||
t.Helper()
|
||||
branches := &fakeBranches{rows: rows}
|
||||
r := New(nil)
|
||||
if err := r.Register(BranchPerformance(branches)); err != nil {
|
||||
t.Fatalf("registering: %v", err)
|
||||
}
|
||||
result, err := r.Call(context.Background(), Agent{Name: "orders", Tools: []string{"branch_performance"}},
|
||||
"branch_performance", nil, caller)
|
||||
if err != nil {
|
||||
t.Fatalf("calling: %v", err)
|
||||
}
|
||||
return result, branches
|
||||
}
|
||||
|
||||
func branchRows(t *testing.T, result Result) []BranchRow {
|
||||
t.Helper()
|
||||
rows, ok := result.Rows.([]BranchRow)
|
||||
if !ok {
|
||||
t.Fatalf("rows are not branches: %T", result.Rows)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func branch(id int, name string, total, delivered, cancelled int) models.Ordersummarylocation {
|
||||
return models.Ordersummarylocation{
|
||||
Locationid: id, Locationname: name,
|
||||
Total: total, Delivered: delivered, Cancelled: cancelled,
|
||||
}
|
||||
}
|
||||
|
||||
/* ── The tenant is the session's ───────────────────────────────────────── */
|
||||
|
||||
func TestBranchPerformanceReadsTheSessionsTenant(t *testing.T) {
|
||||
_, branches := runBranches(t, nil, Caller{Userid: 904, Tenantid: 1147})
|
||||
if branches.seen != 1147 {
|
||||
t.Fatalf("read tenant %d", branches.seen)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── The arithmetic ────────────────────────────────────────────────────── */
|
||||
|
||||
func TestTheCancelRateIsComputedHereNotByTheModel(t *testing.T) {
|
||||
// A model asked to divide two numbers in a sentence usually gets it right
|
||||
// and sometimes does not, and the answer looks the same either way.
|
||||
result, _ := runBranches(t, []models.Ordersummarylocation{branch(1, "R Mart", 200, 150, 30)}, merchantCaller)
|
||||
row := branchRows(t, result)[0]
|
||||
|
||||
if row.CancelRate != 15 {
|
||||
t.Fatalf("30 of 200 reported as %.1f%%", row.CancelRate)
|
||||
}
|
||||
if row.DeliveryRate != 75 {
|
||||
t.Fatalf("150 of 200 reported as %.1f%%", row.DeliveryRate)
|
||||
}
|
||||
if row.Outstanding != 20 {
|
||||
t.Fatalf("outstanding is %d, not 20", row.Outstanding)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABranchWithNoOrdersHasNoRate(t *testing.T) {
|
||||
// 0% would read as "nothing is cancelled here", which is a claim about
|
||||
// performance rather than the absence of any.
|
||||
result, _ := runBranches(t, []models.Ordersummarylocation{branch(2, "New Shop", 0, 0, 0)}, merchantCaller)
|
||||
row := branchRows(t, result)[0]
|
||||
|
||||
if row.CancelRate != 0 || row.Orders != 0 {
|
||||
t.Fatalf("an empty branch got a rate: %+v", row)
|
||||
}
|
||||
if row.Note == "" {
|
||||
t.Fatal("an empty branch does not say why it has no numbers")
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Standing out is relative ──────────────────────────────────────────── */
|
||||
|
||||
func TestABranchIsJudgedAgainstItsOwnBusinessNotAFixedNumber(t *testing.T) {
|
||||
// 9% is poor in a business averaging 3% and unremarkable in one averaging
|
||||
// 11%. A fixed threshold flags every branch of the second or none of the
|
||||
// first.
|
||||
tight, _ := runBranches(t, []models.Ordersummarylocation{
|
||||
branch(1, "Good", 100, 97, 2),
|
||||
branch(2, "Bad", 100, 88, 9),
|
||||
}, merchantCaller)
|
||||
|
||||
loose, _ := runBranches(t, []models.Ordersummarylocation{
|
||||
branch(1, "One", 100, 85, 12),
|
||||
branch(2, "Two", 100, 87, 9),
|
||||
}, merchantCaller)
|
||||
|
||||
flaggedTight := 0
|
||||
for _, row := range branchRows(t, tight) {
|
||||
if row.Note != "" {
|
||||
flaggedTight++
|
||||
}
|
||||
}
|
||||
flaggedLoose := 0
|
||||
for _, row := range branchRows(t, loose) {
|
||||
if row.Note != "" {
|
||||
flaggedLoose++
|
||||
}
|
||||
}
|
||||
|
||||
if flaggedTight == 0 {
|
||||
t.Fatal("9% against a 5.5% average was not worth mentioning")
|
||||
}
|
||||
if flaggedLoose > 0 {
|
||||
t.Fatal("9% against a 10.5% average was flagged as standing out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheWorstBranchIsFirst(t *testing.T) {
|
||||
result, _ := runBranches(t, []models.Ordersummarylocation{
|
||||
branch(1, "Fine", 100, 95, 2),
|
||||
branch(2, "Poor", 100, 80, 18),
|
||||
branch(3, "Middling", 100, 90, 7),
|
||||
}, merchantCaller)
|
||||
|
||||
if got := branchRows(t, result)[0].Branch; got != "Poor" {
|
||||
t.Fatalf("the worst branch is not first: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheBusinessAverageIsStatedForTheModelToCompareAgainst(t *testing.T) {
|
||||
// Without it a model reports "15% cancelled" with no idea whether that is
|
||||
// alarming or ordinary for this shop.
|
||||
result, _ := runBranches(t, []models.Ordersummarylocation{
|
||||
branch(1, "One", 100, 90, 10),
|
||||
branch(2, "Two", 100, 80, 20),
|
||||
}, merchantCaller)
|
||||
|
||||
if result.Note == "" {
|
||||
t.Fatal("the answer carries no business-wide figure")
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Failure ───────────────────────────────────────────────────────────── */
|
||||
|
||||
func TestABrokenBranchReadIsAnErrorNotAnEmptyShop(t *testing.T) {
|
||||
// An empty list would be reported as "you have no branches", which is a
|
||||
// statement about the business rather than about the database.
|
||||
branches := &fakeBranches{err: errors.New("the database is down")}
|
||||
r := New(nil)
|
||||
_ = r.Register(BranchPerformance(branches))
|
||||
|
||||
_, err := r.Call(context.Background(), Agent{Name: "orders", Tools: []string{"branch_performance"}},
|
||||
"branch_performance", nil, merchantCaller)
|
||||
if err == nil {
|
||||
t.Fatal("a failed read was reported as a successful answer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaffMustPickATenantForBranchPerformance(t *testing.T) {
|
||||
branches := &fakeBranches{}
|
||||
r := New(nil)
|
||||
_ = r.Register(BranchPerformance(branches))
|
||||
|
||||
_, err := r.Call(context.Background(), Agent{Name: "orders", Tools: []string{"branch_performance"}},
|
||||
"branch_performance", nil, Caller{Userid: 12, Superadmin: true})
|
||||
if err == nil {
|
||||
t.Fatal("staff compared branches across every tenant at once")
|
||||
}
|
||||
}
|
||||
|
||||
/* ── percent ───────────────────────────────────────────────────────────── */
|
||||
|
||||
func TestPercentGuardsTheZeroDenominator(t *testing.T) {
|
||||
// NaN serialises as null, which a model reads as "no data" rather than
|
||||
// "no orders".
|
||||
if got := percent(0, 0); got != 0 {
|
||||
t.Fatalf("0 of 0 is %v", got)
|
||||
}
|
||||
if got := percent(1, 3); got != 33.3 {
|
||||
t.Fatalf("1 of 3 rounded to %v", got)
|
||||
}
|
||||
if got := percent(2, 3); got != 66.7 {
|
||||
t.Fatalf("2 of 3 rounded to %v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user