# Test Specification — Tier2SpatialAnalyzer

## Acceptance Criteria Traceability

| AC ID | Acceptance Criterion | Test IDs | Coverage |
|-------|---------------------|----------|----------|
| AC-02 | Tier 2 latency ≤200ms per ROI | PT-01, PT-02 | Covered |
| AC-06 | Concealed position recall ≥60% | AT-01 | Covered |
| AC-07 | Concealed position precision ≥20% initial | AT-01 | Covered |
| AC-08 | Footpath detection recall ≥70% | AT-02 | Covered |
| AC-23 | Distinguish fresh footpaths from stale ones | IT-03, AT-03 | Covered |
| AC-24 | Trace footpaths to endpoints, identify concealed structures | IT-01, IT-02, AT-04 | Covered |
| AC-25 | Handle path intersections by following freshest branch | IT-04 | Covered |

---

## Integration Tests

### IT-01: trace_mask Produces Valid Skeleton and Waypoints

**Summary**: Verify trace_mask correctly skeletonizes a clean binary footpath mask and returns waypoints at endpoints.

**Traces to**: AC-24

**Input data**:
- Binary mask (1080x1920) with a single continuous footpath shape (L-shaped, ~300px long)
- gsd=0.15 (meters per pixel)

**Expected result**:
- SpatialAnalysisResult with pattern_type="mask_trace"
- waypoints list length ≥ 2 (at least start and end)
- skeleton is non-None, shape matches input mask
- trajectory has ≥ 10 points along the skeleton
- overall_direction is a unit-ish vector (magnitude > 0)
- Each waypoint has x, y within mask bounds, label is a string, confidence in [0,1]

**Max execution time**: 200ms

**Dependencies**: None (stateless)

---

### IT-02: trace_mask Handles Fragmented Mask

**Summary**: Verify morphological closing connects nearby mask fragments before skeletonization.

**Traces to**: AC-24

**Input data**:
- Binary mask with 5 disconnected fragments (gaps of ~10px) forming a roughly linear path

**Expected result**:
- Closing connects fragments into 1-2 components
- Skeleton follows the general path direction
- Waypoints at the two extremes of the longest connected component
- No crash or empty result

**Max execution time**: 200ms

**Dependencies**: None

---

### IT-03: Freshness Tag Assignment

**Summary**: Verify analyze_roi assigns freshness_tag based on path-vs-terrain contrast for mask traces.

**Traces to**: AC-23

**Input data**:
- Frame with high-contrast footpath on snow (bright terrain, dark path) → expected "high_contrast"
- Frame with low-contrast footpath on mud (similar intensity) → expected "low_contrast"

**Expected result**:
- High contrast ROI: freshness_tag="high_contrast"
- Low contrast ROI: freshness_tag="low_contrast"

**Max execution time**: 50ms per ROI

**Dependencies**: None

---

### IT-04: trace_mask Handles Intersections by Selecting Longest Branch

**Summary**: Verify that when a skeleton has multiple branches (intersection), the longest connected component is selected.

**Traces to**: AC-25

**Input data**:
- Binary mask forming a T-intersection: main path ~400px, branch ~100px

**Expected result**:
- Longest skeleton component selected (~400px worth of skeleton)
- Short branch pruned (< min_branch_length or shorter than main path)
- Waypoints at the two endpoints of the main path

**Max execution time**: 200ms

**Dependencies**: None

---

### IT-05: trace_cluster Produces Waypoints for Valid Cluster

**Summary**: Verify trace_cluster groups nearby detections and produces ordered waypoints.

**Traces to**: AC-24

**Input data**:
- 4 detections: labels=["radar_dish", "aa_launcher", "radar_dish", "military_truck"]
- Centers: (100,100), (200,150), (150,300), (250,250) — all within 300px cluster_radius
- Scenario: cluster_follow, min_cluster_size=2, cluster_radius_px=300

