package vergex import ( "fmt" "net/url" "strings" "testing" ) func TestParseSignalRankingAndFilterTradFiItems(t *testing.T) { body := []byte(`{ "data": { "rankings": [ {"marketType":"hip3-perp","symbol":"AAPL","bias":"long","confidence":0.88,"compositeZ":1.75}, {"marketType":"stock","symbol":"NVDA","bias":"long","confidence":0.81,"compositeZ":1.25}, {"market_type":"core_perp","symbol":"BTC","bias":"short","score":0.91} ] } }`) ranking, err := ParseSignalRanking(body) if err != nil { t.Fatalf("ParseSignalRanking returned error: %v", err) } if len(ranking.Items) != 3 { t.Fatalf("items len = %d, want 3", len(ranking.Items)) } if ranking.Items[0].Symbol != "AAPL" || ranking.Items[0].MarketType != "hip3-perp" || ranking.Items[0].Bias != "long" { t.Fatalf("unexpected first item: %+v", ranking.Items[0]) } items := FilterTradFiItems(ranking.Items, "hip3_perp", 5) if len(items) != 2 { t.Fatalf("filtered len = %d, want 2", len(items)) } if got := TradableSymbol(items[0].Symbol); got != "xyz:AAPL" { t.Fatalf("TradableSymbol = %q, want xyz:AAPL", got) } if got := TradableSymbol(items[1].Symbol); got != "xyz:NVDA" { t.Fatalf("TradableSymbol = %q, want xyz:NVDA", got) } } func TestFilterTradFiItemsAllowsFullClaw402Board(t *testing.T) { items := make([]SignalRankItem, 0, 35) for i := 1; i <= 35; i++ { items = append(items, SignalRankItem{ Rank: i, Symbol: fmt.Sprintf("xyz:STK%02d", i), MarketType: "hip3_perp", Bias: "bullish", }) } filtered := FilterTradFiItems(items, "hip3_perp", 30) if len(filtered) != 30 { t.Fatalf("filtered len = %d, want 30", len(filtered)) } if filtered[0].Symbol != "STK01" || filtered[29].Symbol != "STK30" { t.Fatalf("unexpected filtered bounds: first=%q last=%q", filtered[0].Symbol, filtered[29].Symbol) } capped := FilterTradFiItems(items, "hip3_perp", 35) if len(capped) != MaxSignalRankingItems { t.Fatalf("capped len = %d, want %d", len(capped), MaxSignalRankingItems) } } func TestParseSignalRankingReadsNestedMarketShape(t *testing.T) { body := []byte(`{ "data": { "items": [ {"market":{"marketType":"hip3_perp","symbol":"xyz:NBIS"},"symbol":"xyz:NBIS","bias":"bullish","compositeZ":1.05,"rank":5}, {"market":{"marketType":"hip3_perp","symbol":"xyz:DRAM"},"symbol":"xyz:DRAM","bias":"bullish","compositeZ":0.47,"rank":10}, {"market":{"marketType":"core_perp","symbol":"BTC"},"symbol":"BTC","bias":"bearish","compositeZ":-0.08,"rank":12} ] } }`) ranking, err := ParseSignalRanking(body) if err != nil { t.Fatalf("ParseSignalRanking returned error: %v", err) } allItems := FilterSignalRankingItems(ranking.Items, "all", 30) if len(allItems) != 3 { t.Fatalf("all filtered len = %d, want 3: %+v", len(allItems), allItems) } if allItems[2].Symbol != "BTC" || allItems[2].MarketType != "core_perp" || allItems[2].Category != "crypto" { t.Fatalf("unexpected crypto item: %+v", allItems[2]) } items := FilterTradFiItems(ranking.Items, "hip3_perp", 30) if len(items) != 2 { t.Fatalf("filtered len = %d, want 2: %+v", len(items), items) } if items[0].Symbol != "NBIS" || items[0].MarketType != "hip3_perp" { t.Fatalf("unexpected first item: %+v", items[0]) } if items[1].Symbol != "DRAM" || items[1].MarketType != "hip3_perp" { t.Fatalf("unexpected second item: %+v", items[1]) } } func TestMarketSymbolPreservesHIP3XYZPrefix(t *testing.T) { if got := MarketSymbol("hip3_perp", "INTC"); got != "xyz:INTC" { t.Fatalf("MarketSymbol hip3_perp/INTC = %q, want xyz:INTC", got) } if got := MarketSymbol("hip3_perp", "xyz:skhx"); got != "xyz:SKHX" { t.Fatalf("MarketSymbol hip3_perp/xyz:skhx = %q, want xyz:SKHX", got) } if got := MarketSymbol("core_perp", "BTC"); got != "BTC" { t.Fatalf("MarketSymbol core_perp/BTC = %q, want BTC", got) } if got := TradableSymbolForMarket("core_perp", "BTC"); got != "BTC" { t.Fatalf("TradableSymbolForMarket core_perp/BTC = %q, want BTC", got) } if got := TradableSymbolForMarket("hip3_perp", "INTC"); got != "xyz:INTC" { t.Fatalf("TradableSymbolForMarket hip3_perp/INTC = %q, want xyz:INTC", got) } } func TestAddQueryDefaultsUsesClaw402GatewayParams(t *testing.T) { params := url.Values{} addQueryDefaults(params, Query{ MarketType: "hip3_perp", Symbol: "INTC", Chain: "hyperliquid", LiqBand: "15", }, true) if got := params.Get("marketType"); got != "hip3_perp" { t.Fatalf("marketType = %q", got) } if got := params.Get("symbol"); got != "xyz:INTC" { t.Fatalf("symbol = %q, want xyz:INTC", got) } if got := params.Get("chain"); got != "mainnet" { t.Fatalf("chain = %q, want mainnet", got) } if got := params.Get("liqBand"); got != "15" { t.Fatalf("liqBand = %q, want 15", got) } } func TestQueryChainMapsHyperliquidToVergexMainnet(t *testing.T) { if got := QueryChain("hyperliquid"); got != "mainnet" { t.Fatalf("QueryChain hyperliquid = %q, want mainnet", got) } } func TestFormatAnalysisForAIIncludesDetailErrors(t *testing.T) { text := FormatAnalysisForAI(&MarketAnalysis{ Symbol: "xyz:NVDA", QuerySymbol: "NVDA", MarketType: "stock", SignalLabError: "upstream returned status 502", HeatmapError: "market not found", }) if !containsAll(text, "Signal Lab: unavailable", "upstream returned status 502", "Cost/Liquidation Heatmap: unavailable", "market not found") { t.Fatalf("formatted analysis did not include detail errors:\n%s", text) } } func TestFormatAnalysisForAIFormatsVergexDetailsAsMarkdown(t *testing.T) { text := FormatAnalysisForAI(&MarketAnalysis{ Symbol: "xyz:DRAM", QuerySymbol: "DRAM", MarketType: "hip3_perp", SignalLab: []byte(`{ "data": { "band": "15", "bias": "bullish", "compositeZ": 1.41, "confidence": "Medium", "dimensions": [ { "family": "I Cost & Positioning", "label": "Capital-gains overhang", "direction": "bullish", "strength": "medium", "percentile": 80, "detail": "price is above aggregate cost" } ] } }`), Heatmap: []byte(`{ "data": { "binStep": 3.2, "bins": [ {"bucketStartPrice": 100, "bucketEndPrice": 103.2, "longCost": 1200000, "shortCost": 1000, "longLiq": 5000, "shortLiq": 700000}, {"bucketStartPrice": 103.2, "bucketEndPrice": 106.4, "longCost": 1000, "shortCost": 2000, "longLiq": 900000, "shortLiq": 4000} ] } }`), }) if !containsAll(text, "#### Signal Lab", "| Family | Signal | Direction | Strength | Percentile | Detail |", "Capital-gains overhang", "#### Cost/Liquidation Heatmap", "| Price zone | Long cost | Short cost | Long liq | Short liq | Main cluster |", "$1.20M", ) { t.Fatalf("formatted analysis is not markdown enough:\n%s", text) } if strings.Contains(text, "Signal Lab: {") || strings.Contains(text, "Cost/Liquidation Heatmap: {") { t.Fatalf("formatted analysis still includes raw inline JSON:\n%s", text) } } func containsAll(text string, needles ...string) bool { for _, needle := range needles { if !strings.Contains(text, needle) { return false } } return true }