| 1 | package economics |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | func TestKeys(t *testing.T) { |
| 9 | if got := ScenarioKey("launch-pricing"); got != "economics/scenarios/launch-pricing.json" { |
| 10 | t.Fatalf("ScenarioKey = %q", got) |
| 11 | } |
| 12 | if got := BillKey("2026-08"); got != "economics/bills/2026-08.json" { |
| 13 | t.Fatalf("BillKey = %q", got) |
| 14 | } |
| 15 | if ActiveKey != "economics/active.json" { |
| 16 | t.Fatalf("ActiveKey = %q", ActiveKey) |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | func TestDefaultScenarioValidates(t *testing.T) { |
| 21 | sc := DefaultScenario() |
| 22 | if err := sc.Validate(); err != nil { |
| 23 | t.Fatalf("default scenario invalid: %v", err) |
| 24 | } |
| 25 | if sc.Mode != "cost-plus" || sc.Margin != 0.8 { |
| 26 | t.Fatalf("default = mode %q margin %v", sc.Mode, sc.Margin) |
| 27 | } |
| 28 | if _, ok := sc.RateCard.Boxes[""]; !ok { |
| 29 | t.Fatal("default rate card must carry a \"\" default box spec") |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestValidateRejects(t *testing.T) { |
| 34 | cases := []func(*Scenario){ |
| 35 | func(s *Scenario) { s.Margin = 1.0 }, // price would be infinite |
| 36 | func(s *Scenario) { s.Margin = -0.1 }, |
| 37 | func(s *Scenario) { s.Mode = "surge" }, |
| 38 | func(s *Scenario) { s.Allocation.Policy = "vibes" }, |
| 39 | func(s *Scenario) { s.Allocation.Policy = "blend"; s.Allocation.CPUWeight = 1.5 }, |
| 40 | func(s *Scenario) { s.Name = "Bad Name!" }, |
| 41 | func(s *Scenario) { s.RateCard.Boxes = nil }, |
| 42 | } |
| 43 | for i, mutate := range cases { |
| 44 | sc := DefaultScenario() |
| 45 | sc.Name = "test" |
| 46 | mutate(&sc) |
| 47 | if err := sc.Validate(); err == nil { |
| 48 | t.Fatalf("case %d: mutation validated", i) |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestValidScenarioName(t *testing.T) { |
| 54 | for name, want := range map[string]bool{ |
| 55 | "launch-pricing": true, "v2": true, "": false, "UP": false, |
| 56 | "has space": false, "-lead": false, "trail-": false, |
| 57 | "waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay-too-long": false, |
| 58 | } { |
| 59 | if got := ValidScenarioName(name); got != want { |
| 60 | t.Fatalf("ValidScenarioName(%q) = %v", name, got) |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func TestDefaultScenarioV2Knobs(t *testing.T) { |
| 66 | sc := DefaultScenario() |
| 67 | if sc.Sheet.BoxMonthServerUSD != 5 || sc.Sheet.BoxMonthDeviceUSD != 1 { |
| 68 | t.Fatalf("box prices = %v/%v", sc.Sheet.BoxMonthServerUSD, sc.Sheet.BoxMonthDeviceUSD) |
| 69 | } |
| 70 | if sc.FleetBreak.MinBoxes != 10 || sc.FleetBreak.Discount != 0.4 { |
| 71 | t.Fatalf("fleet break = %+v", sc.FleetBreak) |
| 72 | } |
| 73 | if sc.Pool.RevenueFrac != 0.10 || sc.Pool.TopupUSD != 200 { |
| 74 | t.Fatalf("pool = %+v", sc.Pool) |
| 75 | } |
| 76 | if err := sc.Validate(); err != nil { |
| 77 | t.Fatal(err) |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func TestValidateRejectsV2(t *testing.T) { |
| 82 | cases := []func(*Scenario){ |
| 83 | func(s *Scenario) { s.FleetBreak.Discount = 1.0 }, |
| 84 | func(s *Scenario) { s.FleetBreak.Discount = -0.1 }, |
| 85 | func(s *Scenario) { s.FleetBreak.MinBoxes = -1 }, |
| 86 | func(s *Scenario) { s.Pool.RevenueFrac = 1.5 }, |
| 87 | func(s *Scenario) { s.Pool.TopupUSD = -5 }, |
| 88 | func(s *Scenario) { s.Sheet.MemGBHourUSD = -0.01 }, |
| 89 | } |
| 90 | for i, mutate := range cases { |
| 91 | sc := DefaultScenario() |
| 92 | sc.Name = "test" |
| 93 | mutate(&sc) |
| 94 | if err := sc.Validate(); err == nil { |
| 95 | t.Fatalf("case %d validated", i) |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | func TestOldScenarioStillParses(t *testing.T) { |
| 101 | old := `{"schema":1,"name":"x","rate_card":{"boxes":{"":{"hour_usd":0.02,"cores":2,"mem_gb":2}}},"allocation":{"policy":"cpu"},"mode":"cost-plus","margin":0.8}` |
| 102 | var sc Scenario |
| 103 | if err := json.Unmarshal([]byte(old), &sc); err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | if sc.Pool.RevenueFrac != 0 || sc.FleetBreak.MinBoxes != 0 { |
| 107 | t.Fatalf("zero-defaults broken: %+v %+v", sc.Pool, sc.FleetBreak) |
| 108 | } |
| 109 | } |
| 110 | |