import XCTest @testable import HaptikaSignal final class PPGCoherencePolicyTests: XCTestCase { func testSessionDurationLimitsRemainPurposeSpecific() { XCTAssertEqual(PPGCoherencePolicy.maximumDurationSeconds(isCoherence: false), 120) XCTAssertEqual(PPGCoherencePolicy.maximumDurationSeconds(isCoherence: true), 300) XCTAssertEqual(PPGCoherencePolicy.coherenceBufferDurationSeconds, 75) } func testLiveTrackerPublishesEveryNewBeatOnlyOnce() { var tracker = PPGLiveBeatTracker() let first = tracker.ingest([ interval(end: 1, duration: 800), interval(end: 1.8, duration: 800), ]) XCTAssertEqual(first.map(\.sequence), [1, 2]) let overlapping = tracker.ingest([ interval(end: 1.82, duration: 800), interval(end: 2.6, duration: 780), ]) XCTAssertEqual(overlapping.map(\.sequence), [3]) XCTAssertEqual(overlapping.first?.timestampSeconds, 2.6) } func testTrackerDoesNotMoveBackwardAndResetsCleanly() { var tracker = PPGLiveBeatTracker() _ = tracker.ingest([interval(end: 10, duration: 700)]) XCTAssertTrue(tracker.ingest([interval(end: 9.9, duration: 700)]).isEmpty) tracker.reset() XCTAssertEqual(tracker.ingest([interval(end: 1, duration: 700)]).first?.sequence, 1) } func testTrackerRejectsInvalidAndRejectedIntervals() { var tracker = PPGLiveBeatTracker() let rejected = PulseInterval( sourceIndex: 0, startSeconds: 0, endSeconds: 1, durationMilliseconds: 800, rejectionReason: .localOutlier, qualityScore: 1 ) XCTAssertTrue(tracker.ingest([ rejected, interval(end: .nan, duration: 800), interval(end: 2, duration: 2_500), ]).isEmpty) } private func interval(end: Double, duration: Double) -> PulseInterval { PulseInterval( sourceIndex: 0, startSeconds: end - duration / 1_000, endSeconds: end, durationMilliseconds: duration, qualityScore: 0.9 ) } }