# S04: Article Versioning + Pipeline Tuning Metadata — UAT

**Milestone:** M004
**Written:** 2026-03-30T07:21:14.192Z

## UAT: S04 — Article Versioning + Pipeline Tuning Metadata

### Preconditions
- PostgreSQL running on ub01:5433 with chrysopedia database
- Alembic migration 002 applied (`docker exec chrysopedia-api alembic upgrade head`)
- At least one technique page exists in the database (from prior pipeline runs)
- API running at ub01:8096
- Frontend built and served

---

### Test 1: TechniquePageVersion model loads correctly
**Steps:**
1. `docker exec chrysopedia-api python -c "from models import TechniquePageVersion; print('OK')"`

**Expected:** Prints "OK", exit 0.

---

### Test 2: Migration 002 creates technique_page_versions table
**Steps:**
1. `docker exec chrysopedia-api alembic upgrade head`
2. `docker exec chrysopedia-db psql -U chrysopedia -d chrysopedia -c "\d technique_page_versions"`

**Expected:** Table exists with columns: id (uuid), technique_page_id (uuid), version_number (integer), content_snapshot (jsonb), pipeline_metadata (jsonb), created_at (timestamp). Composite unique index on (technique_page_id, version_number).

---

### Test 3: Version list endpoint returns empty for page with no versions
**Steps:**
1. Pick an existing technique slug: `curl -s http://ub01:8096/api/v1/techniques | jq '.items[0].slug'`
2. `curl -s http://ub01:8096/api/v1/techniques/{slug}/versions`

**Expected:** `{"items": [], "total": 0}`

---

### Test 4: Technique detail includes version_count field
**Steps:**
1. `curl -s http://ub01:8096/api/v1/techniques/{slug} | jq '.version_count'`

**Expected:** Returns `0` (integer, not null/missing).

---

### Test 5: Re-running pipeline creates a version snapshot
**Steps:**
1. Note an existing technique page slug and its current body content
2. Trigger re-processing: `curl -X POST http://ub01:8096/api/v1/pipeline/trigger/{video_id}`
3. Wait for pipeline completion (check worker logs)
4. `curl -s http://ub01:8096/api/v1/techniques/{slug}/versions`

**Expected:** `total: 1`, items contains one version with `version_number: 1`, `pipeline_metadata` includes `prompt_hashes` and `models` keys, `created_at` is recent.

---

### Test 6: Version detail returns full content snapshot
**Steps:**
1. After Test 5, `curl -s http://ub01:8096/api/v1/techniques/{slug}/versions/1`

**Expected:** Response includes `version_number: 1`, `content_snapshot` object with keys: title, slug, topic_category, topic_tags, summary, body_sections, signal_chains, plugins, source_quality. `pipeline_metadata` includes prompt file hashes.

---

### Test 7: Version 404 for nonexistent version number
**Steps:**
1. `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/techniques/{slug}/versions/999`

**Expected:** HTTP 404.

---

### Test 8: Versions 404 for nonexistent slug
**Steps:**
1. `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/techniques/nonexistent-slug-xyz/versions`

**Expected:** HTTP 404.

---

### Test 9: Frontend shows version count when > 0
**Steps:**
1. After Test 5 creates a version, navigate to the technique page in browser: `http://ub01:8096/technique/{slug}`
2. Look at the meta stats line below the title

**Expected:** Shows "1 version" (or "N versions") in the meta stats line alongside moment count.

---

### Test 10: Frontend hides version count when 0
**Steps:**
1. Navigate to a technique page with no version history: `http://ub01:8096/technique/{slug-with-no-versions}`

**Expected:** No version count text appears in the meta stats line.

---

### Test 11: Pipeline metadata captures prompt hashes
**Steps:**
1. After Test 5, `curl -s http://ub01:8096/api/v1/techniques/{slug}/versions/1 | jq '.pipeline_metadata.prompt_hashes'`

**Expected:** Object with keys matching prompt file names (e.g., stage2_segmentation, stage3_extraction, etc.), each value is a 64-char hex SHA-256 hash string.

---

### Test 12: Multiple re-runs increment version number
**Steps:**
1. Trigger pipeline again for the same video
2. Wait for completion
3. `curl -s http://ub01:8096/api/v1/techniques/{slug}/versions`

**Expected:** `total: 2`, versions list shows version_number 2 and 1 (DESC order), each with distinct created_at timestamps.

---

### Edge Cases

**E1: Concurrent pipeline runs for same video** — Not expected in normal operation (single admin). If it happens, the composite unique index on (technique_page_id, version_number) prevents duplicate version numbers.

**E2: Missing prompt files** — If a prompt file referenced in settings doesn't exist, pipeline_metadata.prompt_hashes should contain an empty string for that file (with WARNING in worker logs).
