Files
nofx/agent/ai500_test.go
tinkle-community 2c6e2827e8 feat(agent): surface the AI500 index board in chat, tools, and sidebar
- provider/nofxos: GetAI500ListCached — 5min TTL cache with stale
  fallback on upstream failure; ResolveClient routes through the claw402
  gateway when a wallet key is configured (user's claw402 model key ->
  CLAW402_WALLET_KEY env -> direct nofxos)
- new GET /api/ai500 endpoint serving the score-sorted board
- new get_ai500_list agent tool + prompt rule: when the user wants coin
  picks or creates a strategy without naming coins, consult AI500's
  high-scoring entries by default
- web: AI500 sidebar panel (rank, score badge, gain since entry,
  5min auto-refresh); clicking an entry asks the agent to analyze it
2026-06-11 22:11:03 +08:00

105 lines
2.9 KiB
Go

package agent
import (
"encoding/json"
"errors"
"log/slog"
"strings"
"testing"
"nofx/provider/nofxos"
)
func withStubbedAI500(t *testing.T, fn func(walletKey string) ([]nofxos.CoinData, error)) {
t.Helper()
original := fetchAI500ForTool
fetchAI500ForTool = fn
t.Cleanup(func() { fetchAI500ForTool = original })
}
func TestToolGetAI500ListSortsByScoreAndLimits(t *testing.T) {
withStubbedAI500(t, func(walletKey string) ([]nofxos.CoinData, error) {
return []nofxos.CoinData{
{Pair: "LOWUSDT", Score: 10, IncreasePercent: -3},
{Pair: "TOPUSDT", Score: 99, IncreasePercent: 42},
{Pair: "MIDUSDT", Score: 55, IncreasePercent: 7},
}, nil
})
a := New(nil, nil, DefaultConfig(), slog.Default())
raw := a.toolGetAI500List("default", `{"limit": 2}`)
var resp struct {
Status string `json:"status"`
Count int `json:"count"`
Coins []struct {
Pair string `json:"pair"`
Score float64 `json:"score"`
IncreasePercent float64 `json:"increase_percent"`
} `json:"coins"`
}
if err := json.Unmarshal([]byte(raw), &resp); err != nil {
t.Fatalf("invalid JSON %q: %v", raw, err)
}
if resp.Status != "ok" || resp.Count != 2 || len(resp.Coins) != 2 {
t.Fatalf("unexpected response: %+v", resp)
}
if resp.Coins[0].Pair != "TOPUSDT" || resp.Coins[1].Pair != "MIDUSDT" {
t.Fatalf("expected score-descending order, got %+v", resp.Coins)
}
}
func TestToolGetAI500ListDefaultLimit(t *testing.T) {
coins := make([]nofxos.CoinData, 30)
for i := range coins {
coins[i] = nofxos.CoinData{Pair: "C", Score: float64(i)}
}
withStubbedAI500(t, func(walletKey string) ([]nofxos.CoinData, error) {
return coins, nil
})
a := New(nil, nil, DefaultConfig(), slog.Default())
var resp struct {
Count int `json:"count"`
}
if err := json.Unmarshal([]byte(a.toolGetAI500List("default", "")), &resp); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if resp.Count != 20 {
t.Fatalf("default limit = %d, want 20", resp.Count)
}
}
func TestToolGetAI500ListUpstreamError(t *testing.T) {
withStubbedAI500(t, func(walletKey string) ([]nofxos.CoinData, error) {
return nil, errors.New("upstream down")
})
a := New(nil, nil, DefaultConfig(), slog.Default())
raw := a.toolGetAI500List("default", "")
if !strings.Contains(raw, `"error"`) {
t.Fatalf("expected error payload, got %q", raw)
}
}
func TestHandleToolCallDispatchesAI500(t *testing.T) {
withStubbedAI500(t, func(walletKey string) ([]nofxos.CoinData, error) {
return []nofxos.CoinData{{Pair: "BTCUSDT", Score: 90}}, nil
})
a := New(nil, nil, DefaultConfig(), slog.Default())
raw := a.handleToolCall(t.Context(), "default", 1, "zh", toolCall("c1", "get_ai500_list", "{}"))
if !strings.Contains(raw, "BTCUSDT") {
t.Fatalf("dispatch failed, got %q", raw)
}
}
func TestAgentToolsIncludeAI500(t *testing.T) {
for _, tool := range agentTools() {
if tool.Function.Name == "get_ai500_list" {
return
}
}
t.Fatal("get_ai500_list missing from agent toolset")
}