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

27 KiB

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:

# 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

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:

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

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

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

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:

# 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:

@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

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

@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

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