Made-with: Cursor
15 KiB
Test Specification — ScanController
Acceptance Criteria Traceability
| AC ID | Acceptance Criterion | Test IDs | Coverage |
|---|---|---|---|
| AC-09 | L1 wide-area scan covers planned route with left-right camera sweep at medium zoom | IT-01, AT-01 | Covered |
| AC-10 | POIs detected during L1: footpaths, tree rows, branch piles, black entrances, houses with vehicles/traces, roads | IT-02, IT-03, AT-02 | Covered |
| AC-11 | L1→L2 transition within 2 seconds of POI detection | IT-04, PT-01, AT-03 | Covered |
| AC-12 | L2 maintains camera lock on POI while UAV continues flight | IT-05, AT-04 | Covered |
| AC-13 | Path-following mode: camera pans along footpath keeping it visible and centered | IT-06, AT-05 | Covered |
| AC-14 | Endpoint hold: camera maintains position on path endpoint for VLM analysis (up to 2s) | IT-07, AT-06 | Covered |
| AC-15 | Return to L1 after analysis completes or configurable timeout (default 5s) | IT-08, AT-07 | Covered |
| AC-21 | POI queue: ordered by confidence and proximity | IT-09, IT-10 | Covered |
| AC-22 | Semantic pipeline consumes YOLO detections as input | IT-02, IT-11 | Covered |
| AC-27 | Coexist with YOLO pipeline without degrading YOLO performance | PT-02 | Covered |
Integration Tests
IT-01: L1 Sweep Cycle Completes
Summary: Verify a single L1 sweep tick advances the scan angle and invokes Tier1 inference.
Traces to: AC-09
Input data:
- Mock Tier1Detector returning empty detection list
- Mock GimbalDriver accepting set_sweep_target()
- Config: sweep_angle_range=45, sweep_step=5
Expected result:
- Scan angle increments by sweep_step
- GimbalDriver.set_sweep_target called with new angle
- Tier1Detector.detect called once
- Tree returns SUCCESS from L1Sweep branch
Max execution time: 500ms
Dependencies: Mock Tier1Detector, Mock GimbalDriver, Mock OutputManager
IT-02: EvaluatePOI Creates POI from Trigger Class Match
Summary: Verify that when Tier1 returns a detection matching a scenario trigger class, a POI is created and queued.
Traces to: AC-10, AC-22
Input data:
- Detection: {label: "footpath_winter", confidence: 0.72, bbox: (0.5, 0.5, 0.1, 0.3)}
- Active scenario: winter_concealment (trigger classes: [footpath_winter, branch_pile, dark_entrance], min_confidence: 0.5)
Expected result:
- POI created with scenario_name="winter_concealment", investigation_type="path_follow"
- POI priority = 0.72 * 1.0 (priority_boost)
- POI added to blackboard poi_queue
Max execution time: 100ms
Dependencies: Mock Tier1Detector
IT-03: EvaluatePOI Cluster Aggregation
Summary: Verify that cluster_follow scenarios aggregate multiple nearby detections into a single cluster POI.
Traces to: AC-10
Input data:
- Detections: 3x {label: "radar_dish", confidence: 0.6}, centers within 200px of each other
- Active scenario: aa_defense_network (type: cluster_follow, min_cluster_size: 2, cluster_radius_px: 300)
Expected result:
- Single cluster POI created (not 3 individual POIs)
- cluster_detections contains all 3 detections
- priority = mean(confidences) * 1.5
Max execution time: 100ms
Dependencies: Mock Tier1Detector
IT-04: L1→L2 Transition Timing
Summary: Verify that when a POI is queued, the next tree tick enters L2Investigation and issues a zoom command.
Traces to: AC-11
Input data:
- POI already on blackboard queue
- Mock GimbalDriver.zoom_to_poi returns success after simulated delay
Expected result:
- Tree selects L2Investigation branch (not L1Sweep)
- GimbalDriver.zoom_to_poi called
- Transition from L1 tick to GimbalDriver call occurs within same tick cycle (<100ms in mock)
Max execution time: 500ms
Dependencies: Mock GimbalDriver, Mock Tier1Detector
IT-05: L2 Investigation Maintains Zoom on POI
Summary: Verify L2DetectLoop continuously captures frames and runs Tier1 at high zoom while investigating a POI.
Traces to: AC-12
Input data:
- POI with investigation_type="zoom_classify"
- Mock Tier1Detector returns detections at high zoom
- Investigation timeout: 10s
Expected result:
- Multiple CaptureFrame + RunTier1 cycles within the L2DetectLoop
- GimbalDriver maintains zoom level (no return_to_sweep called during investigation)
Max execution time: 2s
Dependencies: Mock Tier1Detector, Mock GimbalDriver, Mock OutputManager
IT-06: PathFollowSubtree Invokes Tier2 and PID
Summary: Verify that for a path_follow investigation, TraceMask is called and gimbal PID follow is engaged along the skeleton trajectory.
Traces to: AC-13
Input data:
- POI with investigation_type="path_follow", scenario=winter_concealment
- Mock Tier2SpatialAnalyzer.trace_mask returns SpatialAnalysisResult with 3 waypoints and trajectory
- Mock GimbalDriver.follow_path accepts direction commands
Expected result:
- Tier2SpatialAnalyzer.trace_mask called with the frame's segmentation mask
- GimbalDriver.follow_path called with direction from SpatialAnalysisResult.overall_direction
- WaypointAnalysis evaluates each waypoint
Max execution time: 1s
Dependencies: Mock Tier2SpatialAnalyzer, Mock GimbalDriver
IT-07: Endpoint Hold for VLM Analysis
Summary: Verify that at a path endpoint with ambiguous confidence, the system holds position and invokes VLMClient.
Traces to: AC-14
Input data:
- Waypoint at skeleton endpoint with confidence=0.4 (below high-confidence threshold)
- Scenario.use_vlm=true
- Mock VLMClient.is_available=true
- Mock VLMClient.analyze returns VLMResponse within 2s
Expected result:
- CheckVLMAvailable returns SUCCESS
- RunVLM action invokes VLMClient.analyze with ROI image and prompt
- LogDetection records tier=3
Max execution time: 3s
Dependencies: Mock VLMClient, Mock OutputManager
IT-08: Return to L1 After Investigation Completes
Summary: Verify the tree returns to L1Sweep after L2Investigation finishes.
Traces to: AC-15
Input data:
- L2Investigation sequence completes (all waypoints analyzed)
- Mock GimbalDriver.return_to_sweep returns success
Expected result:
- ReturnToSweep action calls GimbalDriver.return_to_sweep
- Next tree tick enters L1Sweep branch (POI queue now empty)
- Scan angle resumes from where it left off
Max execution time: 500ms
Dependencies: Mock GimbalDriver
IT-09: POI Queue Priority Ordering
Summary: Verify the POI queue returns the highest-priority POI first.
Traces to: AC-21
Input data:
- 3 POIs: {priority: 0.5}, {priority: 0.9}, {priority: 0.3}
Expected result:
- PickHighestPOI retrieves POI with priority=0.9 first
- Subsequent picks return 0.5, then 0.3
Max execution time: 100ms
Dependencies: None
IT-10: POI Queue Deduplication
Summary: Verify that overlapping POIs (same bbox area) are deduplicated.
Traces to: AC-21
Input data:
- Two detections with >70% bbox overlap, same scenario trigger
Expected result:
- Only one POI created; higher-confidence one kept
Max execution time: 100ms
Dependencies: None
IT-11: HealthGuard Disables Semantic When Overheating
Summary: Verify the HealthGuard decorator routes to FallbackBehavior when capability flags degrade.
Traces to: AC-22, AC-27
Input data:
- capability_flags: {semantic_available: false, gimbal_available: true, vlm_available: false}
Expected result:
- HealthGuard activates FallbackBehavior
- Tree runs existing YOLO only (no EvaluatePOI, no L2 investigation)
Max execution time: 500ms
Dependencies: Mock Tier1Detector
Performance Tests
PT-01: L1→L2 Transition Latency Under Load
Summary: Measure the time from POI detection to GimbalDriver.zoom_to_poi call under continuous inference load.
Traces to: AC-11
Load scenario:
- Continuous L1 sweep at 30 FPS
- POI injected at frame N
- Duration: 60 seconds
- Ramp-up: immediate
Expected results:
| Metric | Target | Failure Threshold |
|---|---|---|
| Transition latency (p50) | ≤500ms | >2000ms |
| Transition latency (p95) | ≤1500ms | >2000ms |
| Transition latency (p99) | ≤2000ms | >2000ms |
Resource limits:
- CPU: ≤80%
- Memory: ≤6GB total (semantic module)
PT-02: Sustained L1 Sweep Does Not Degrade YOLO Throughput
Summary: Verify that running the full behavior tree with L1 sweep does not reduce YOLO inference FPS below baseline.
Traces to: AC-27
Load scenario:
- Baseline: YOLO only, 30 FPS, 300 frames
- Test: YOLO + ScanController L1 sweep, 30 FPS, 300 frames
- Duration: 10 seconds each
- Ramp-up: none
Expected results:
| Metric | Target | Failure Threshold |
|---|---|---|
| FPS delta (baseline vs test) | ≤5% reduction | >10% reduction |
| Frame drop rate | ≤1% | >5% |
Resource limits:
- GPU memory: ≤2.5GB for YOLO engine
- CPU: ≤60% for tree overhead
Security Tests
ST-01: Health Endpoint Does Not Expose Sensitive Data
Summary: Verify /api/v1/health response contains only operational metrics, no file paths, secrets, or internal state.
Traces to: AC-27
Attack vector: Information disclosure via health endpoint
Test procedure:
- GET /api/v1/health
- Parse response JSON
- Check no field contains file system paths, config values, or credentials
Expected behavior: Response contains only status, readiness booleans, temperature, capability flags, counters.
Pass criteria: No field value matches regex for file paths (/[a-z]+/), env vars, or credential patterns.
Fail criteria: Any file path, secret, or config detail in response body.
ST-02: Detect Endpoint Input Validation
Summary: Verify /api/v1/detect rejects malformed input gracefully.
Traces to: AC-22
Attack vector: Denial of service via oversized or malformed frame submission
Test procedure:
- POST /api/v1/detect with empty body → expect 400
- POST /api/v1/detect with 100MB payload → expect 413
- POST /api/v1/detect with non-image content type → expect 415
Expected behavior: Server returns appropriate HTTP error codes, does not crash.
Pass criteria: All 3 requests return expected error codes; server remains operational.
Fail criteria: Server crashes, hangs, or returns 500.
Acceptance Tests
AT-01: Full L1 Sweep Covers Angle Range
Summary: Verify the sweep completes from -sweep_angle_range to +sweep_angle_range and wraps around.
Traces to: AC-09
Preconditions:
- System running with mock gimbal in dev environment
- Config: sweep_angle_range=45, sweep_step=5
Steps:
| Step | Action | Expected Result |
|---|---|---|
| 1 | Start system | L1Sweep active, scan_angle starts at -45 |
| 2 | Let system run for 18+ ticks | Scan angle reaches +45 |
| 3 | Next tick | Scan angle wraps back to -45 |
AT-02: POI Detection for All Trigger Classes
Summary: Verify that each configured trigger class type produces a POI when detected.
Traces to: AC-10
Preconditions:
- All search scenarios enabled
- Mock Tier1 returns one detection per trigger class sequentially
Steps:
| Step | Action | Expected Result |
|---|---|---|
| 1 | Inject footpath_winter detection (conf=0.7) | POI created with scenario=winter_concealment |
| 2 | Inject branch_pile detection (conf=0.6) | POI created with scenario=winter_concealment |
| 3 | Inject building_block detection (conf=0.8) | POI created with scenario=building_area_search |
| 4 | Inject radar_dish + aa_launcher (conf=0.5 each, within 200px) | Cluster POI created with scenario=aa_defense_network |
AT-03: End-to-End L1→L2→L1 Cycle
Summary: Verify complete investigation lifecycle from POI detection to return to sweep.
Traces to: AC-11
Preconditions:
- System running with mock components
- winter_concealment scenario active
Steps:
| Step | Action | Expected Result |
|---|---|---|
| 1 | Inject footpath_winter detection | POI queued, L2Investigation starts |
| 2 | Wait for zoom_to_poi | GimbalDriver zooms to POI location |
| 3 | Tier2.trace_mask returns waypoints | PathFollowSubtree engages |
| 4 | Investigation completes | GimbalDriver.return_to_sweep called |
| 5 | Next tick | L1Sweep resumes |
AT-04: L2 Camera Lock During Investigation
Summary: Verify gimbal maintains zoom and tracking during L2 investigation.
Traces to: AC-12
Preconditions:
- L2Investigation active on a POI
Steps:
| Step | Action | Expected Result |
|---|---|---|
| 1 | L2 investigation starts | Gimbal zoomed to POI |
| 2 | Monitor gimbal state during investigation | Zoom level remains constant |
| 3 | Investigation timeout reached | return_to_sweep called (not before) |
AT-05: Path Following Stays Centered
Summary: Verify gimbal PID follows path trajectory keeping the path centered.
Traces to: AC-13
Preconditions:
- PathFollowSubtree active with a mock skeleton trajectory
Steps:
| Step | Action | Expected Result |
|---|---|---|
| 1 | PIDFollow starts | follow_path called with trajectory direction |
| 2 | Multiple PID updates (10 cycles) | Direction updates sent to GimbalDriver |
| 3 | Path trajectory ends | PIDFollow returns SUCCESS |
AT-06: VLM Analysis at Path Endpoint
Summary: Verify VLM is invoked for ambiguous endpoint classifications.
Traces to: AC-14
Preconditions:
- PathFollowSubtree at WaypointAnalysis step
- Waypoint confidence below threshold
- VLM available
Steps:
| Step | Action | Expected Result |
|---|---|---|
| 1 | WaypointAnalysis evaluates endpoint | HighConfidence condition fails |
| 2 | AmbiguousWithVLM sequence begins | CheckVLMAvailable returns SUCCESS |
| 3 | RunVLM action | VLMClient.analyze called, response received |
| 4 | LogDetection | Detection logged with tier=3 |
AT-07: Timeout Returns to L1
Summary: Verify investigation times out and returns to L1 when timeout expires.
Traces to: AC-15
Preconditions:
- L2Investigation active
- Config: investigation_timeout_s=5
- Mock Tier2 returns long-running analysis
Steps:
| Step | Action | Expected Result |
|---|---|---|
| 1 | L2DetectLoop starts | Investigation proceeds |
| 2 | 5 seconds elapse | L2DetectLoop repeat terminates |
| 3 | ReportToOperator called | Partial results reported |
| 4 | ReturnToSweep | GimbalDriver.return_to_sweep called |
Test Data Management
Required test data:
| Data Set | Description | Source | Size |
|---|---|---|---|
| mock_detections | Pre-defined detection lists per scenario type | Generated fixtures | ~10 KB |
| mock_spatial_results | SpatialAnalysisResult objects with waypoints | Generated fixtures | ~5 KB |
| mock_vlm_responses | VLMResponse objects for endpoint analysis | Generated fixtures | ~2 KB |
| scenario_configs | YAML search scenario configurations (valid + invalid) | Generated fixtures | ~3 KB |
Setup procedure:
- Load mock component implementations that return fixture data
- Initialize BT with test config
- Set blackboard variables to known state
Teardown procedure:
- Shutdown tree
- Clear blackboard
- Reset mock call counters
Data isolation strategy: Each test initializes a fresh BT instance with clean blackboard. No shared mutable state between tests.