mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-18 23:11:12 +02:00
test: Add Vitest configuration and initial tests for the DexScreener connect form.
This commit is contained in:
parent
828d7d695a
commit
fd9eddf7fa
11 changed files with 1480 additions and 116 deletions
|
|
@ -38,3 +38,75 @@ Backend của SurfSense là một ứng dụng **Python FastAPI** mạnh mẽ,
|
||||||
- Nếu là tác vụ AI: Đẩy vào LangGraph runner để streaming phản hồi.
|
- Nếu là tác vụ AI: Đẩy vào LangGraph runner để streaming phản hồi.
|
||||||
- Nếu là tác vụ dài (Ingestion): Đẩy job vào Redis queue cho Celery.
|
- Nếu là tác vụ dài (Ingestion): Đẩy job vào Redis queue cho Celery.
|
||||||
5. **Response**: Trả về JSON hoặc Streaming Response (SSE).
|
5. **Response**: Trả về JSON hoặc Streaming Response (SSE).
|
||||||
|
|
||||||
|
## Critical RAG Pipeline Fix (Feb 2026)
|
||||||
|
|
||||||
|
### DexScreener Connector Integration
|
||||||
|
|
||||||
|
**Issue Discovered**: DexScreener connector was successfully implemented and indexed data into `search_space_id = 7`, but the LLM could not retrieve this data when users asked about crypto prices.
|
||||||
|
|
||||||
|
**Root Cause**: Missing connector mapping in `_CONNECTOR_TYPE_TO_SEARCHABLE` dictionary.
|
||||||
|
|
||||||
|
**File**: `surfsense_backend/app/agents/new_chat/chat_deepagent.py`
|
||||||
|
|
||||||
|
**The Problem**:
|
||||||
|
```python
|
||||||
|
# BEFORE (Missing mapping)
|
||||||
|
_CONNECTOR_TYPE_TO_SEARCHABLE = {
|
||||||
|
"GMAIL": "GMAIL",
|
||||||
|
"GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE",
|
||||||
|
"SLACK_CONNECTOR": "SLACK",
|
||||||
|
# ... other connectors ...
|
||||||
|
# ❌ DEXSCREENER_CONNECTOR was MISSING
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
1. `connector_service.get_available_connectors()` returned DexScreener connector type
|
||||||
|
2. `_map_connectors_to_searchable_types()` could not find mapping → ignored DexScreener
|
||||||
|
3. LLM's tool description didn't mention DexScreener as available
|
||||||
|
4. LLM never searched DexScreener data, responded "can't see price data"
|
||||||
|
|
||||||
|
**The Fix**:
|
||||||
|
```python
|
||||||
|
# AFTER (Fixed)
|
||||||
|
_CONNECTOR_TYPE_TO_SEARCHABLE = {
|
||||||
|
"GMAIL": "GMAIL",
|
||||||
|
"GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE",
|
||||||
|
"SLACK_CONNECTOR": "SLACK",
|
||||||
|
# ... other connectors ...
|
||||||
|
"DEXSCREENER_CONNECTOR": "DEXSCREENER_CONNECTOR", # ✅ Added
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification**:
|
||||||
|
- User query: *"What's the current price of WETH?"*
|
||||||
|
- LLM successfully retrieved: ~$2,442 USD with DexScreener citations
|
||||||
|
- Citations linked to indexed trading pairs with metadata (chain, DEX, liquidity, volume)
|
||||||
|
|
||||||
|
**Lesson Learned**: When adding new connectors, **ALWAYS** update the `_CONNECTOR_TYPE_TO_SEARCHABLE` mapping to enable RAG retrieval. This is a critical step that's easy to miss during implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connector Architecture Pattern
|
||||||
|
|
||||||
|
### Adding New Connectors (Best Practices)
|
||||||
|
|
||||||
|
Khi thêm connector mới, cần update **4 locations**:
|
||||||
|
|
||||||
|
1. **Connector Class** (`app/connectors/`)
|
||||||
|
- Implement data fetching logic
|
||||||
|
- Format data to markdown for indexing
|
||||||
|
|
||||||
|
2. **Database Enum** (`app/db.py`)
|
||||||
|
- Add to `SearchSourceConnectorType` enum
|
||||||
|
|
||||||
|
3. **API Routes** (`app/routes/`)
|
||||||
|
- Create add/delete/test endpoints
|
||||||
|
|
||||||
|
4. **RAG Mapping** (`app/agents/new_chat/chat_deepagent.py`) ⚠️ **CRITICAL**
|
||||||
|
- Add to `_CONNECTOR_TYPE_TO_SEARCHABLE` dictionary
|
||||||
|
- **Failure to do this = LLM cannot access connector data**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@
|
||||||
- 🎫 **Jira** - Tìm kiếm tickets
|
- 🎫 **Jira** - Tìm kiếm tickets
|
||||||
- 📚 **Confluence** - Tìm kiếm wiki pages
|
- 📚 **Confluence** - Tìm kiếm wiki pages
|
||||||
- 🗂️ **Microsoft Teams** - Tìm kiếm chats và files
|
- 🗂️ **Microsoft Teams** - Tìm kiếm chats và files
|
||||||
|
- 💰 **DexScreener** - Theo dõi giá token crypto và trading pairs
|
||||||
|
|
||||||
**Tổng cộng:** SurfSense hỗ trợ **26+ connectors** khác nhau!
|
**Tổng cộng:** SurfSense hỗ trợ **27+ connectors** khác nhau!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -238,6 +239,30 @@ User → SurfSense → Notion OAuth → Access Token → SurfSense
|
||||||
- Chỉ hoạt động khi SurfSense chạy self-hosted
|
- Chỉ hoạt động khi SurfSense chạy self-hosted
|
||||||
- Truy cập trực tiếp vào local file system
|
- Truy cập trực tiếp vào local file system
|
||||||
|
|
||||||
|
### 5. API-Based (No Authentication)
|
||||||
|
|
||||||
|
**Ví dụ:** DexScreener Connector
|
||||||
|
|
||||||
|
- Không cần OAuth hay API key (public API)
|
||||||
|
- User chỉ cần cấu hình tokens muốn theo dõi
|
||||||
|
- Ưu điểm:
|
||||||
|
- Setup cực kỳ đơn giản (không cần đăng ký API key)
|
||||||
|
- Miễn phí hoàn toàn
|
||||||
|
- Real-time data từ public blockchain
|
||||||
|
- Nhược điểm:
|
||||||
|
- Bị giới hạn rate limit của public API
|
||||||
|
- Không có personalized data
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
```
|
||||||
|
User → Nhập token addresses → SurfSense → DexScreener Public API → Token Price Data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case:**
|
||||||
|
- Theo dõi giá crypto tokens (WETH, USDC, etc.)
|
||||||
|
- Phân tích trading pairs trên các DEX
|
||||||
|
- AI có thể trả lời: *"What's the current price of WETH?"*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Cấu Hình Connector
|
## 🛠️ Cấu Hình Connector
|
||||||
|
|
@ -278,6 +303,24 @@ Mỗi connector có các settings:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**DexScreener:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"chain": "ethereum",
|
||||||
|
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||||
|
"name": "WETH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "bsc",
|
||||||
|
"address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
||||||
|
"name": "WBNB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💡 Use Cases
|
## 💡 Use Cases
|
||||||
|
|
@ -320,6 +363,23 @@ Mỗi connector có các settings:
|
||||||
3. Slack discussion về issue
|
3. Slack discussion về issue
|
||||||
4. Confluence: Payment API Architecture
|
4. Confluence: Payment API Architecture
|
||||||
|
|
||||||
|
### 3. Crypto Trader
|
||||||
|
|
||||||
|
**Scenario:** Theo dõi giá token và phân tích market trends.
|
||||||
|
|
||||||
|
**Connectors kết nối:**
|
||||||
|
- DexScreener (token prices và trading pairs)
|
||||||
|
- Twitter/X (crypto news - nếu có connector)
|
||||||
|
- Notion (trading notes)
|
||||||
|
|
||||||
|
**Search query trong AI Chat:** *"What's the current price of WETH and how has it changed in the last 24 hours?"*
|
||||||
|
|
||||||
|
**Kết quả:**
|
||||||
|
- AI trả lời với real-time price data từ DexScreener
|
||||||
|
- Hiển thị price changes (5m, 1h, 24h)
|
||||||
|
- Liquidity và volume information
|
||||||
|
- Citations link đến DexScreener pairs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚨 Lưu Ý Quan Trọng
|
## 🚨 Lưu Ý Quan Trọng
|
||||||
|
|
|
||||||
|
|
@ -796,6 +796,76 @@ async def index_connector_data(connector: SearchSourceConnector):
|
||||||
# ... other connector types
|
# ... other connector types
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ CRITICAL: Enable RAG Retrieval
|
||||||
|
|
||||||
|
**This is the most commonly missed step when adding new connectors!**
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
Even if your connector successfully:
|
||||||
|
1. ✅ Stores data in the database
|
||||||
|
2. ✅ Indexes data into vector store
|
||||||
|
3. ✅ Shows up in the UI
|
||||||
|
|
||||||
|
The LLM **WILL NOT** be able to retrieve this data unless you add the connector to the RAG mapping.
|
||||||
|
|
||||||
|
### The Fix
|
||||||
|
|
||||||
|
**File:** `surfsense_backend/app/agents/new_chat/chat_deepagent.py`
|
||||||
|
|
||||||
|
**Add your connector to `_CONNECTOR_TYPE_TO_SEARCHABLE`:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
_CONNECTOR_TYPE_TO_SEARCHABLE = {
|
||||||
|
"GMAIL": "GMAIL",
|
||||||
|
"GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE",
|
||||||
|
"SLACK_CONNECTOR": "SLACK",
|
||||||
|
"NOTION_CONNECTOR": "NOTION",
|
||||||
|
# ... other connectors ...
|
||||||
|
|
||||||
|
# ✅ ADD YOUR NEW CONNECTOR HERE
|
||||||
|
"DEXSCREENER_CONNECTOR": "DEXSCREENER_CONNECTOR",
|
||||||
|
"YOUR_CONNECTOR": "YOUR_CONNECTOR", # Example
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why This Matters
|
||||||
|
|
||||||
|
This mapping is used by `_map_connectors_to_searchable_types()` to:
|
||||||
|
1. Build the list of available search spaces for the LLM
|
||||||
|
2. Include connector types in the tool description
|
||||||
|
3. Enable the LLM to search your connector's data
|
||||||
|
|
||||||
|
**Without this mapping:**
|
||||||
|
- LLM won't know your connector exists
|
||||||
|
- LLM can't search your indexed data
|
||||||
|
- Users will get "I don't have access to that data" responses
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
After adding the mapping, test with a user query:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example for DexScreener
|
||||||
|
curl -X POST http://localhost:8000/api/chat \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"message": "What is the current price of WETH?",
|
||||||
|
"space_id": 7
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** LLM retrieves data and provides answer with citations.
|
||||||
|
|
||||||
|
**If failed:** Check that:
|
||||||
|
1. Connector is in `_CONNECTOR_TYPE_TO_SEARCHABLE`
|
||||||
|
2. Connector type matches exactly (case-sensitive)
|
||||||
|
3. Data is indexed in the correct `search_space_id`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
### 1. **Error Handling**
|
### 1. **Error Handling**
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,11 @@ SurfSense cho phép bạn kết nối với **26+ ứng dụng bên ngoài** nh
|
||||||
- **Kết nối Google Drive:** Tìm files và documents ngay trong SurfSense
|
- **Kết nối Google Drive:** Tìm files và documents ngay trong SurfSense
|
||||||
- **Kết nối Slack:** Tìm conversations và shared files từ workspace
|
- **Kết nối Slack:** Tìm conversations và shared files từ workspace
|
||||||
- **Kết nối Notion:** Tìm kiếm trong pages và databases
|
- **Kết nối Notion:** Tìm kiếm trong pages và databases
|
||||||
|
- **Kết nối DexScreener:** Theo dõi giá crypto tokens real-time
|
||||||
|
- Không cần API key
|
||||||
|
- Chỉ cần nhập token addresses muốn theo dõi
|
||||||
|
- AI có thể trả lời: *"What's the current price of WETH?"*
|
||||||
|
- Xem trading pairs, liquidity, volume, price changes
|
||||||
|
|
||||||
**Quản lý Connectors:**
|
**Quản lý Connectors:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
**Story Title**: DexScreener Connector Integration
|
**Story Title**: DexScreener Connector Integration
|
||||||
**Epic**: SurfSense Connectors Enhancement
|
**Epic**: SurfSense Connectors Enhancement
|
||||||
**Priority**: High
|
**Priority**: High
|
||||||
**Status**: Ready for Development
|
**Status**: ✅ Implementation Complete (2026-02-01)
|
||||||
**Created**: 2026-01-31
|
**Created**: 2026-01-31
|
||||||
|
|
||||||
## 🎯 User Story
|
## 🎯 User Story
|
||||||
|
|
@ -27,63 +27,63 @@ This connector will integrate with SurfSense's existing connector architecture,
|
||||||
|
|
||||||
## ✅ Acceptance Criteria
|
## ✅ Acceptance Criteria
|
||||||
|
|
||||||
### AC1: Connector Configuration
|
### AC1: Connector Configuration ✅
|
||||||
- [ ] User can add DexScreener connector via API endpoint
|
- [x] User can add DexScreener connector via API endpoint
|
||||||
- [ ] User can configure multiple tokens to track (up to 50)
|
- [x] User can configure multiple tokens to track (up to 50)
|
||||||
- [ ] Each token config includes: chain ID, token address, optional name
|
- [x] Each token config includes: chain ID, token address, optional name
|
||||||
- [ ] User can update connector configuration
|
- [x] User can update connector configuration
|
||||||
- [ ] User can delete connector
|
- [x] User can delete connector
|
||||||
- [ ] Configuration is persisted in database
|
- [x] Configuration is persisted in database
|
||||||
|
|
||||||
### AC2: API Integration
|
### AC2: API Integration ✅
|
||||||
- [ ] Connector successfully calls DexScreener API endpoints
|
- [x] Connector successfully calls DexScreener API endpoints
|
||||||
- [ ] Handles rate limits (300 req/min) appropriately
|
- [x] Handles rate limits (300 req/min) appropriately
|
||||||
- [ ] Implements retry logic with exponential backoff
|
- [x] Implements retry logic with exponential backoff
|
||||||
- [ ] Validates API responses
|
- [x] Validates API responses
|
||||||
- [ ] Handles API errors gracefully (network failures, invalid data, etc.)
|
- [x] Handles API errors gracefully (network failures, invalid data, etc.)
|
||||||
|
|
||||||
### AC3: Data Indexing
|
### AC3: Data Indexing ✅
|
||||||
- [ ] Fetches trading pairs for all configured tokens
|
- [x] Fetches trading pairs for all configured tokens
|
||||||
- [ ] Converts pair data to markdown format with all key metrics:
|
- [x] Converts pair data to markdown format with all key metrics:
|
||||||
- Token information (names, symbols, addresses)
|
- Token information (names, symbols, addresses)
|
||||||
- Price data (USD, native, 24h changes)
|
- Price data (USD, native, 24h changes)
|
||||||
- Volume metrics (24h, 6h, 1h)
|
- Volume metrics (24h, 6h, 1h)
|
||||||
- Liquidity information
|
- Liquidity information
|
||||||
- Market cap and FDV
|
- Market cap and FDV
|
||||||
- Transaction counts
|
- Transaction counts
|
||||||
- [ ] Generates unique identifier hash for each pair
|
- [x] Generates unique identifier hash for each pair
|
||||||
- [ ] Generates content hash to detect changes
|
- [x] Generates content hash to detect changes
|
||||||
- [ ] Creates document chunks for vector search
|
- [x] Creates document chunks for vector search
|
||||||
- [ ] Generates embeddings using configured LLM
|
- [x] Generates embeddings using configured LLM
|
||||||
- [ ] Stores documents in database with proper metadata
|
- [x] Stores documents in database with proper metadata
|
||||||
- [ ] Updates existing documents when content changes
|
- [x] Updates existing documents when content changes
|
||||||
- [ ] Skips unchanged documents
|
- [x] Skips unchanged documents
|
||||||
|
|
||||||
### AC4: Periodic Indexing
|
### AC4: Periodic Indexing ✅
|
||||||
- [ ] Indexing task is registered with Celery
|
- [x] Indexing task is registered with Celery
|
||||||
- [ ] Periodic scheduler triggers indexing (default: 60 min interval)
|
- [x] Periodic scheduler triggers indexing (default: 60 min interval)
|
||||||
- [ ] Manual indexing can be triggered via API
|
- [x] Manual indexing can be triggered via API
|
||||||
- [ ] Last indexed timestamp is updated after successful indexing
|
- [x] Last indexed timestamp is updated after successful indexing
|
||||||
- [ ] Indexing errors are logged properly
|
- [x] Indexing errors are logged properly
|
||||||
- [ ] Failed indexing doesn't block future attempts
|
- [x] Failed indexing doesn't block future attempts
|
||||||
|
|
||||||
### AC5: Search Integration
|
### AC5: Search Integration ✅
|
||||||
- [ ] Indexed DexScreener data appears in search results
|
- [x] Indexed DexScreener data appears in search results
|
||||||
- [ ] Documents are searchable by:
|
- [x] Documents are searchable by:
|
||||||
- Token names and symbols
|
- Token names and symbols
|
||||||
- Pair addresses
|
- Pair addresses
|
||||||
- Chain IDs
|
- Chain IDs
|
||||||
- DEX names
|
- DEX names
|
||||||
- Price ranges
|
- Price ranges
|
||||||
- Volume metrics
|
- Volume metrics
|
||||||
- [ ] Search results include relevant metadata
|
- [x] Search results include relevant metadata
|
||||||
- [ ] Vector search returns semantically similar pairs
|
- [x] Vector search returns semantically similar pairs
|
||||||
|
|
||||||
### AC6: AI Chat Integration
|
### AC6: AI Chat Integration ✅
|
||||||
- [ ] AI chat can access DexScreener data as context
|
- [x] AI chat can access DexScreener data as context
|
||||||
- [ ] Chat responses include relevant trading pair information
|
- [x] Chat responses include relevant trading pair information
|
||||||
- [ ] Citations link to DexScreener URLs
|
- [x] Citations link to DexScreener URLs
|
||||||
- [ ] Metadata is properly formatted in chat responses
|
- [x] Metadata is properly formatted in chat responses
|
||||||
|
|
||||||
## 🏗️ Technical Implementation
|
## 🏗️ Technical Implementation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
**Story Title**: DexScreener Connector Frontend UI
|
**Story Title**: DexScreener Connector Frontend UI
|
||||||
**Epic**: SurfSense Connectors Enhancement
|
**Epic**: SurfSense Connectors Enhancement
|
||||||
**Priority**: High
|
**Priority**: High
|
||||||
**Status**: Ready for Development
|
**Status**: ✅ Implementation Complete (2026-02-01)
|
||||||
**Created**: 2026-01-31
|
**Created**: 2026-01-31
|
||||||
**Depends On**: Story 1.1 (Backend API)
|
**Depends On**: Story 1.1 (Backend API)
|
||||||
|
|
||||||
|
|
@ -29,21 +29,21 @@ This story implements the user-facing components following SurfSense's establish
|
||||||
|
|
||||||
## ✅ Acceptance Criteria
|
## ✅ Acceptance Criteria
|
||||||
|
|
||||||
### AC1: Connect Form Component
|
### AC1: Connect Form Component ✅
|
||||||
- [ ] User can access DexScreener connector from connector popup
|
- [x] User can access DexScreener connector from connector popup
|
||||||
- [ ] Form includes connector name field (min 3 characters)
|
- [x] Form includes connector name field (min 3 characters)
|
||||||
- [ ] User can add multiple tokens (up to 50)
|
- [x] User can add multiple tokens (up to 50)
|
||||||
- [ ] Each token has: chain selector, token address input, optional name
|
- [x] Each token has: chain selector, token address input, optional name
|
||||||
- [ ] Form validates token addresses (40-character hex)
|
- [x] Form validates token addresses (40-character hex)
|
||||||
- [ ] User can remove tokens from list
|
- [x] User can remove tokens from list
|
||||||
- [ ] Date range selector for initial indexing
|
- [x] Date range selector for initial indexing
|
||||||
- [ ] Periodic sync toggle with frequency selector
|
- [x] Periodic sync toggle with frequency selector
|
||||||
- [ ] "What you get" benefits section displayed
|
- [x] "What you get" benefits section displayed
|
||||||
- [ ] Form submits to backend API endpoint
|
- [x] Form submits to backend API endpoint
|
||||||
|
|
||||||
### AC2: Token Management UI
|
### AC2: Token Management UI ✅
|
||||||
- [ ] Dynamic token list with add/remove buttons
|
- [x] Dynamic token list with add/remove buttons
|
||||||
- [ ] Chain selector dropdown with popular chains:
|
- [x] Chain selector dropdown with popular chains:
|
||||||
- Ethereum
|
- Ethereum
|
||||||
- BSC (Binance Smart Chain)
|
- BSC (Binance Smart Chain)
|
||||||
- Polygon
|
- Polygon
|
||||||
|
|
@ -52,42 +52,42 @@ This story implements the user-facing components following SurfSense's establish
|
||||||
- Base
|
- Base
|
||||||
- Avalanche
|
- Avalanche
|
||||||
- Solana
|
- Solana
|
||||||
- [ ] Token address input with validation
|
- [x] Token address input with validation
|
||||||
- [ ] Optional token name/label field
|
- [x] Optional token name/label field
|
||||||
- [ ] Visual feedback for validation errors
|
- [x] Visual feedback for validation errors
|
||||||
- [ ] Responsive design for mobile/desktop
|
- [x] Responsive design for mobile/desktop
|
||||||
|
|
||||||
### AC3: Connector Config Component
|
### AC3: Connector Config Component ✅
|
||||||
- [ ] Edit mode for existing connector
|
- [x] Edit mode for existing connector
|
||||||
- [ ] Update connector name
|
- [x] Update connector name
|
||||||
- [ ] Add/remove tokens from tracked list
|
- [x] Add/remove tokens from tracked list
|
||||||
- [ ] View current token configuration
|
- [x] View current token configuration
|
||||||
- [ ] Save changes button
|
- [x] Save changes button
|
||||||
- [ ] Cancel/discard changes option
|
- [x] Cancel/discard changes option
|
||||||
|
|
||||||
### AC4: Connector Benefits
|
### AC4: Connector Benefits ✅
|
||||||
- [ ] Display benefits list in connect form
|
- [x] Display benefits list in connect form
|
||||||
- [ ] Benefits include:
|
- [x] Benefits include:
|
||||||
- "Real-time cryptocurrency trading pair data"
|
- "Real-time cryptocurrency trading pair data"
|
||||||
- "Track prices, volume, and liquidity across multiple DEXs"
|
- "Track prices, volume, and liquidity across multiple DEXs"
|
||||||
- "Search and analyze token market data with AI"
|
- "Search and analyze token market data with AI"
|
||||||
- "Monitor your crypto portfolio with automated updates"
|
- "Monitor your crypto portfolio with automated updates"
|
||||||
- "Access historical price and volume trends"
|
- "Access historical price and volume trends"
|
||||||
|
|
||||||
### AC5: Documentation
|
### AC5: Documentation ✅
|
||||||
- [ ] MDX documentation file created
|
- [x] MDX documentation file created
|
||||||
- [ ] Setup guide with screenshots
|
- [x] Setup guide with screenshots
|
||||||
- [ ] Token configuration instructions
|
- [x] Token configuration instructions
|
||||||
- [ ] Chain selection guide
|
- [x] Chain selection guide
|
||||||
- [ ] Troubleshooting section
|
- [x] Troubleshooting section
|
||||||
- [ ] Link to DexScreener API docs
|
- [x] Link to DexScreener API docs
|
||||||
|
|
||||||
### AC6: Integration
|
### AC6: Integration ✅
|
||||||
- [ ] Connector registered in connector registry
|
- [x] Connector registered in connector registry
|
||||||
- [ ] Icon/logo added to public assets
|
- [x] Icon/logo added to public assets
|
||||||
- [ ] Connector appears in connector list
|
- [x] Connector appears in connector list
|
||||||
- [ ] Form properly integrated with connector popup
|
- [x] Form properly integrated with connector popup
|
||||||
- [ ] Config component properly integrated
|
- [x] Config component properly integrated
|
||||||
|
|
||||||
## 🏗️ Technical Implementation
|
## 🏗️ Technical Implementation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { DexScreenerConnectForm } from '@/components/assistant-ui/connector-popup/connect-forms/components/dexscreener-connect-form';
|
||||||
|
|
||||||
|
// Mock the form submission
|
||||||
|
const mockOnSubmit = vi.fn();
|
||||||
|
const mockOnBack = vi.fn();
|
||||||
|
|
||||||
|
describe('DexScreenerConnectForm', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Initial Rendering', () => {
|
||||||
|
it('should render the form with all required fields', () => {
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
// Check for connector name input
|
||||||
|
expect(screen.getByLabelText('Connector Name')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check for benefits section
|
||||||
|
expect(screen.getByText('No API Key Required')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check for add token button
|
||||||
|
expect(screen.getByRole('button', { name: /add token/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the DexScreener info alert', () => {
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('No API Key Required')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/DexScreener API is public and free to use/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form Validation', () => {
|
||||||
|
it('should accept valid connector name', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
const nameInput = screen.getByLabelText('Connector Name');
|
||||||
|
await user.clear(nameInput);
|
||||||
|
await user.type(nameInput, 'Valid Name');
|
||||||
|
|
||||||
|
// Should accept valid name
|
||||||
|
expect(nameInput).toHaveValue('Valid Name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid Ethereum address format', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
const addressInput = screen.getByLabelText('Token Address');
|
||||||
|
const validAddress = '0x' + 'a'.repeat(40);
|
||||||
|
await user.clear(addressInput);
|
||||||
|
await user.type(addressInput, validAddress);
|
||||||
|
|
||||||
|
// Should accept valid address
|
||||||
|
expect(addressInput).toHaveValue(validAddress);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Token Management', () => {
|
||||||
|
it('should add a new token when Add Token button is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
// Initially should have 1 token (default)
|
||||||
|
expect(screen.getByText('Token #1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const addTokenButton = screen.getByRole('button', { name: /add token/i });
|
||||||
|
await user.click(addTokenButton);
|
||||||
|
|
||||||
|
// Should now have 2 tokens
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Token #2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a token when remove button is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
// Add a second token first
|
||||||
|
const addTokenButton = screen.getByRole('button', { name: /add token/i });
|
||||||
|
await user.click(addTokenButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Token #2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove the second token
|
||||||
|
const removeButtons = screen.getAllByRole('button', { name: '' }); // X buttons have no text
|
||||||
|
const lastRemoveButton = removeButtons[removeButtons.length - 1];
|
||||||
|
await user.click(lastRemoveButton);
|
||||||
|
|
||||||
|
// Token #2 should be gone
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Token #2')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow adding multiple tokens up to the limit', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
const addTokenButton = screen.getByRole('button', { name: /add token/i });
|
||||||
|
|
||||||
|
// Add 2 more tokens (already have 1)
|
||||||
|
await user.click(addTokenButton);
|
||||||
|
await user.click(addTokenButton);
|
||||||
|
|
||||||
|
// Should have 3 tokens total
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Token #3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('3 / 50 tokens')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable Add Token button when maximum tokens (50) are reached', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
const addTokenButton = screen.getByRole('button', { name: /add token/i });
|
||||||
|
|
||||||
|
// Add 49 more tokens (already have 1) - this is slow but necessary
|
||||||
|
for (let i = 0; i < 49; i++) {
|
||||||
|
await user.click(addTokenButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button should be disabled and show max message
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(addTokenButton).toBeDisabled();
|
||||||
|
expect(screen.getByText(/maximum reached/i)).toBeInTheDocument();
|
||||||
|
}, { timeout: 10000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Chain Selection', () => {
|
||||||
|
it('should display supported chains in the dropdown', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
// Click on the chain selector
|
||||||
|
const chainSelect = screen.getByRole('combobox', { name: /chain/i });
|
||||||
|
await user.click(chainSelect);
|
||||||
|
|
||||||
|
// Check for supported chains
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('option', { name: /ethereum/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('option', { name: /bsc/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('option', { name: /polygon/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow chain selection', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
const chainSelect = screen.getByRole('combobox', { name: /chain/i });
|
||||||
|
await user.click(chainSelect);
|
||||||
|
|
||||||
|
// Select BSC
|
||||||
|
const bscOption = screen.getByRole('option', { name: /bsc/i });
|
||||||
|
await user.click(bscOption);
|
||||||
|
|
||||||
|
// Verify dropdown closed (option should not be visible anymore)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByRole('option', { name: /bsc/i })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form Submission', () => {
|
||||||
|
it('should call onSubmit with valid data', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DexScreenerConnectForm onSubmit={mockOnSubmit} onBack={mockOnBack} isSubmitting={false} />);
|
||||||
|
|
||||||
|
// Fill connector name
|
||||||
|
const nameInput = screen.getByLabelText('Connector Name');
|
||||||
|
await user.clear(nameInput);
|
||||||
|
await user.type(nameInput, 'My Connector');
|
||||||
|
|
||||||
|
// Fill token address (first token already exists)
|
||||||
|
const addressInput = screen.getByLabelText('Token Address');
|
||||||
|
const validAddress = '0x' + 'a'.repeat(40);
|
||||||
|
await user.type(addressInput, validAddress);
|
||||||
|
|
||||||
|
// Find and click submit button
|
||||||
|
const buttons = screen.getAllByRole('button');
|
||||||
|
const submitButton = buttons.find(btn => btn.textContent?.includes('Connect'));
|
||||||
|
|
||||||
|
if (submitButton) {
|
||||||
|
await user.click(submitButton);
|
||||||
|
|
||||||
|
// Verify onSubmit was called
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockOnSubmit).toHaveBeenCalled();
|
||||||
|
}, { timeout: 3000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -18,7 +18,10 @@
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"format:fix": "npx @biomejs/biome check --fix"
|
"format:fix": "npx @biomejs/biome check --fix",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:coverage": "vitest --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/react": "^1.2.12",
|
"@ai-sdk/react": "^1.2.12",
|
||||||
|
|
@ -114,18 +117,25 @@
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^20.19.9",
|
"@types/node": "^20.19.9",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"@vitest/ui": "^3.2.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"drizzle-kit": "^0.31.5",
|
"drizzle-kit": "^0.31.5",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-config-next": "15.2.0",
|
"eslint-config-next": "15.2.0",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
929
surfsense_web/pnpm-lock.yaml
generated
929
surfsense_web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
29
surfsense_web/vitest.config.ts
Normal file
29
surfsense_web/vitest.config.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './vitest.setup.ts',
|
||||||
|
css: true,
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'vitest.setup.ts',
|
||||||
|
'**/*.config.ts',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/types/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
43
surfsense_web/vitest.setup.ts
Normal file
43
surfsense_web/vitest.setup.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { cleanup } from '@testing-library/react';
|
||||||
|
import { afterEach, vi } from 'vitest';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Cleanup after each test
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock ResizeObserver (required for Radix UI components)
|
||||||
|
global.ResizeObserver = class ResizeObserver {
|
||||||
|
observe() { }
|
||||||
|
unobserve() { }
|
||||||
|
disconnect() { }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock PointerEvent APIs for Radix UI Select
|
||||||
|
HTMLElement.prototype.hasPointerCapture = vi.fn(() => false);
|
||||||
|
HTMLElement.prototype.setPointerCapture = vi.fn();
|
||||||
|
HTMLElement.prototype.releasePointerCapture = vi.fn();
|
||||||
|
HTMLElement.prototype.scrollIntoView = vi.fn();
|
||||||
|
|
||||||
|
// Mock Next.js router
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: vi.fn(),
|
||||||
|
replace: vi.fn(),
|
||||||
|
prefetch: vi.fn(),
|
||||||
|
back: vi.fn(),
|
||||||
|
pathname: '/',
|
||||||
|
query: {},
|
||||||
|
}),
|
||||||
|
usePathname: () => '/',
|
||||||
|
useSearchParams: () => new URLSearchParams(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock Next.js Image component
|
||||||
|
vi.mock('next/image', () => ({
|
||||||
|
default: (props: any) => {
|
||||||
|
return React.createElement('img', props);
|
||||||
|
},
|
||||||
|
}));
|
||||||
Loading…
Add table
Add a link
Reference in a new issue