import XCTest
import PhysarumEngine
@testable import Physarum
/// T2b: the ATV controller holds phone-streamed audio factors for the render loop and relaxes to
/// neutral on stream drop (staleness) or disconnect — never frozen on the last beat.
final class AmbientControllerAudioTests: XCTestCase {
private func make(staleAfter: Double = 0.5) -> AmbientController {
let factory = PresetSlots.Factory(presets: [PresetCatalog.fallbackPreset], version: 1)
let s = AppState(slotStore: SlotStore(backend: InMemorySlotBackend(), factory: factory))
return AmbientController(appState: s, fadeDuration: 0.5, blackHold: 0.2, dwell: 2.0, audioStaleAfter: staleAfter)
}
private var frame: AudioFactorFrame {
var f = AudioFactors(); f[.moveSpeed] = 1.5; return AudioFactorFrame(f)
}
func testReceiveHoldsFrame() {
let c = make()
c.receiveAudioFactors(frame)
XCTAssertEqual(c.remoteAudio, frame)
}
func testStaleClearsToNeutral() {
let c = make(staleAfter: 0.5)
c.receiveAudioFactors(frame)
c.tick(0.3); XCTAssertEqual(c.remoteAudio, frame) // still fresh
c.tick(0.3) // total 0.6 > 0.5 → stale
XCTAssertNil(c.remoteAudio)
}
func testFreshFrameResetsStaleTimer() {
let c = make(staleAfter: 0.5)
c.receiveAudioFactors(frame)
c.tick(0.4)
c.receiveAudioFactors(frame) // reset
c.tick(0.4) // 0.4 since the reset < 0.5
XCTAssertEqual(c.remoteAudio, frame)
}
func testDisconnectClearsAudio() {
let c = make()
c.receiveAudioFactors(frame)
c.resumeAutonomousCycle() // disconnect path
XCTAssertNil(c.remoteAudio)
}
}