**Expected result**:
- SpatialAnalysisResult with pattern_type="cluster_trace"
- waypoints list length = 4 (one per detection)
- Waypoints ordered by nearest-neighbor from a starting position
- cluster_bbox covers all 4 detection centers
- trajectory connects waypoints in visit order

**Max execution time**: 50ms

**Dependencies**: None

---

### IT-06: trace_cluster Returns Empty for Below-Minimum Cluster

**Summary**: Verify trace_cluster returns empty result when cluster size is below minimum.

**Traces to**: AC-24

**Input data**:
- 1 detection: {label: "radar_dish", center: (100,100)}
- Scenario: min_cluster_size=2

**Expected result**:
- SpatialAnalysisResult with empty waypoints list
- pattern_type="cluster_trace"
- No exception

**Max execution time**: 10ms

**Dependencies**: None

---

### IT-07: trace_mask Empty Mask Returns Empty Result

**Summary**: Verify trace_mask handles an all-zero mask gracefully.

**Traces to**: AC-24

**Input data**:
- All-zero binary mask (1080x1920)

**Expected result**:
- SpatialAnalysisResult with empty waypoints list
- skeleton is None or all-zero
- No exception

**Max execution time**: 50ms

**Dependencies**: None

---

### IT-08: analyze_roi Classifies Dark ROI as Potential Concealment

**Summary**: Verify the darkness + contrast heuristic labels a dark center with bright surrounding as a target class.

**Traces to**: AC-06, AC-07

**Input data**:
- ROI (100x100) with center 50% at mean intensity 40 (dark), surrounding mean 180 (bright)
- Config: darkness_threshold=80, contrast_threshold=0.3

**Expected result**:
- label from scenario's target_classes (not "unknown")
- confidence > 0
- Contrast = (180-40)/180 = 0.78 > 0.3 → passes threshold

**Max execution time**: 10ms

**Dependencies**: None

---

### IT-09: analyze_roi Rejects Bright ROI as Unknown

**Summary**: Verify the heuristic does not flag bright, low-contrast regions.

**Traces to**: AC-07

**Input data**:
- ROI (100x100) with center mean 160, surrounding mean 170
- Config: darkness_threshold=80, contrast_threshold=0.3

**Expected result**:
- label="unknown"
- darkness 160 > threshold 80 → fails darkness check

**Max execution time**: 10ms

**Dependencies**: None

---

## Performance Tests

### PT-01: trace_mask Latency on Full-Resolution Mask

**Summary**: Measure end-to-end trace_mask processing time on 1080p masks.

**Traces to**: AC-02

**Load scenario**:
- 50 different binary masks (1080x1920), varying complexity
- Sequential processing
- Duration: ~5s

**Expected results**:

| Metric | Target | Failure Threshold |
|--------|--------|-------------------|
| Latency (p50) | ≤30ms | >200ms |
| Latency (p95) | ≤100ms | >200ms |
| Latency (p99) | ≤150ms | >200ms |

**Resource limits**:
- CPU: ≤50% single core
- Memory: ≤200MB additional

---

### PT-02: trace_cluster Latency with Many Detections

**Summary**: Measure trace_cluster processing time with increasing detection counts.

**Traces to**: AC-02

**Load scenario**:
- Detection counts: 10, 20, 50 detections
- cluster_radius_px=300
- Sequential processing

**Expected results**:

| Metric | Target | Failure Threshold |
|--------|--------|-------------------|
| Latency (10 dets, p95) | ≤5ms | >50ms |
| Latency (50 dets, p95) | ≤20ms | >200ms |

**Resource limits**:
- CPU: ≤30% single core
- Memory: ≤50MB additional

---

## Security Tests

### ST-01: ROI Boundary Clipping Prevents Out-of-Bounds Access

**Summary**: Verify analyze_roi clips ROI to frame boundaries when bbox extends beyond frame edges.

**Traces to**: AC-24

**Attack vector**: Crafted detection with bbox extending beyond frame dimensions

**Test procedure**:
1. Call analyze_roi with bbox extending 50px beyond frame right edge
2. Call analyze_roi with bbox at negative coordinates

