package tools import ( "context" "errors" "strings" "testing" "time" "nearle/models" ) // The caller these tools are exercised as. Named rather than reusing `anyone` // so a change to one file's fixture cannot quietly alter another's meaning. var merchantCaller = Caller{Userid: 904, Tenantid: 1147} func call1(t *testing.T, tool Tool, args map[string]any, caller Caller) (Result, error) { t.Helper() r := New(nil) if err := r.Register(tool); err != nil { t.Fatalf("registering: %v", err) } return r.Call(context.Background(), Agent{Name: "orders", Tools: []string{tool.Name}}, tool.Name, args, caller) } /* ── Delivery progress ─────────────────────────────────────────────────── */ func TestDeliveryProgressCountsTheLadderInOrder(t *testing.T) { rows := []models.Deliveryinfo{ {Orderstatus: "delivered"}, {Orderstatus: "delivered"}, {Orderstatus: "active"}, {Orderstatus: "pending"}, {Orderstatus: "pending"}, {Orderstatus: "pending"}, } result, err := call1(t, DeliveryProgress(&fakeDeliveries{rows: rows}), nil, merchantCaller) if err != nil { t.Fatalf("calling: %v", err) } stages, ok := result.Rows.([]StageCount) if !ok { t.Fatalf("rows are not stages: %T", result.Rows) } // Journey order, not map order: "picked 3, accepted 8" makes a reader // rebuild the ladder in their head every time. if stages[0].Stage != "Not yet accepted" { t.Fatalf("the ladder is out of order: %+v", stages) } if result.Count != 4 { t.Fatalf("in-progress count is %d, not 4", result.Count) } } func TestDeliveryProgressLeavesOutEmptyStages(t *testing.T) { // A ladder of zeroes buries the rungs that have anything on them. result, _ := call1(t, DeliveryProgress(&fakeDeliveries{ rows: []models.Deliveryinfo{{Orderstatus: "pending"}}, }), nil, merchantCaller) stages := result.Rows.([]StageCount) if len(stages) != 1 { t.Fatalf("empty stages were listed: %+v", stages) } } func TestSkippedAndRejectedAreStoppedNotInProgress(t *testing.T) { // Both need a person, and neither is somebody currently carrying the job. result, _ := call1(t, DeliveryProgress(&fakeDeliveries{ rows: []models.Deliveryinfo{{Orderstatus: "skipped"}, {Orderstatus: "rejected"}}, }), nil, merchantCaller) if result.Count != 0 { t.Fatalf("stopped jobs counted as in progress: %d", result.Count) } for _, stage := range result.Rows.([]StageCount) { if stage.Live { t.Fatalf("%q reported as in progress", stage.Stage) } } } func TestAnUnknownStatusIsReportedNotSwallowed(t *testing.T) { // A stage nobody counted is how a whole category of work goes missing from // a board that looks complete. result, _ := call1(t, DeliveryProgress(&fakeDeliveries{ rows: []models.Deliveryinfo{{Orderstatus: "teleported"}}, }), nil, merchantCaller) if !strings.Contains(result.Note, "teleported") { t.Fatalf("an unrecognised status vanished: %q", result.Note) } } /* ── Pending approvals ─────────────────────────────────────────────────── */ type fakeApprovals struct { rows []models.StockRequest err error seenStatus string seenTenant int } func (f *fakeApprovals) GetStockRequests(tenantID, locationID int, status, date string, pageNo, pageSize int) ([]models.StockRequest, error) { f.seenTenant = tenantID f.seenStatus = status return f.rows, f.err } func TestApprovalsAskForPendingOnly(t *testing.T) { approvals := &fakeApprovals{} if _, err := call1(t, PendingApprovals(approvals, nil), nil, merchantCaller); err != nil { t.Fatalf("calling: %v", err) } if approvals.seenStatus != "Pending" { t.Fatalf("asked for status %q", approvals.seenStatus) } if approvals.seenTenant != 1147 { t.Fatalf("asked for tenant %d", approvals.seenTenant) } } func TestTheLongestWaitIsFirst(t *testing.T) { now := time.Date(2026, 9, 23, 12, 0, 0, 0, time.UTC) approvals := &fakeApprovals{rows: []models.StockRequest{ {Requestid: 1, Productname: "Rice", Created: now.AddDate(0, 0, -1)}, {Requestid: 2, Productname: "Oil", Created: now.AddDate(0, 0, -9)}, }} result, _ := call1(t, PendingApprovals(approvals, func() time.Time { return now }), nil, merchantCaller) rows := result.Rows.([]PendingApproval) if rows[0].Requestid != 2 || rows[0].WaitingDays != 9 { t.Fatalf("the nine-day-old request is not first: %+v", rows) } } func TestARequestFromTheFutureDoesNotWaitNegativeDays(t *testing.T) { now := time.Date(2026, 9, 23, 12, 0, 0, 0, time.UTC) approvals := &fakeApprovals{rows: []models.StockRequest{ {Requestid: 1, Created: now.AddDate(0, 0, 3)}, }} result, _ := call1(t, PendingApprovals(approvals, func() time.Time { return now }), nil, merchantCaller) if got := result.Rows.([]PendingApproval)[0].WaitingDays; got != 0 { t.Fatalf("a clock ahead of ours produced %d days", got) } } /* ── Low stock ─────────────────────────────────────────────────────────── */ type fakeStocks struct { rows []models.Productstocks err error seenTenant string seenBranch string } func (f *fakeStocks) GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error) { f.seenTenant, f.seenBranch = tenantID, locationID return f.rows, f.err } func TestOutOfStockIsDistinctFromLow(t *testing.T) { // "Out" and "nearly out" need different actions, and a model left to infer // the difference from a number will sometimes call zero "low". stocks := &fakeStocks{rows: []models.Productstocks{ {Productid: 1, Productname: "Rice", Quantity: 0}, {Productid: 2, Productname: "Oil", Quantity: 3}, {Productid: 3, Productname: "Salt", Quantity: 40}, }} result, _ := call1(t, LowStock(stocks), nil, merchantCaller) rows := result.Rows.([]LowStockLine) if len(rows) != 2 { t.Fatalf("a well-stocked product was listed: %+v", rows) } if rows[0].State != "out" || rows[1].State != "low" { t.Fatalf("states are wrong: %+v", rows) } } func TestTheLowStockThresholdCanBeMoved(t *testing.T) { // Five is a guess — nothing in this backend records a reorder level — so // the person asking has to be able to change it. stocks := &fakeStocks{rows: []models.Productstocks{{Productid: 1, Quantity: 12}}} quiet, _ := call1(t, LowStock(stocks), nil, merchantCaller) if quiet.Count != 0 { t.Fatal("twelve was low at the default threshold") } loud, _ := call1(t, LowStock(stocks), map[string]any{"at_or_below": 20}, merchantCaller) if loud.Count != 1 { t.Fatal("twelve was not low at a threshold of twenty") } } func TestLowStockIsStableBetweenRefetches(t *testing.T) { // Two products on the same count must not swap places every refresh. stocks := &fakeStocks{rows: []models.Productstocks{ {Productid: 2, Productname: "Zinc", Quantity: 1}, {Productid: 1, Productname: "Atta", Quantity: 1}, }} result, _ := call1(t, LowStock(stocks), nil, merchantCaller) rows := result.Rows.([]LowStockLine) if rows[0].Product != "Atta" { t.Fatalf("ties are not broken by name: %+v", rows) } } /* ── Tills ─────────────────────────────────────────────────────────────── */ type fakeTills struct { rows []map[string]string err error } func (f *fakeTills) LocationHealth(context.Context, string) ([]map[string]string, error) { return f.rows, f.err } func TestAMissingHeartbeatIsNamedNotBlank(t *testing.T) { // Presence expires from Redis, so absence IS the signal. A blank state // reads as "fine" to a model. tills := &fakeTills{rows: []map[string]string{{"terminalid": "T5EDD"}}} result, _ := call1(t, TillsNotSyncing(tills), nil, Caller{Userid: 904, Tenantid: 1147, Locationid: 1185}) rows := result.Rows.([]TillStatus) if rows[0].State != "no heartbeat" { t.Fatalf("a till with no state reported as %q", rows[0].State) } if result.Count != 1 { t.Fatalf("a silent till was not counted as a problem: %d", result.Count) } } func TestTillKeysAreReadUnderEverySpellingTheHashUses(t *testing.T) { // Presence rows come from a Redis hash rather than a struct, so the // spellings are whatever the writer used. Reading one name and finding // nothing would report every till as silent. tills := &fakeTills{rows: []map[string]string{{"terminal_id": "T99", "status": "online"}}} result, _ := call1(t, TillsNotSyncing(tills), nil, Caller{Userid: 904, Tenantid: 1147, Locationid: 1185}) rows := result.Rows.([]TillStatus) if rows[0].Terminal != "T99" { t.Fatalf("the terminal id was not found: %+v", rows[0]) } if result.Count != 0 { t.Fatal("an online till was counted as a problem") } } func TestTillsRefuseAnAllBranchesQuestionRatherThanAnsweringEmpty(t *testing.T) { // Presence is keyed by outlet and there is no all-branches read. An empty // list would be reported as "all tills are fine". _, err := call1(t, TillsNotSyncing(&fakeTills{}), nil, merchantCaller) if err == nil { t.Fatal("an all-branches till question was answered") } } func TestNoTerminalsAtAllIsExplained(t *testing.T) { result, _ := call1(t, TillsNotSyncing(&fakeTills{}), nil, Caller{Userid: 904, Tenantid: 1147, Locationid: 1185}) if result.Note == "" { t.Fatal("a branch with no terminals says nothing") } } func TestABrokenPresenceReadIsAnError(t *testing.T) { _, err := call1(t, TillsNotSyncing(&fakeTills{err: errors.New("redis is down")}), nil, Caller{Userid: 904, Tenantid: 1147, Locationid: 1185}) if err == nil { t.Fatal("a failed presence read was reported as healthy tills") } }