import XCTest import Foundation import ForgeGrade import ForgeColor @testable import ForgePanel final class ControlMapTests: XCTestCase { // Trackball delta maps to CDL offset per ball (lift/gamma/gain zones map to SOP). func testTrackballToCDL() { var grade = PanelGradeState() let map = ControlMap.waveDefault // Gain ball (ball 3) X delta -> slope red+. let action = map.action(for: .encoder(id: TangentControls.gainBallX, delta: 10)) guard case .adjustCDL(let param, let amount) = action else { return XCTFail("wrong action") } XCTAssertEqual(param, .slopeRed) XCTAssertEqual(amount, 10 * map.sensitivity(for: .slopeRed), accuracy: 1e-8) grade.apply(action!) XCTAssertEqual(grade.cdl.slope.x, 1 + 10 * map.sensitivity(for: .slopeRed), accuracy: 1e-6) } // Master ring adjusts all three slope channels. func testMasterRing() { var grade = PanelGradeState() let map = ControlMap.waveDefault let action = map.action(for: .encoder(id: TangentControls.gainRing, delta: 5)) guard case .adjustCDL(let param, _) = action, param == .slopeMaster else { return XCTFail("wrong mapping") } grade.apply(action!) XCTAssertEqual(grade.cdl.slope.x, grade.cdl.slope.y, accuracy: 1e-7) XCTAssertEqual(grade.cdl.slope.y, grade.cdl.slope.z, accuracy: 1e-7) XCTAssertGreaterThan(grade.cdl.slope.x, 1) } // Sat knob. func testSatKnob() { var grade = PanelGradeState() let map = ControlMap.waveDefault let action = map.action(for: .encoder(id: TangentControls.satKnob, delta: -20)) grade.apply(action!) XCTAssertLessThan(grade.cdl.saturation, 1) } // Buttons dispatch commands. func testButtons() { let map = ControlMap.waveDefault XCTAssertEqual(map.action(for: .button(id: TangentControls.bypassButton, pressed: true)), .toggleBypass) XCTAssertEqual(map.action(for: .button(id: TangentControls.grabStillButton, pressed: true)), .grabStill) XCTAssertEqual(map.action(for: .button(id: TangentControls.nextSlotButton, pressed: true)), .nextSlot) XCTAssertEqual(map.action(for: .button(id: TangentControls.resetButton, pressed: true)), .resetGrade) // Release events ignored. XCTAssertNil(map.action(for: .button(id: TangentControls.bypassButton, pressed: false))) } // Unmapped control -> nil. func testUnmappedControl() { XCTAssertNil(ControlMap.waveDefault.action(for: .encoder(id: 9999, delta: 1))) } // Reset action restores identity. func testReset() { var grade = PanelGradeState() grade.apply(.adjustCDL(param: .slopeRed, amount: 0.5)) grade.apply(.adjustCDL(param: .offsetBlue, amount: -0.1)) XCTAssertNotEqual(grade.cdl, CDL.identity) grade.apply(.resetGrade) XCTAssertEqual(grade.cdl, CDL.identity) } // Display text for current params (panel readout strings). func testDisplayText() { var grade = PanelGradeState() grade.apply(.adjustCDL(param: .slopeRed, amount: 0.234)) let text = grade.displayText(for: .slopeRed) XCTAssertEqual(text, "SlpR 1.234") } } final class TangentProtocolTests: XCTestCase { // Frame encode: u32 LE length + JSON payload. Round trip. func testFraming() throws { let msg = TangentMessage.controlUpdate(.encoder(id: 42, delta: -3)) let framed = TangentWire.encode(msg) var buf = framed let decoded = try XCTUnwrap(TangentWire.decodeFirst(&buf)) guard case .controlUpdate(.encoder(let id, let delta)) = decoded else { return XCTFail("wrong decode") } XCTAssertEqual(id, 42) XCTAssertEqual(delta, -3) XCTAssertTrue(buf.isEmpty) } // Partial frames buffer until complete. func testPartialFrames() throws { let msg = TangentMessage.controlUpdate(.button(id: 7, pressed: true)) let framed = TangentWire.encode(msg) var buf = Data(framed.prefix(3)) XCTAssertNil(try TangentWire.decodeFirst(&buf)) buf.append(framed.suffix(from: 3)) XCTAssertNotNil(try TangentWire.decodeFirst(&buf)) } // Two frames in one read both decode. func testTwoFramesOneBuffer() throws { var buf = TangentWire.encode(.controlUpdate(.encoder(id: 1, delta: 1))) buf.append(TangentWire.encode(.controlUpdate(.encoder(id: 2, delta: 2)))) let first = try XCTUnwrap(TangentWire.decodeFirst(&buf)) let second = try XCTUnwrap(TangentWire.decodeFirst(&buf)) guard case .controlUpdate(.encoder(let id1, _)) = first, case .controlUpdate(.encoder(let id2, _)) = second else { return XCTFail() } XCTAssertEqual(id1, 1) XCTAssertEqual(id2, 2) } // Display update encodes. func testDisplayUpdate() throws { let msg = TangentMessage.displayUpdate(line: 0, text: "SlpR 1.234") var buf = TangentWire.encode(msg) let decoded = try XCTUnwrap(TangentWire.decodeFirst(&buf)) guard case .displayUpdate(let line, let text) = decoded else { return XCTFail() } XCTAssertEqual(line, 0) XCTAssertEqual(text, "SlpR 1.234") } // Garbage payload throws. func testGarbageRejected() { var lenBytes = Data() var len = UInt32(4).littleEndian withUnsafeBytes(of: &len) { lenBytes.append(contentsOf: $0) } var buf = lenBytes + Data("zzzz".utf8) XCTAssertThrowsError(try TangentWire.decodeFirst(&buf)) } }