mcp in cart
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/promotions"
|
||||
)
|
||||
|
||||
// call issues a tools/call against the handler and returns the decoded text
|
||||
// payload (the JSON the tool returned), failing on a tool/transport error.
|
||||
func call(t *testing.T, h http.Handler, name string, args map[string]any) map[string]any {
|
||||
t.Helper()
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": map[string]any{"name": name, "arguments": args},
|
||||
})
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(body))
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s: status %d", name, rec.Code)
|
||||
}
|
||||
var resp struct {
|
||||
Result struct {
|
||||
IsError bool `json:"isError"`
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
} `json:"result"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("%s: decode response: %v (%s)", name, err, rec.Body.String())
|
||||
}
|
||||
if resp.Error != nil {
|
||||
t.Fatalf("%s: rpc error: %s", name, resp.Error.Message)
|
||||
}
|
||||
if resp.Result.IsError {
|
||||
t.Fatalf("%s: tool error: %s", name, resp.Result.Content[0].Text)
|
||||
}
|
||||
var out map[string]any
|
||||
if len(resp.Result.Content) > 0 {
|
||||
_ = json.Unmarshal([]byte(resp.Result.Content[0].Text), &out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestPromotionMCPRoundTrip(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "promotions.json")
|
||||
store, err := promotions.NewStore(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := New(store, nil).Handler()
|
||||
|
||||
rule := map[string]any{
|
||||
"id": "volymrabatt",
|
||||
"name": "Volymrabatt",
|
||||
"status": "active",
|
||||
"priority": 100,
|
||||
"conditions": []any{
|
||||
map[string]any{"id": "min", "type": "cart_total", "operator": ">=", "value": 2000000},
|
||||
},
|
||||
"actions": []any{
|
||||
map[string]any{
|
||||
"id": "tiers", "type": "tiered_discount", "value": 0,
|
||||
"config": map[string]any{
|
||||
"tiers": []any{
|
||||
map[string]any{"minTotal": 2000000, "maxTotal": 4000000, "percent": 5},
|
||||
map[string]any{"minTotal": 4000000, "maxTotal": 6000000, "percent": 9},
|
||||
map[string]any{"minTotal": 6000000, "maxTotal": 9000000, "percent": 14},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Upsert (create).
|
||||
got := call(t, h, "upsert_promotion", map[string]any{"promotion": rule})
|
||||
if got["replaced"] != false {
|
||||
t.Errorf("first upsert should create, got replaced=%v", got["replaced"])
|
||||
}
|
||||
|
||||
// It persisted to disk and a fresh store sees it.
|
||||
reloaded, err := promotions.NewStore(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(reloaded.List()) != 1 {
|
||||
t.Fatalf("expected 1 persisted promotion, got %d", len(reloaded.List()))
|
||||
}
|
||||
|
||||
// List.
|
||||
listed := call(t, h, "list_promotions", map[string]any{"status": "active"})
|
||||
if c, _ := listed["count"].(float64); c != 1 {
|
||||
t.Errorf("list count = %v, want 1", listed["count"])
|
||||
}
|
||||
|
||||
// Preview the 9% mid tier at 50 000 kr.
|
||||
preview := call(t, h, "preview_cart_discount", map[string]any{"cartTotalIncVat": 5000000})
|
||||
if d, _ := preview["discountIncVat"].(float64); int64(d) != 450000 {
|
||||
t.Errorf("discount = %v, want 450000", preview["discountIncVat"])
|
||||
}
|
||||
if n, _ := preview["netIncVat"].(float64); int64(n) != 4550000 {
|
||||
t.Errorf("net = %v, want 4550000", preview["netIncVat"])
|
||||
}
|
||||
|
||||
// Toggle off → preview yields no discount.
|
||||
call(t, h, "set_promotion_status", map[string]any{"id": "volymrabatt", "status": "inactive"})
|
||||
off := call(t, h, "preview_cart_discount", map[string]any{"cartTotalIncVat": 5000000})
|
||||
if d, _ := off["discountIncVat"].(float64); d != 0 {
|
||||
t.Errorf("inactive promo should give 0 discount, got %v", off["discountIncVat"])
|
||||
}
|
||||
|
||||
// Delete.
|
||||
call(t, h, "delete_promotion", map[string]any{"id": "volymrabatt"})
|
||||
final := call(t, h, "list_promotions", nil)
|
||||
if c, _ := final["count"].(float64); c != 0 {
|
||||
t.Errorf("after delete count = %v, want 0", final["count"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user