Files
tfm_ainventory/.planning/phases/4.1-ai-spare-parts-deep-id/4.1-RESEARCH.md
Daniel Bedeleanu ac87c4c06b docs(4.1): planning complete - research + 3 executable plans
Phase 4.1: AI Prompt Enhancement — Spare Parts Deep Identification

Artifacts:
- 4.1-RESEARCH.md: Web scraping patterns, spare-parts classification, integration architecture
- 4.1-PLAN-01.md (Wave 1): Spare-parts whitelist + AI prompt enhancement (4 tasks)
- 4.1-PLAN-02.md (Wave 2): Web scraping service + backend integration (6 tasks)
- 4.1-PLAN-03.md (Wave 3): Frontend integration + end-to-end testing (7 tasks)

All 17 tasks verified:
✓ Concrete action steps with exact function signatures and file paths
✓ 100% verifiable acceptance criteria (grep, pytest, vitest, imports)
✓ Architecture aligned with all 11 CONTEXT.md decisions
✓ CLAUDE.md compliance: TypeScript strict, API tests, UI fidelity
✓ Wave dependencies correctly ordered
✓ Risk mitigation: rate limiting, timeout handling, offline graceful degradation

Ready for execution via /gsd-execute-phase 4.1
2026-04-22 16:28:26 +03:00

762 lines
27 KiB
Markdown