**Expected behavior**: ROI is clipped to valid frame area; no segfault, no array index error.

**Pass criteria**: Function returns a classification without raising exceptions.

**Fail criteria**: IndexError, segfault, or uncaught exception.

---

## Acceptance Tests

### AT-01: Concealed Position Detection Rate on Validation Set

**Summary**: Verify the heuristic achieves ≥60% recall and ≥20% precision on concealed position ROIs.

**Traces to**: AC-06, AC-07

**Preconditions**:
- Validation set of 100+ annotated concealment ROIs (positive: actual concealed positions, negative: shadows, puddles, dark soil)
- Config thresholds set to production defaults

**Steps**:

| Step | Action | Expected Result |
|------|--------|-----------------|
| 1 | Run analyze_roi on all positive ROIs | Count TP (label != "unknown") and FN |
| 2 | Run analyze_roi on all negative ROIs | Count FP (label != "unknown") and TN |
| 3 | Compute recall = TP/(TP+FN) | ≥ 60% |
| 4 | Compute precision = TP/(TP+FP) | ≥ 20% |

---

### AT-02: Footpath Endpoint Detection Rate

**Summary**: Verify trace_mask finds endpoints for ≥70% of annotated footpaths.

**Traces to**: AC-08

**Preconditions**:
- 50+ annotated footpath masks with ground-truth endpoint locations

**Steps**:

| Step | Action | Expected Result |
|------|--------|-----------------|
| 1 | Run trace_mask on each mask | SpatialAnalysisResult per mask |
| 2 | Match waypoints to ground-truth endpoints (within 30px) | Count matches |
| 3 | Compute recall = matched / total GT endpoints | ≥ 70% |

---

### AT-03: Freshness Discrimination on Seasonal Data

**Summary**: Verify freshness_tag distinguishes fresh high-contrast paths from stale low-contrast ones.

**Traces to**: AC-23

**Preconditions**:
- 30 annotated ROIs: 15 fresh (high-contrast), 15 stale (low-contrast)

**Steps**:

| Step | Action | Expected Result |
|------|--------|-----------------|
| 1 | Run analyze_roi on all 30 ROIs | freshness_tag assigned to each |
| 2 | Check fresh ROIs tagged "high_contrast" | ≥ 80% correctly tagged |
| 3 | Check stale ROIs tagged "low_contrast" | ≥ 60% correctly tagged |

---

### AT-04: End-to-End Mask Trace Pipeline

**Summary**: Verify the full pipeline from binary mask to classified waypoints.

**Traces to**: AC-24

**Preconditions**:
- 10 real footpath masks from field imagery
- Known endpoint locations

**Steps**:

| Step | Action | Expected Result |
|------|--------|-----------------|
| 1 | trace_mask on each mask | SpatialAnalysisResult returned |
| 2 | Verify waypoints exist at path endpoints | ≥ 70% of endpoints found |
| 3 | Verify trajectory follows skeleton | Trajectory points lie within 5px of skeleton |
| 4 | Verify overall_direction matches path orientation | Angle error < 30° |

---

## Test Data Management

**Required test data**:

| Data Set | Description | Source | Size |
|----------|-------------|--------|------|
| footpath_masks | 50+ binary footpath masks (1080p) with GT endpoints | Annotated from Tier1 output | ~500 MB |
| concealment_rois | 100+ ROI crops: positives (actual concealment) + negatives (shadows, puddles) | Annotated field imagery | ~200 MB |
| freshness_rois | 30 ROI crops: 15 fresh, 15 stale | Annotated field imagery | ~60 MB |
| cluster_fixtures | Synthetic detection lists for cluster tracing tests | Generated | ~1 KB |
| fragmented_masks | Masks with deliberate gaps and noise | Generated from real masks | ~100 MB |

**Setup procedure**:
1. Load test masks/ROIs from fixture directory
2. Load config with test thresholds

**Teardown procedure**:
1. No persistent state to clean (stateless component)

**Data isolation strategy**: Each test creates its own input arrays. No shared mutable state.
