Files
Jarvis/packages/Vision/Tests/VisionTests/CameraCaptureRealHardwareTests.swift
james.maesandClaude Opus 4.7 19362f6988 test(track-c 6): real-hardware vision integration test (XCTSkipIf no camera)
Closes vision-audit §7 critical gap: "zero tests exercise
CameraCapture.captureFrame()'s success path. No test asserts that a
non-trivial JPEG comes back. The 1×1 stub passes every test in this
package."

CameraCaptureRealHardwareTests:
- testRealCameraCaptureProducesNonTrivialJPEG: opens a real session,
  invokes captureFrame() through the production photoOutput delegate
  flow (Track-C 1), and asserts: jpeg byte count > 1000, SOI marker
  prefix [0xFF, 0xD8], width > 1, height > 1. The historical 1×1 stub
  was 125 bytes — the > 1000 threshold and width/height > 1 guards
  make a regression to a trivial fixture trip the test.
- testRealCameraFrameStreamYieldsSamplesWithinTimeout: subscribes to
  frameStream(forPresence: true) (Track-C 2 fan-out) and asserts a
  PresenceFrameSample.sampleBuffer arrives within 2 seconds of opening
  the session.

Gating (skip paths, never prompts):
1. JARVIS_REAL_CAMERA=1 must be set in the environment (matches the
   JARVIS_REAL_MODELS convention used by Memory + Voice hardware
   tests).
2. AVCaptureDevice.default(for: .video) must be non-nil.
3. AVCaptureDevice.authorizationStatus(for: .video) must be
   .authorized — the test does NOT call requestAccess(for:), so a
   headless / first-launch test runner never blocks on a TCC prompt.

Run interactively after granting Camera TCC to the test host:
   JARVIS_REAL_CAMERA=1 swift test --package-path packages/Vision \
     --filter CameraCaptureRealHardwareTests

Vision 70 → 72 (2 tests, both skip without env+TCC). Six boundary
gates pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:20:57 -05:00

110 lines
4.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
import AVFoundation
@testable import JarvisVision
/// Track-C 6 — real-hardware vision integration test.
///
/// Exercises the full Track-C 1..2 path on a host with a real camera and
/// pre-granted Camera TCC: open() → capturePhoto delegate → frameStream
/// receives video samples → both producers yield real bytes.
///
/// **Gating:** the test skips unless ALL of the following hold:
/// 1. `JARVIS_REAL_CAMERA=1` is set in the environment (matches the
/// `JARVIS_REAL_MODELS` convention used elsewhere in the test suite).
/// 2. `AVCaptureDevice.default(for: .video)` is non-nil.
/// 3. `AVCaptureDevice.authorizationStatus(for: .video) == .authorized`
/// — we deliberately do NOT call `requestAccess(for:)` from a test
/// so headless / first-launch test runs never block on a TCC prompt.
///
/// Run interactively after granting Camera TCC to the test host:
/// JARVIS_REAL_CAMERA=1 swift test --package-path packages/Vision \
/// --filter CameraCaptureRealHardwareTests
final class CameraCaptureRealHardwareTests: XCTestCase {
func testRealCameraCaptureProducesNonTrivialJPEG() async throws {
try Self.skipUnlessRealCameraAvailable()
let (degStream, degCont) = AsyncStream<VisionDegradationReason>.makeStream()
let capture = CameraCapture(degradationContinuation: degCont, degradationStream: degStream)
try await capture.open()
defer { Task { await capture.shutdown() } }
// Capture a frame via the real photoOutput delegate path.
let frame = try await capture.captureFrame()
// Real JPEG: at minimum a 4-byte SOI + APP0 marker prefix and a
// body more than the 1×1 stub's 125-byte payload.
XCTAssertGreaterThan(frame.jpegData.count, 1000,
"real camera frame must be larger than the historical 125-byte stub")
XCTAssertEqual(frame.jpegData.prefix(2), Data([0xFF, 0xD8]),
"real frame must carry the JPEG SOI marker")
XCTAssertGreaterThan(frame.width, 1,
"real camera frame width must exceed the 1×1 stub")
XCTAssertGreaterThan(frame.height, 1,
"real camera frame height must exceed the 1×1 stub")
_ = degStream
}
func testRealCameraFrameStreamYieldsSamplesWithinTimeout() async throws {
try Self.skipUnlessRealCameraAvailable()
let (degStream, degCont) = AsyncStream<VisionDegradationReason>.makeStream()
let capture = CameraCapture(degradationContinuation: degCont, degradationStream: degStream)
try await capture.open()
defer { Task { await capture.shutdown() } }
// Subscribe and wait up to 2s for the first sample buffer to arrive
// through the AVCaptureVideoDataOutput delegate fan-out.
let stream = capture.frameStream(forPresence: true)
let sample = await Self.firstSample(from: stream, timeout: 2.0)
XCTAssertNotNil(sample, "real camera must emit a video sample within 2s")
if let sample {
switch sample {
case .sampleBuffer:
break // expected production shape
case .cgImage, .syntheticDetection:
XCTFail("real camera must yield .sampleBuffer, not \(sample)")
}
}
_ = degStream
}
// MARK: - Gating helper
static func skipUnlessRealCameraAvailable() throws {
try XCTSkipUnless(
ProcessInfo.processInfo.environment["JARVIS_REAL_CAMERA"] == "1",
"Set JARVIS_REAL_CAMERA=1 to run hardware vision tests (requires a real camera and pre-granted Camera TCC)"
)
try XCTSkipIf(
AVCaptureDevice.default(for: .video) == nil,
"no camera device — skipping hardware test"
)
let status = AVCaptureDevice.authorizationStatus(for: .video)
try XCTSkipUnless(
status == .authorized,
"Camera TCC not granted (status=\(status.rawValue)); the test does NOT prompt — grant manually before running"
)
}
private static func firstSample(
from stream: AsyncStream<PresenceFrameSample>,
timeout: TimeInterval
) async -> PresenceFrameSample? {
let collector = Task<PresenceFrameSample?, Never> {
for await sample in stream { return sample }
return nil
}
let timer = Task<PresenceFrameSample?, Never> {
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
collector.cancel()
return nil
}
let result = await collector.value
timer.cancel()
return result
}
}