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

381 lines
14 KiB
Go

package tools
import (
"context"
"errors"
"strings"
"testing"
"time"
"nearle/models"
)
const cardSecret = "a-test-signing-key-long-enough"
// writingApprovals records every write, so a test can assert that nothing
// happened as easily as that something did.
type writingApprovals struct {
fakeApprovals
wrote []int
status string
err error
}
func (w *writingApprovals) UpdateStockRequest(requestID int, status string) error {
if w.err != nil {
return w.err
}
w.wrote = append(w.wrote, requestID)
w.status = status
return nil
}
func pendingRequest(id, qty int, product, branch string) models.StockRequest {
return models.StockRequest{
Requestid: id, Qty: qty, Productname: product, Locationname: branch,
Status: "Pending", Created: time.Date(2026, 9, 20, 9, 0, 0, 0, time.UTC),
}
}
func approvalSetup(t *testing.T, rows ...models.StockRequest) (*Registry, *writingApprovals, Agent) {
t.Helper()
t.Setenv("POS_TOKEN_SECRET", cardSecret)
store := &writingApprovals{}
store.rows = rows
r := New(nil)
if err := r.Register(ApproveStockRequest(store, store)); err != nil {
t.Fatalf("registering: %v", err)
}
return r, store, Agent{Name: "inventory", Tools: []string{"approve_stock_request"}}
}
var owner = Caller{Userid: 904, Tenantid: 1147}
// propose runs the model's half and returns the card.
func propose(t *testing.T, r *Registry, agent Agent, id int, caller Caller) (Proposal, error) {
t.Helper()
result, err := r.Call(context.Background(), agent, "approve_stock_request",
map[string]any{"requestid": id}, caller)
if err != nil {
return Proposal{}, err
}
proposal, ok := result.Rows.(Proposal)
if !ok {
t.Fatalf("a write returned %T, not a proposal", result.Rows)
}
return proposal, nil
}
/* ── Nothing auto-executes ─────────────────────────────────────────────── */
func TestAskingForAWriteChangesNothing(t *testing.T) {
// The whole phase in one test. The model asked; the database did not move.
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, err := propose(t, r, agent, 41, owner)
if err != nil {
t.Fatalf("proposing: %v", err)
}
if len(store.wrote) != 0 {
t.Fatalf("a write happened on the model's say so: %v", store.wrote)
}
if proposal.Card == "" {
t.Fatal("no card came back, so nothing can be approved")
}
}
func TestTheModelIsToldItHasNotDoneTheThing(t *testing.T) {
// Without this it reports the action in the past tense, and the person
// believes it.
r, _, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
result, err := r.Call(context.Background(), agent, "approve_stock_request",
map[string]any{"requestid": 41}, owner)
if err != nil {
t.Fatalf("proposing: %v", err)
}
note := strings.ToLower(result.Note)
if !strings.Contains(note, "not been done") || !strings.Contains(note, "do not say it is done") {
t.Fatalf("the model was not told to hold off: %q", result.Note)
}
}
func TestAWriteToolCannotBeBuiltWithAPlainHandler(t *testing.T) {
// The structural half: there is no shape of Tool a caller can construct
// that writes when the model asks for it.
r := New(nil)
err := r.Register(Tool{
Name: "sneaky", Description: strings.Repeat("a write pretending to be ordinary ", 3),
Scope: ScopeWrite,
Handler: func(context.Context, Request) (Result, error) {
t.Fatal("a hand-built write tool ran")
return Result{}, nil
},
})
if err == nil {
t.Fatal("a write tool was registered without a propose/execute pair")
}
}
/* ── Approving ─────────────────────────────────────────────────────────── */
func TestAnApprovedCardPerformsTheWriteExactlyOnce(t *testing.T) {
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, _ := propose(t, r, agent, 41, owner)
result, err := r.Approve(context.Background(), agent, proposal.Card, owner)
if err != nil {
t.Fatalf("approving: %v", err)
}
if len(store.wrote) != 1 || store.wrote[0] != 41 {
t.Fatalf("the write did not happen as expected: %v", store.wrote)
}
if store.status != "Approved" {
t.Fatalf("wrote status %q — the column expects the capitalised word", store.status)
}
if result.Count != 1 {
t.Fatalf("the result does not describe the change: %+v", result)
}
}
func TestAnUnapprovedCardIsTheOnlyWayInAndAForgedOneFails(t *testing.T) {
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
for _, forged := range []string{
"", "c1.", "c1.rubbish.signature", "not-a-card",
"c1.eyJ0IjoiYXBwcm92ZV9zdG9ja19yZXF1ZXN0In0.wrongsignature",
} {
if _, err := r.Approve(context.Background(), agent, forged, owner); err == nil {
t.Fatalf("a forged card was accepted: %q", forged)
}
}
if len(store.wrote) != 0 {
t.Fatalf("a forged card wrote something: %v", store.wrote)
}
}
func TestACardIsNotTransferable(t *testing.T) {
// Without this, one person's approval could be replayed by another —
// including somebody in a different shop.
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, _ := propose(t, r, agent, 41, owner)
someoneElse := Caller{Userid: 905, Tenantid: 1147}
if _, err := r.Approve(context.Background(), agent, proposal.Card, someoneElse); !errors.Is(err, ErrNotYourCard) {
t.Fatalf("another user approved somebody else's card: %v", err)
}
anotherShop := Caller{Userid: 904, Tenantid: 916}
if _, err := r.Approve(context.Background(), agent, proposal.Card, anotherShop); !errors.Is(err, ErrNotYourCard) {
t.Fatalf("another shop approved this card: %v", err)
}
if len(store.wrote) != 0 {
t.Fatalf("a transferred card wrote something: %v", store.wrote)
}
}
func TestApprovalDoesNotCarryForward(t *testing.T) {
// Approving one request is not consent for the next. A second card has to
// be resolved and approved on its own.
r, store, agent := approvalSetup(t,
pendingRequest(41, 12, "Rice", "R Mart"),
pendingRequest(42, 3, "Oil", "R Mart"))
first, _ := propose(t, r, agent, 41, owner)
if _, err := r.Approve(context.Background(), agent, first.Card, owner); err != nil {
t.Fatalf("approving: %v", err)
}
if len(store.wrote) != 1 || store.wrote[0] != 41 {
t.Fatalf("approving one request touched another: %v", store.wrote)
}
}
/* ── Re-validated at execution ─────────────────────────────────────────── */
func TestACardApprovedTwiceDoesNotWriteTwice(t *testing.T) {
// A signed card carries no nonce. Replay is refused where it belongs —
// against the live database, which no longer has the request pending.
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, _ := propose(t, r, agent, 41, owner)
if _, err := r.Approve(context.Background(), agent, proposal.Card, owner); err != nil {
t.Fatalf("first approval: %v", err)
}
// The request is no longer pending, exactly as the database would report.
store.rows = nil
if _, err := r.Approve(context.Background(), agent, proposal.Card, owner); err == nil {
t.Fatal("the same card wrote a second time")
}
if len(store.wrote) != 1 {
t.Fatalf("the write happened %d times", len(store.wrote))
}
}
func TestACardExpires(t *testing.T) {
// A card resolved against a shop half an hour ago is a card about a shop
// that has moved on.
r, _, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, _ := propose(t, r, agent, 41, owner)
r.now = func() time.Time { return time.Now().Add(2 * CardExpiryForTests) }
if _, err := r.Approve(context.Background(), agent, proposal.Card, owner); err == nil {
t.Fatal("an expired card was approved")
}
}
func TestAFailedWriteIsReportedNotSwallowed(t *testing.T) {
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, _ := propose(t, r, agent, 41, owner)
store.err = errors.New("the database is down")
if _, err := r.Approve(context.Background(), agent, proposal.Card, owner); err == nil {
t.Fatal("a failed write reported success")
}
}
/* ── Ownership is re-derived, never compared ───────────────────────────── */
func TestARequestFromAnotherShopIsNotFound(t *testing.T) {
// The lookup is scoped to the caller's tenant, so another merchant's id is
// simply absent. "Not found" and "not yours" are deliberately the same
// message — distinguishing them would make this a way to ask whether an id
// exists somewhere else on the platform.
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
if _, err := propose(t, r, agent, 999, owner); err == nil {
t.Fatal("a request that is not this shop's was resolved")
}
if len(store.wrote) != 0 {
t.Fatalf("something was written: %v", store.wrote)
}
}
func TestTheModelCannotNameAShop(t *testing.T) {
tool := ApproveStockRequest(&writingApprovals{}, &writingApprovals{})
for _, field := range tool.Schema.Fields {
if scopingArguments[strings.ToLower(field.Name)] {
t.Fatalf("the write offers %q for the model to set", field.Name)
}
}
}
/* ── What the person sees ──────────────────────────────────────────────── */
func TestTheCardShowsWhatWasResolvedNotWhatWasAsked(t *testing.T) {
// A person approving "approve this request" must see which request, from
// which branch, for what — the sentence and the action are produced by
// different things and only one of them is checkable.
r, _, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, _ := propose(t, r, agent, 41, owner)
if !strings.Contains(proposal.Summary, "Rice") || !strings.Contains(proposal.Summary, "R Mart") {
t.Fatalf("the summary names nothing checkable: %q", proposal.Summary)
}
labels := map[string]string{}
for _, detail := range proposal.Details {
labels[detail.Label] = detail.Value
}
if labels["Request"] != "#41" {
t.Fatalf("the card does not name the request by id: %+v", proposal.Details)
}
for _, want := range []string{"Product", "Quantity", "Branch"} {
if labels[want] == "" {
t.Fatalf("the card is missing %q: %+v", proposal.Details, want)
}
}
}
func TestAnUnusualQuantityIsFlaggedAndAnOrdinaryOneIsNot(t *testing.T) {
// A warning on every card is a warning on none.
r, _, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
ordinary, _ := propose(t, r, agent, 41, owner)
if ordinary.Warning != "" {
t.Fatalf("an ordinary request was flagged: %q", ordinary.Warning)
}
r2, _, agent2 := approvalSetup(t, pendingRequest(42, 9000, "Rice", "R Mart"))
large, _ := propose(t, r2, agent2, 42, owner)
if large.Warning == "" {
t.Fatal("nine thousand units passed without comment")
}
}
/* ── The audit trail ───────────────────────────────────────────────────── */
func TestAnApprovalIsRecordedBeforeTheWriteLeaves(t *testing.T) {
// A crash mid-write has to leave a trace that it was attempted. A row
// written only on success is missing exactly when it is needed.
t.Setenv("POS_TOKEN_SECRET", cardSecret)
audit := &CollectAudit{}
store := &writingApprovals{}
store.rows = []models.StockRequest{pendingRequest(41, 12, "Rice", "R Mart")}
store.err = errors.New("the database died mid-write")
r := New(audit)
_ = r.Register(ApproveStockRequest(store, store))
agent := Agent{Name: "inventory", Tools: []string{"approve_stock_request"}}
proposal, _ := propose(t, r, agent, 41, owner)
_, _ = r.Approve(context.Background(), agent, proposal.Card, owner)
var sawApproved, sawFailed bool
for _, entry := range audit.Entries {
if entry.Outcome == "approved" {
sawApproved = true
}
if entry.Outcome == OutcomeFailed {
sawFailed = true
}
}
if !sawApproved {
t.Fatal("a write that died left no record that it was attempted")
}
if !sawFailed {
t.Fatal("the failure itself was not recorded")
}
}
func TestARejectedCardLeavesNoSideEffect(t *testing.T) {
// Rejection is the console never calling Approve. Nothing to undo, nothing
// half-written — and the proposal itself is still in the trail.
t.Setenv("POS_TOKEN_SECRET", cardSecret)
audit := &CollectAudit{}
store := &writingApprovals{}
store.rows = []models.StockRequest{pendingRequest(41, 12, "Rice", "R Mart")}
r := New(audit)
_ = r.Register(ApproveStockRequest(store, store))
agent := Agent{Name: "inventory", Tools: []string{"approve_stock_request"}}
if _, err := propose(t, r, agent, 41, owner); err != nil {
t.Fatalf("proposing: %v", err)
}
if len(store.wrote) != 0 {
t.Fatalf("an unapproved proposal wrote something: %v", store.wrote)
}
last, _ := audit.Last()
if last.Outcome != "proposed" {
t.Fatalf("the proposal is not in the trail: %+v", last)
}
}
/* ── The allow-list still applies at approval ──────────────────────────── */
func TestACardCannotOutliveAnAgentLosingTheTool(t *testing.T) {
r, store, agent := approvalSetup(t, pendingRequest(41, 12, "Rice", "R Mart"))
proposal, _ := propose(t, r, agent, 41, owner)
narrowed := Agent{Name: "inventory", Tools: []string{"low_stock"}}
if _, err := r.Approve(context.Background(), narrowed, proposal.Card, owner); !errors.Is(err, ErrNotAllowed) {
t.Fatalf("a card was a way around the allow-list: %v", err)
}
if len(store.wrote) != 0 {
t.Fatalf("something was written: %v", store.wrote)
}
}