# S03: LLM Extraction Pipeline + Qdrant Integration — UAT

**Milestone:** M001
**Written:** 2026-03-29T22:59:23.268Z

## UAT: LLM Extraction Pipeline + Qdrant Integration

### Preconditions
- PostgreSQL running with chrysopedia database and schema applied
- Redis running (for Celery broker and classification cache)
- Python venv activated with all requirements installed
- Working directory: `backend/`

---

### Test 1: Pipeline Infrastructure Imports
**Steps:**
1. Run `python -c "from config import Settings; s = Settings(); print(s.llm_api_url, s.llm_fallback_url, s.embedding_model, s.qdrant_url, s.qdrant_collection, s.review_mode)"`
2. Run `python -c "from pipeline.schemas import SegmentationResult, ExtractionResult, ClassificationResult, SynthesisResult, TopicSegment, ExtractedMoment, ClassifiedMoment, SynthesizedPage; print('all 8 schemas ok')"`
3. Run `python -c "from pipeline.llm_client import LLMClient; from pipeline.embedding_client import EmbeddingClient; from pipeline.qdrant_client import QdrantManager; print('all clients ok')"`
4. Run `python -c "from worker import celery_app; tasks = [t for t in celery_app.tasks if 'stage' in t or 'pipeline' in t]; assert len(tasks) == 6; print(sorted(tasks))"`

**Expected:** All commands exit 0. Settings shows correct defaults. 8 schemas import. 3 clients import. 6 tasks registered.

---

### Test 2: Stage 2 — Segmentation Updates Topic Labels
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_stage2_segmentation_updates_topic_labels -v`

**Expected:** Test passes. TranscriptSegment rows have topic_label set from mocked LLM segmentation output.

---

### Test 3: Stage 3 — Extraction Creates Key Moments
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_stage3_extraction_creates_key_moments -v`

**Expected:** Test passes. KeyMoment rows created with title, summary, start_time, end_time, content_type. SourceVideo processing_status = 'extracted'.

---

### Test 4: Stage 4 — Classification Assigns Tags
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_stage4_classification_assigns_tags -v`

**Expected:** Test passes. Classification data stored in Redis matching canonical tag categories from canonical_tags.yaml.

---

### Test 5: Stage 5 — Synthesis Creates Technique Pages
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_stage5_synthesis_creates_technique_pages -v`

**Expected:** Test passes. TechniquePage rows created with body_sections, signal_chains, summary, topic_tags. KeyMoments linked via technique_page_id. Processing status updated to reviewed/published.

---

### Test 6: Stage 6 — Embedding and Qdrant Upsert
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_stage6_embeds_and_upserts_to_qdrant -v`

**Expected:** Test passes. EmbeddingClient.embed called with technique page and key moment text. QdrantManager.upsert called with metadata payloads.

---

### Test 7: Pipeline Resumability
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_run_pipeline_resumes_from_extracted -v`

**Expected:** Test passes. When video has processing_status='extracted', only stages 4+5+6 execute (not 2+3).

---

### Test 8: Manual Pipeline Trigger API
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_pipeline_trigger_endpoint -v`
2. Run `python -m pytest tests/test_pipeline.py::test_pipeline_trigger_404_for_missing_video -v`

**Expected:** Both pass. POST /api/v1/pipeline/trigger/{video_id} returns 200 with status for existing video, 404 for missing video.

---

### Test 9: Ingest Auto-Dispatches Pipeline
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_ingest_dispatches_pipeline -v`

**Expected:** Test passes. After ingest commit, run_pipeline.delay() is called with the video_id.

---

### Test 10: LLM Fallback on Primary Failure
**Steps:**
1. Run `python -m pytest tests/test_pipeline.py::test_llm_fallback_on_primary_failure -v`

**Expected:** Test passes. When primary LLM endpoint raises APIConnectionError, fallback endpoint is used successfully.

---

### Test 11: Full Test Suite Regression
**Steps:**
1. Run `python -m pytest tests/ -v`

**Expected:** All 16 tests pass (6 ingest + 10 pipeline). No regressions from S02 ingest tests.

---

### Test 12: Prompt Template Files Exist and Are Non-Empty
**Steps:**
1. Run `test -s ../prompts/stage2_segmentation.txt && test -s ../prompts/stage3_extraction.txt && test -s ../prompts/stage4_classification.txt && test -s ../prompts/stage5_synthesis.txt && echo "all prompts non-empty"`
2. Run `grep -l '<transcript>' ../prompts/*.txt | wc -l` (verify XML-style fencing)

**Expected:** All 4 files exist and are non-empty. XML-style tags present in prompt files.

---

### Edge Cases

**EC1: Ingest succeeds even if Celery/Redis is down**
The ingest endpoint wraps run_pipeline.delay() in try/except. If Celery dispatch fails, ingest still returns 200 and logs a WARNING. Verified by test_ingest_dispatches_pipeline mock setup.

**EC2: stage6 embedding failure doesn't break pipeline**
stage6_embed_and_index catches all exceptions with max_retries=0. If Qdrant or embedding API is unreachable, pipeline completes with stages 2-5 results intact. Verified by test_stage6 mock setup.

**EC3: LLM returns malformed JSON**
_safe_parse_llm_response retries once with a JSON nudge prompt. On second failure, logs ERROR with raw response excerpt and raises.

---

### Operational Readiness (Q8)

**Health signal:** `source_videos.processing_status` tracks per-video pipeline progress. Celery task registry shows 6 tasks via `python -c "from worker import celery_app; print([t for t in celery_app.tasks if 'stage' in t or 'pipeline' in t])"`.

**Failure signal:** Video stuck at a processing_status other than `reviewed`/`published` for >10 minutes indicates a pipeline failure. Check Celery worker logs for ERROR entries with stage name and video_id.

**Recovery procedure:** POST `/api/v1/pipeline/trigger/{video_id}` to re-trigger the pipeline from the last completed stage. For embedding-only issues, the pipeline can be re-run — stage6 is idempotent (creates new Qdrant points, doesn't remove old ones).

**Monitoring gaps:** No pipeline duration metrics exposed yet. No dead letter queue for permanently failed tasks. No Qdrant point count monitoring. Stage 4 Redis TTL (24h) could expire before stage 5 runs if pipeline is paused.
