# S02: Stage 5 Quality Scorer & Voice Preservation Dial — UAT

**Milestone:** M013
**Written:** 2026-04-01T09:01:48.179Z

## UAT: Stage 5 Quality Scorer & Voice Preservation Dial

### Preconditions
- Python 3.12+ available
- Working directory: project root (`/home/aux/projects/content-to-kb-automator`)
- No live LLM endpoint required (tests verify error handling)

### Test Case 1: CLI Help Output
**Steps:**
1. Run `python -m pipeline.quality score --help`

**Expected:**
- Exit code 0
- Output shows three arguments: `--file`, `--slug`, `--voice-level`
- `--file` and `--slug` shown as mutually exclusive (required group)
- `--voice-level` shown as optional with float type

### Test Case 2: Score with File Input (No LLM)
**Steps:**
1. Run `python -m pipeline.quality score --file backend/pipeline/quality/fixtures/sample_moments.json`

**Expected:**
- Exit code 1 (no LLM available)
- Output includes "Cannot reach LLM endpoint" with the endpoint URL
- No Python traceback in output
- Shows "Scoring page for 'KOAN Sound' (6 moments)..."

### Test Case 3: Score with Voice Level (No LLM)
**Steps:**
1. Run `python -m pipeline.quality score --file backend/pipeline/quality/fixtures/sample_moments.json --voice-level 0.3`

**Expected:**
- Exit code 1 (no LLM available)
- Output includes "Re-synthesizing + scoring" and "voice_level=0.3"
- Output includes "band=low" (0.3 falls in low band ≤0.33)
- No Python traceback

### Test Case 4: Voice Dial Band Boundaries
**Steps:**
1. Run:
```python
from pipeline.quality.voice_dial import VoiceDial
vd = VoiceDial("base prompt text")
low = vd.modify(0.1)
mid = vd.modify(0.5)
high = vd.modify(0.9)
```

**Expected:**
- `low != mid` — low band appends voice suppression instructions
- `mid != high` — high band appends voice amplification instructions
- `mid == "base prompt text"` — mid band returns base prompt unmodified
- `low` contains words like "suppress", "neutral", or "third-person"
- `high` contains words like "quote", "direct", or "personality"

### Test Case 5: Invalid Voice Level
**Steps:**
1. Run `python -m pipeline.quality score --file backend/pipeline/quality/fixtures/sample_moments.json --voice-level 1.5`

**Expected:**
- Exit code 1
- Error message: "--voice-level must be between 0.0 and 1.0"

### Test Case 6: Missing Required Input
**Steps:**
1. Run `python -m pipeline.quality score`

**Expected:**
- Exit code 2 (argparse error)
- Error message about requiring --file or --slug

### Test Case 7: Slug Path (Deferred)
**Steps:**
1. Run `python -m pipeline.quality score --slug test-technique`

**Expected:**
- Exit code 1
- Output: "DB loading not yet implemented"

### Test Case 8: Fixture File Validity
**Steps:**
1. Run:
```python
import json
with open("backend/pipeline/quality/fixtures/sample_moments.json") as f:
    data = json.load(f)
```

**Expected:**
- `data["creator_name"]` is a non-empty string
- `data["moments"]` is a list with ≥5 entries
- Each moment has: `summary`, `transcript_excerpt`, `topic_tags`, `topic_category`
- At least one moment contains specific plugin/setting mentions in transcript_excerpt

### Test Case 9: Import from Project Root
**Steps:**
1. From project root, run:
```python
from pipeline.quality.scorer import ScoreRunner, ScoreResult
from pipeline.quality.voice_dial import VoiceDial
```

**Expected:**
- Both imports succeed without error
- Works from project root (not just backend/)