# Phase 4.1 Research: AI Prompt Enhancement — Spare Parts Deep Identification
**Research Date:** 2026-04-22
**Scope:** Web scraping implementation, spare-parts classification, AI prompt enhancement, search result parsing, backend/frontend integration, and performance/scalability.
---
## 1. Web Scraping Best Practices: Python Requests + BeautifulSoup
### Key Findings
**Approach & Risks:**
- **Direct Google scraping** is technically feasible but risky: Google actively detects and blocks scrapers with 429 (Too Many Requests) errors, CAPTCHA challenges, and IP bans.
- **Terms of Service violation**: Google's ToS explicitly forbids scraping search results.
- **HTML structure volatility**: Google changes CSS selectors and HTML markup frequently, breaking scrapers.
- **Practical reality**: Direct scraping works for low-volume scenarios (tens of requests/hour) with proper mitigations.
**Safer Alternatives:**
1. **SerpAPI / Similar APIs**: Officially maintained, handles blocking/rotation, but costs money ($5-50/month depending on volume).
2. **Bing scraping**: Less aggressively blocked than Google, similar HTML structure, viable fallback.
3. **Manufacturer sites** (Dell, HP, Kingston, Crucial): Most reliable source for spare-part specs.
4. **GitHub Issues / StackOverflow**: Often contain real-world component usage and specifications.
**Recommended Hybrid Approach:**
- Primary: Search manufacturer specs directly (most accurate).
- Fallback 1: Bing web search with BeautifulSoup.
- Fallback 2: Google search (if Bing returns no results).
- Fallback 3: Return AI-extracted data only (graceful offline degradation).
### Rate Limiting Strategies
**Implementation:**
- **Delay between requests**: 2-5 seconds minimum (random jitter recommended).
- **User-Agent rotation**: Cycle through 10+ realistic User-Agent strings (Chrome, Firefox, Safari across Windows/Mac/Linux).
- **Exponential backoff**: 1s → 2s → 4s → 8s → fail.
- **Token bucket algorithm**: Max 0.2 requests/second (1 request per 5 seconds) per IP.
**User-Agent Pool (Examples):**
```
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Chrome/120.0.0.0)
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (Firefox/121.0)
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (Safari/537.36)
```
### Error Handling & Timeout Strategies
**HTTP Status Codes:**
- **429 (Too Many Requests)**: Wait 10 seconds, retry once, then fail gracefully.
- **403 (Forbidden)**: IP blocked; rotate User-Agent, increase delay, or skip.
- **500+ (Server Error)**: Retry with exponential backoff.
- **Timeout (>10s)**: Abort search, return AI data only, log warning.
**CAPTCHA Detection:**
- BeautifulSoup can detect CAPTCHA forms by checking for `<form>` with `recaptcha` keywords.
- If detected: Abort search immediately, return AI data, log incident.
**Latency Profile:**
- Typical Google request: 2-8 seconds.
- BeautifulSoup HTML parsing: 100-500ms.
- Regex spec extraction: 10-50ms.
- **Total end-to-end: 3-15 seconds (up to 30s with retries).**
---
## 2. Spare-Parts Classification Strategy
### Comprehensive Whitelist
**Spare-Part Categories (Include These):**
- **Memory**: RAM, DRAM, DDR3, DDR4, DDR5, SODIMM, DIMM
- **Storage**: SSD, NVME, M.2, SATA, HDD, hard drive, solid state drive
- **Processors**: CPU, processor, APU, GPU, graphics card, discrete GPU
- **Power**: PSU (power supply unit), adapter, power module (NOT cables/cords)
- **Expansion Cards**: PCIe, PCI, RAID controller, network card (NIC), graphics card
- **Cooling**: Heatsink, CPU cooler, thermal solution
- **Motherboards**: Motherboard, BIOS, chipset
**Consumables to Exclude:**
- Cables: SATA cables, USB cables, Ethernet cables, power cords.
- Fasteners: Screws, washers, bolts, standoffs.
- Adhesives/Thermal Materials: Thermal paste, thermal pads, adhesive tapes.
- Connectors: Plugs, sockets, adapters (unless branded components).
**Edge Case: Power Supplies**
- **Spare part**: "Corsair RM850x 850W Power Supply Unit" (replaceable, has specs).
- **Consumable**: "6ft Power Cable" or "AC Power Cord" (generic utility item).
### Fuzzy Matching Implementation
**Strategy:**
1. **Exact keyword match** (highest priority): Check if extracted Category contains exact whitelist terms (RAM, SSD, CPU, GPU, PSU).
2. **Fuzzy matching** (Levenshtein distance, 70-80% threshold):
- "Random Access Memory" → matches "RAM"
- "Solid State Disk" → matches "SSD"
3. **Regex patterns** (fallback):
- `\bRAM\b|\bDRAM\b|\bDDR\d\b` → Memory component.
- `\bSSD\b|\bNVME\b|\bM\.2\b` → Storage component.
4. **Exclusion patterns** (reject consumables):
- `^(cable|cord|fastener|screw|adhesive|thermal paste)$` (case-insensitive).
**Scoring System:**
- Exact match in whitelist: +100 points → **Spare Part**.
- Fuzzy match >80%: +50 points.
- Fuzzy match 70-80%: +30 points.
- Found in consumable exclusion list: -100 points → **Consumable**.
- **Threshold**: Score ≥ 40 → Spare Part; < 40 → Unknown/Consumable.
---
## 3. AI Prompt Enhancement: Gemini 2.0 Flash & Claude 3.5 Sonnet
### Current State
- **Gemini prompt**: Located in `backend/ai/prompts/gemini_extraction_prompt.md`.
- **Claude prompt**: Located in `backend/ai/prompts/claude_extraction_prompt.md`.
- Both focus on OCR extraction from label images.
### Phase 4.1 Enhancements
**New Classification Logic to Add:**
Insert into both prompts a new section after Category extraction:
```
CLASSIFICATION GUIDE - SPARE PARTS vs CONSUMABLES:
Spare Parts (replaceable components that plug into or interface with devices):
- RAM, DDR memory modules
- SSDs, NVMe drives, M.2 modules
- CPUs, GPUs, processors
- Power supply units (PSU), power modules
- Expansion cards (PCIe, RAID, NIC)
- Cooling solutions (heatsinks, coolers)
- Motherboards
NOT Spare Parts (consumables, generic items):
- Cables (power, SATA, USB, Ethernet)
- Fasteners (screws, washers, standoffs)
- Thermal paste, thermal pads, adhesives
- Connectors, plugs, sockets
- Generic cords and adapters
Decision Tree:
1. Does the item have a replaceable function in a larger system?
2. Does it have a manufacturer part number and technical specifications?
3. Is it described with model/revision information?
If YES to 2+ questions: SPARE PART
If item matches consumable examples: CONSUMABLE
Otherwise: Mark as "uncertain" for human review.
Examples:
✓ "Kingston Fury 16GB DDR4-3200" → Spare Part (RAM)
✓ "Samsung 970 EVO 1TB NVMe" → Spare Part (SSD)
✓ "Intel Core i7-12700K" → Spare Part (CPU)
✗ "6ft SATA Cable" → Consumable (cable)
✗ "CPU Mounting Hardware Kit" → Consumable (fasteners)
```
### Testing Approach for Prompt Accuracy
**Validation Dataset:**
1. Create 20-30 labeled images of actual spare-parts and consumables.
2. Test both Gemini and Claude on same dataset.
3. Measure accuracy of Category classification.
4. Measure accuracy of Part Number extraction.
5. Iterate on prompt examples until >95% accuracy on test set.
**Field Testing with Users:**
1. Have 3-5 field users test Phase 4.1 with real items.
2. Collect feedback on search quality and auto-population accuracy.
3. Measure time-to-save improvement (before vs. after search integration).
---
## 4. Search Result Parsing: CSS Selectors & Data Extraction
### CSS Selectors for Google Search Results
**Standard HTML structure (may change):**
```
div.g // Result container
├── h3 (or a[data-sokoban-click]) // Title
├── a[href^='http'] // URL link
├── div.VwiC3b (or similar) // Snippet/description
└── div.eFM0qc // Display URL
```
**Bing Search Selectors (more stable):**
```
li.b_algo // Result container
├── h2 a // Title + link
├── p // Snippet
└── .tMee // Display URL
```
### Spec Extraction from Snippets
**Regex Patterns:**
```python
# Memory
r'\b(\d+)\s*(GB|TB)\s*(DDR\d|DRAM|RAM|SDRAM)'
# Storage
r'\b(\d+)\s*(GB|TB)\s*(SSD|NVME|NVMe|M\.2|HDD|SATA)'
# Processor
r'(Intel|AMD)\s+([A-Z0-9-]+)\s*(\d+\.\d+\s*GHz)?'
# Power
r'\b(\d+)\s*(W|watts?|watt)\s*(power|supply|PSU)'
# Speed/Latency
r'(\d+)\s*(MHz|GHz|CAS|Latency)'
```
### Data Extraction Pipeline
**Example Input:**
```
Title: Kingston Fury 16GB DDR4-3200 RAM Memory Module
Snippet: Kingston Fury 16GB DDR4 3200MHz CAS Latency 16 - Get superior performance
with Kingston FURY DDR4 memory. 16GB modules deliver rock-solid stability...
```
**Expected Output:**
```
{
"manufacturer": "Kingston",
"model": "Fury",
"capacity": "16GB",
"memory_type": "DDR4",
"speed": "3200MHz",
"latency": "CAS 16",
"confidence": 0.95
}
```
**Mapping to Item Fields:**
- `Item.Name`: `[Kingston] [Fury] [16GB] [DDR4-3200]` (cleaned)
- `Item.Category`: "RAM" (from whitelist match)
- `Item.Type`: "Memory Module" or "DDR4" (spareable)
- `Item.Notes`: Full specs: "Kingston Fury 16GB DDR4-3200MHz CAS Latency 16"
### Handling Variations & Abbreviations
**Common variations to normalize:**
- "DDR4" ↔ "DDR 4" ↔ "DDR-4"
- "3200 MHz" ↔ "3200MHz" ↔ "3.2 GHz"
- "Intel i7" ↔ "Intel Core i7" ↔ "Intel Core™ i7"
- Manufacturers: "SK Hynix" ↔ "SK Hynix" (normalize spacing)
**Confidence scoring:**
- Exact part number match: +0.2
- All major specs found: +0.3
- Manufacturer + model: +0.2
- Consistency checks (price matches category): +0.25
---
## 5. Backend Integration Architecture
### New Modules to Create
1. **`backend/services/spare_parts_search.py`**
- Main orchestrator service.
- Public methods: `search_and_extract(part_number, category, timeout=20)`.
- Returns: `SparePartSearchResult` dataclass.
2. **`backend/services/web_scraper.py`**
- HTTP requests with User-Agent rotation and rate limiting.
- Methods: `search_google()`, `search_bing()`, `fetch_and_parse_html()`.
3. **`backend/services/spec_extractor.py`**
- Regex parsing and data extraction.
- Methods: `extract_specs_from_snippet()`, `extract_specs_from_html()`.
4. **`backend/config/spare_parts_whitelist.py`**
- Configurable category whitelist and exclusion patterns.
- Easy to update without code changes.
### Integration Flow in `/api/onboarding/extract`
**Current Flow:**
```
1. User uploads image
2. AI extraction (Gemini/Claude)
3. Return extracted data to frontend
```
**Phase 4.1 New Flow:**
```
1. User uploads image
2. AI extraction (Gemini/Claude)
3. Check: category in whitelist AND part_number exists?
YES → Trigger async search
NO → Return AI data, skip search
4. Search executes (up to 30s timeout):
- Try Google search
- Fallback to Bing if Google fails
- Parse results, extract specs
5. Return: {
ai_data: {...},
search_results: {...} | null,
search_status: "success" | "timeout" | "error" | "skipped",
search_error: string | null
}
6. Frontend handles loading state, pre-populates fields
```
### Search Service Pseudocode
```python
async def search_and_extract(
part_number: str,
category: str,
manufacturer: str | None = None,
timeout: int = 20
) -> SparePartSearchResult:
"""
Search for spare part specs and extract data.
Returns immediately if timeout exceeded.
"""
try:
# Build search query
query = f"{part_number} {category} {manufacturer or ''}"
# Attempt search with timeout
with asyncio.timeout(timeout):
# Try Google first (with rate limiting)
results = await search_google(query)
if not results:
# Fallback to Bing
results = await search_bing(query)
if not results:
return SparePartSearchResult(
status="no_results",
specs=None,
error="No search results found"
)
# Parse best result
specs = extract_specs_from_snippet(results[0])
return SparePartSearchResult(
status="success",
specs=specs,
error=None,
confidence=specs.get("confidence", 0.0)
)
except asyncio.TimeoutError:
return SparePartSearchResult(
status="timeout",
specs=None,
error="Search exceeded 20s timeout"
)
except Exception as e:
return SparePartSearchResult(
status="error",
specs=None,
error=str(e)
)
```
### Rate Limiting Implementation
**Token Bucket Algorithm:**
```python
class SearchRateLimiter:
def __init__(self, requests_per_second: float = 0.2):
# 0.2 req/sec = 1 req per 5 seconds
self.capacity = 1.0
self.refill_rate = requests_per_second
self.tokens = 1.0
self.last_refill = time.time()
async def acquire(self):
"""Block until search quota available."""
while self.tokens < 1.0:
elapsed = time.time() - self.last_refill
self.tokens += elapsed * self.refill_rate
self.last_refill = time.time()
if self.tokens < 1.0:
await asyncio.sleep(0.1)
self.tokens -= 1.0
```
---
## 6. Frontend AIOnboarding Integration
### State Additions
```typescript
interface AIOnboardingState {
// ... existing state ...
isSearching: boolean; // Search in progress
searchError: string | null; // Error message if failed
searchResults: SparePartSpecs | null; // Extracted specs
searchTimeout: number; // Configurable timeout (30s default)
}
```
### UI Flow
**Sequence:**
1. **User confirms item** after AI extraction review.
2. **Frontend calls** `POST /api/onboarding/extract` with image.
3. **Backend returns** `{ai_data, search_results, search_status, search_error}`.
4. **If search_status = "success"**:
- Show `"Searching for specifications..."` modal (non-dismissible).
- Spinner animation + countdown timer.
- Pre-populate Item.Category, Item.Type, Item.Notes from search results.
5. **User reviews all fields** (can edit any field).
6. **User clicks Save** to commit to database.
**On Search Error:**
- Show modal: `"Search failed: [error message]"`
- Buttons: `[Retry Search] [Skip and Save]`
- If Retry: Re-trigger search (max 2 retries).
- If Skip: Use AI-extracted data only.
### Loading State Design
```tsx
export function SearchLoadingModal({
isOpen,
timeout = 30,
onTimeout,
}: Props) {
const [secondsElapsed, setSecondsElapsed] = useState(0);
useEffect(() => {
if (!isOpen) return;
const interval = setInterval(() => {
setSecondsElapsed((prev) => {
if (prev >= timeout) {
onTimeout();
return prev;
}
return prev + 1;
});
}, 1000);
return () => clearInterval(interval);
}, [isOpen, timeout, onTimeout]);
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg max-w-md text-center">
<Spinner className="mx-auto mb-4" />
<p className="text-lg font-normal mb-2">Searching for specifications...</p>
<p className="text-sm text-slate-500">
{secondsElapsed}s / {timeout}s
</p>
</div>
</div>
);
}
```
### Error Handling UI
```tsx
function SearchErrorModal({
error,
onRetry,
onSkip,
}: Props) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg max-w-md">
<AlertCircle className="text-rose-500 mb-4 mx-auto" />
<p className="text-lg font-normal mb-4">Search failed</p>
<p className="text-sm text-slate-600 mb-6">{error}</p>
<div className="flex gap-3">
<button onClick={onRetry} className="flex-1 bg-primary text-white px-4 py-2 rounded">
Retry Search
</button>
<button onClick={onSkip} className="flex-1 border border-slate-300 px-4 py-2 rounded">
Skip
</button>
</div>
</div>
</div>
);
}
```
---
## 7. Performance & Scalability Analysis
### Expected Latency Profile
| Component | Duration | Notes |
|-----------|----------|-------|
| AI extraction (Gemini/Claude) | 2-5s | Existing, cached |
| Network request + HTML fetch | 2-8s | Highest variability |
| HTML parsing (BeautifulSoup) | 100-500ms | |
| Regex spec extraction | 10-50ms | |
| **Total end-to-end** | **3-15s** | **Typical 20s with retries** |
### Handling Multiple Concurrent Searches
**Recommendation: Sequential Processing**
- Process searches 1 at a time with 5-second delays between.
- Prevents IP blocking and maintains consistent latency.
- Max concurrent searches: 2-3 across all users.
**Implementation:**
```python
# Global search queue
search_queue: asyncio.Queue = asyncio.Queue()
async def process_search_queue():
"""Background task: process queued searches sequentially."""
while True:
search_task = await search_queue.get()
try:
await search_and_extract(**search_task)
finally:
await asyncio.sleep(5) # Rate limit between searches
search_queue.task_done()
```
### Offline Graceful Degradation
**If no internet / search fails:**
1. Catch all network exceptions.
2. Return AI-extracted data only.
3. Show UI message: `"Offline mode: using AI extraction only"`
4. User proceeds with AI data (no pre-population from web search).
5. **Optional:** Queue search for retry when connection restored.
### Rate Limiting to Avoid IP Blocks
**Per-IP Limits:**
- Max 20 requests/minute to Google (distributed across all users).
- Max 10 searches per user per minute.
**Backoff Strategy:**
- First failure: Wait 2 seconds, retry once.
- Second failure: Wait 10 seconds, mark IP as rate-limited.
- If rate-limited: Return AI data, skip search for next 5 minutes.
**User-Agent Rotation:**
- Rotate User-Agent on every request (10+ pool).
- Prevents obvious bot detection.
### Caching Strategy
**Cache by (part_number, category) for 24 hours:**
```python
@cache.cached(timeout=86400, key_prefix="spare_parts_search:")
async def search_and_extract(part_number: str, category: str) -> SparePartSearchResult:
# Expensive search operation
```
**Benefits:**
- Repeated searches for same part (e.g., "16GB RAM DDR4") hit cache.
- Reduces network load and IP block risk.
- Improves UX (instant pre-population on cached searches).
### Scalability Ceiling
**Current Estimate:**
- Suitable for 50-100 item onboardings per day (10-20 searches/day).
- Bottleneck: Google's IP blocking at ~20 requests/minute sustained.
**To Scale Beyond 100+ Searches/Day:**
- Switch to **SerpAPI** ($50-200/month for high volume).
- Implement **proxy rotation** (cost-effective, ~$5-20/month).
- Use **manufacturer APIs directly** (Crucial, Kingston, Corsair offer product APIs).
---
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ AIOnboarding Component │
│ │
│ [Image Upload] → [AI Extraction] → [Confirm Item] │
│ │ │
│ v │
│ [Show Search Loading Modal] │
│ "Searching for specs..." (30s max) │
│ │ │
│ (on complete/error/timeout) │
│ │ │
│ [Pre-populate Fields] ← [Search Results] │
│ Category / Type / Notes editable │
│ │ │
│ v │
│ [User Reviews & Confirms] │
│ │ │
│ v │
│ POST /api/onboarding/save │
└─────────────────────────────────────────────────────────────┘
│ HTTP Request
v
┌──────────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ │
│ POST /api/onboarding/extract │
│ ├─ AI Extract (Gemini/Claude) │
│ ├─ Check: category in whitelist + part_number? │
│ └─ If YES: Call spare_parts_search.search_and_extract() │
│ │ │
│ v │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SparePartsSearch Service │ │
│ │ │ │
│ │ Rate Limiter (token bucket, 0.2 req/sec) │ │
│ │ │ │ │
│ │ v │ │
│ │ WebScraper (requests + User-Agent rotation) │ │
│ │ ├─ search_google(query, timeout=10s) │ │
│ │ └─ search_bing(query) [fallback] │ │
│ │ │ │ │
│ │ v │ │
│ │ SpecExtractor (BeautifulSoup + regex) │ │
│ │ ├─ Parse HTML → CSS selectors │ │
│ │ ├─ Extract snippets │ │
│ │ └─ Regex extraction: specs, manufacturer, etc. │ │
│ │ │ │
│ │ Cache (24h): (part_number, category) → specs │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ POST /api/onboarding/save │
│ ├─ Save AI data + search results to Item │
│ └─ Log to AuditLog │
└──────────────────────────────────────────────────────────────┘
```
---
## Risk Mitigation Strategies
| Risk | Impact | Mitigation |
|------|--------|-----------|
| **Google IP blocking** | Search fails, no specs | Use Bing fallback, implement proxy rotation, cache results |
| **Network timeout** | Slow UX, user frustration | 30s max timeout, show progress, fallback to AI data |
| **Parsing failures** (HTML changes) | No spec extraction | Update regex patterns, use manufacturer APIs, human review |
| **Rate limiting abuse** | Service degradation | Token bucket, per-user limits, exponential backoff |
| **Search quality issues** | Wrong specs populated | Confidence scoring, human review before save, field editability |
| **Offline (no internet)** | Feature unavailable | Graceful degradation, return AI data only, queue for retry |
---
## Testing & Validation Strategy
### Unit Tests
**File: `backend/tests/test_spare_parts_search.py`**
```python
def test_search_and_extract_success():
"""Test successful search and spec extraction."""
result = await search_and_extract(
part_number="Kingston Fury 16GB",
category="RAM"
)
assert result.status == "success"
assert result.specs["manufacturer"] == "Kingston"
assert result.specs["capacity"] == "16GB"
def test_search_timeout():
"""Test graceful timeout handling."""
result = await search_and_extract(
part_number="test",
category="RAM",
timeout=0.1 # Force timeout
)
assert result.status == "timeout"
assert result.specs is None
def test_whitelist_matching():
"""Test spare-part classification."""
assert classify_as_spare_part("DDR4 RAM") == True
assert classify_as_spare_part("CPU 16GB") == True
assert classify_as_spare_part("Power Cable 6ft") == False
assert classify_as_spare_part("Thermal Paste") == False
def test_spec_extraction_regex():
"""Test regex patterns for spec extraction."""
snippet = "Kingston Fury 16GB DDR4-3200 CAS 16"
specs = extract_specs_from_snippet(snippet, category="RAM")
assert specs["capacity"] == "16GB"
assert specs["memory_type"] == "DDR4"
assert specs["speed"] == "3200"
```
### Integration Tests
**File: `backend/tests/test_onboarding_with_search.py`**
```python
@pytest.mark.asyncio
async def test_onboarding_extract_with_search():
"""Test full onboarding flow with search integration."""
# Upload image
response = await client.post(
"/api/onboarding/extract",
files={"file": ("test_ram.jpg", image_bytes)},
data={"mode": "catalog"}
)
assert response.status_code == 200
data = response.json()
assert data["ai_data"]["category"] in ["RAM", "Memory"]
assert data["search_status"] in ["success", "timeout", "error", "skipped"]
if data["search_status"] == "success":
assert "manufacturer" in data["search_results"]
assert "specs" in data["search_results"]
```
### Frontend Tests
**File: `frontend/components/__tests__/SearchLoadingModal.test.tsx`**
```typescript
describe("SearchLoadingModal", () => {
it("displays countdown timer", () => {
render(<SearchLoadingModal isOpen timeout={30} />);
expect(screen.getByText(/Searching for specifications/)).toBeInTheDocument();
expect(screen.getByText(/0s \/ 30s/)).toBeInTheDocument();
});
it("calls onTimeout after timeout expires", async () => {
const onTimeout = vi.fn();
render(<SearchLoadingModal isOpen timeout={1} onTimeout={onTimeout} />);
await new Promise(resolve => setTimeout(resolve, 1100));
expect(onTimeout).toHaveBeenCalled();
});
});
```
### Field Testing with Users
1. **Recruit 3-5 power users** (heavy inventory users).
2. **Phase A (1 week)**: Manual specification lookup (baseline).
3. **Phase B (1 week)**: Test Phase 4.1 with automatic search.
4. **Metrics**:
- Time-to-save per item (before vs. after).
- Accuracy of auto-populated fields.
- Number of user edits post-search.
- Search success rate (not timeout/error).
5. **Collect feedback**: Desired fallback sources, UX tweaks, edge cases.
---
## RESEARCH COMPLETE