package prediction import ( "context" "fmt" "math" "net/http" "net/http/httptest" "os" "strings" "testing" "time" ) func fixture() Market { return Market{ID: "1", Question: "Will it rain?", Description: "Rain at the airport.", Outcomes: `["Yes","No"]`, Tokens: `["11","12"]`, Prices: `["0.421","0.579"]`, Active: true, Accepting: true, End: time.Now().Add(time.Hour).UTC().Format(time.RFC3339)} } func TestTradable(t *testing.T) { good := fixture() if !good.Tradable(time.Now()) { t.Fatal("valid market rejected") } cases := map[string]func(*Market){"closed": func(m *Market) { m.Closed = true }, "inactive": func(m *Market) { m.Active = false }, "paused": func(m *Market) { m.Accepting = false }, "archived": func(m *Market) { m.Archived = true }, "expired": func(m *Market) { m.End = time.Now().Add(-time.Hour).Format(time.RFC3339) }, "missing expiry": func(m *Market) { m.End = "" }, "started sport": func(m *Market) { m.Start = time.Now().Add(-time.Minute).Format(time.RFC3339) }, "bad start": func(m *Market) { m.Start = "tomorrow" }, "disputed": func(m *Market) { m.Resolution = "disputed" }, "bad price": func(m *Market) { m.Prices = `["NaN","0.5"]` }, "duplicate tokens": func(m *Market) { m.Tokens = `["11","11"]` }} for name, change := range cases { t.Run(name, func(t *testing.T) { m := good change(&m) if m.Tradable(time.Now()) { t.Fatal("unsafe market tradable") } }) } good.Start = time.Now().Add(time.Minute).UTC().Format("2006-01-02 15:04:05-07") if !good.Tradable(time.Now()) { t.Fatal("Gamma sports date format rejected") } } func TestSettlementOnlyFinalized(t *testing.T) { m := fixture() m.Prices = `["1","0"]` if _, ok := m.Settlement("11"); ok { t.Fatal("open market settled") } m.Closed = true m.Prices = `["0.999","0.001"]` if _, ok := m.Settlement("11"); ok { t.Fatal("near-final quote settled") } m.Prices = `["1","0"]` if p, ok := m.Settlement("11"); !ok || p != 1 { t.Fatal("winner not paid") } if p, ok := m.Settlement("12"); !ok || p != 0 { t.Fatal("loser not settled") } m.Prices = `["0.5","0.5"]` if p, ok := m.Settlement("11"); !ok || p != .5 { t.Fatal("split result rejected") } if _, ok := m.Settlement("unknown"); ok { t.Fatal("wrong token settled") } } func TestRoundingCannotMintCredits(t *testing.T) { for q := 1; q <= 20; q++ { for i := 1; i < 1000; i++ { p := float64(i) / 1000 if SellValue(p, q) > BuyCost(p, q) { t.Fatal("round-trip profit") } } } if BuyCost(.421, 1) != 43 || SellValue(.421, 1) != 42 || BuyCost(.29, 100) != 2900 || BuyCost(.421, 10) != 430 || SellValue(.421, 10) != 420 || BuyPrice(1e-14) != 1 || SellPrice(.29) != 29 { t.Fatal("incorrect whole-credit rounding") } } func TestClientFailuresAndBoundaries(t *testing.T) { for _, tc := range []struct { name, body string status int }{ {"rate limit", `{}`, 429}, {"server error", `{}`, 500}, {"bad JSON", ``, 200}, {"wrong identity", `{"id":"2"}`, 200}, } { t.Run(tc.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tc.status); fmt.Fprint(w, tc.body) })) defer server.Close() c := NewClient() c.Gamma = server.URL if _, err := c.Market(context.Background(), "1"); err == nil { t.Fatal("accepted bad response") } }) } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := NewClient().List(ctx); err == nil { t.Fatal("ignored cancellation") } } func TestGraphUsesContinuousLines(t *testing.T) { points := []Point{{T: 100, P: .2}, {T: 200, P: .8}, {T: 300, P: .3}} unicode := GraphMode(points, true, 32) for _, bad := range []string{"●", "*", "•"} { if strings.Contains(unicode, bad) { t.Fatalf("dot plot survived: %s", unicode) } } for _, part := range []string{"─", "┘", "┌", "┐", "└"} { if !strings.Contains(unicode, part) { t.Fatalf("missing connected line %s", part) } } flat := GraphMode([]Point{{P: .5}, {P: .5}}, true, 16) if !strings.Contains(flat, " 50% │"+strings.Repeat("─", 16)+"│") { t.Fatal("flat series should be an unbroken line") } ascii := GraphMode(points, false, 32) for _, r := range ascii { if r > 127 { t.Fatal("ASCII chart contains Unicode") } } } func TestGraphAndTerminalSanitization(t *testing.T) { graph := Graph([]Point{{T: 100, P: .1}, {T: 200, P: .9}}) for _, want := range []string{"100%", " 0%", "-", "UTC"} { if !strings.Contains(graph, want) { t.Fatalf("missing %s", want) } } if !strings.Contains(Graph(nil), "not available") { t.Fatal("empty history") } _ = Graph([]Point{{P: math.NaN()}, {P: 2}}) clean := Clean("a\x1b[31m\r\n\xff\u202eb", 100) if strings.ContainsAny(clean, "\x1b\r\n\u202e") { t.Fatal("terminal control survived") } } // Explicit opt-in smoke test; normal tests never depend on the live service. func TestPolymarketLive(t *testing.T) { if os.Getenv("TEST_POLYMARKET_LIVE") != "1" { t.Skip("set TEST_POLYMARKET_LIVE=1 for live smoke test") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() c := NewClient() markets, err := c.List(ctx) if err != nil { t.Fatal(err) } if len(markets) == 0 { t.Fatal("no eligible markets") } m, err := c.Market(ctx, markets[0].ID) if err != nil { t.Fatal(err) } _, tokens, _, _ := m.Values() history, err := c.History(ctx, tokens[0]) if err != nil { t.Fatal(err) } t.Logf("%d eligible markets; fetched %q and %d history points", len(markets), m.Question, len(history)) }