import XCTest @testable import TVAnarchyCore final class TransferHealthTests: XCTestCase { func testStatusMapping() { func h(_ status: Int, complete: Bool = false, rate: Int = 0) -> TransferHealth { TransferHealth.classify(status: status, isComplete: complete, rateDownload: rate, peersConnected: 5, error: 0, secondsStuck: nil) } XCTAssertEqual(h(6), .seeding) XCTAssertEqual(h(0, complete: true), .done) XCTAssertEqual(h(0, complete: false), .stopped) XCTAssertEqual(h(1), .checking) XCTAssertEqual(h(3), .queued) XCTAssertEqual(h(4, rate: 1_000), .downloading) } func testErrorWins() { // Tracker error (2) and local error (3) are genuine → errored, beating an // otherwise-healthy downloading state. XCTAssertEqual( TransferHealth.classify(status: 4, isComplete: false, rateDownload: 5_000, peersConnected: 9, error: 3, secondsStuck: 0), .errored) XCTAssertEqual( TransferHealth.classify(status: 4, isComplete: false, rateDownload: 5_000, peersConnected: 9, error: 2, secondsStuck: 0), .errored) } /// A tracker *warning* (error == 1) is one tracker flapping on an otherwise /// healthy torrent — must NOT red-flag/notify (would spam on benign warnings). func testTrackerWarningIsNotErrored() { // Healthy downloading torrent that carries a stale tracker warning. XCTAssertEqual( TransferHealth.classify(status: 4, isComplete: false, rateDownload: 5_000, peersConnected: 9, error: 1, secondsStuck: 0), .downloading) // A seeding torrent with a tracker warning is still just seeding. XCTAssertEqual( TransferHealth.classify(status: 6, isComplete: true, rateDownload: 0, peersConnected: 0, error: 1, secondsStuck: nil), .seeding) XCTAssertFalse(TransferHealth.seeding.needsAttention) } func testStalledVsDeadThresholds() { // 0 rate, peers present, past the stall window → stalled. XCTAssertEqual( TransferHealth.classify(status: 4, isComplete: false, rateDownload: 0, peersConnected: 4, error: 0, secondsStuck: 130), .stalled) // 0 rate, NO peers, past the dead window → dead. XCTAssertEqual( TransferHealth.classify(status: 4, isComplete: false, rateDownload: 0, peersConnected: 0, error: 0, secondsStuck: 400), .dead) // briefly 0 rate, not yet past stall → still downloading (no false alarm). XCTAssertEqual( TransferHealth.classify(status: 4, isComplete: false, rateDownload: 0, peersConnected: 0, error: 0, secondsStuck: 10), .downloading) } func testAttentionAndSortOrder() { XCTAssertTrue(TransferHealth.errored.needsAttention) XCTAssertTrue(TransferHealth.dead.needsAttention) XCTAssertFalse(TransferHealth.downloading.needsAttention) // attention states sort ahead of active work, which sorts ahead of idle. XCTAssertLessThan(TransferHealth.dead.sortRank, TransferHealth.downloading.sortRank) XCTAssertLessThan(TransferHealth.downloading.sortRank, TransferHealth.seeding.sortRank) } @MainActor func testDownloadOrderAttentionFirstThenEta() { func row(_ id: Int, eta: Int) -> TorrentRow { try! JSONDecoder().decode(TorrentRow.self, from: Data(#""" {"id":\#(id),"name":"t\#(id)","percentDone":0.5,"status":4,"doneDate":0,"addedDate":1, "haveValid":1,"sizeWhenDone":2,"rateDownload":1,"rateUpload":0,"uploadRatio":0, "eta":\#(eta),"downloadDir":"/d"} """#.utf8)) } let soon = row(1, eta: 60), later = row(2, eta: 600), dead = row(3, eta: -1) // dead needs attention → first regardless of ETA; then soonest ETA. XCTAssertTrue(DownloadsController.downloadOrder(dead, .dead, soon, .downloading)) XCTAssertTrue(DownloadsController.downloadOrder(soon, .downloading, later, .downloading)) XCTAssertFalse(DownloadsController.downloadOrder(later, .downloading, soon, .downloading)) } /// A row from a pre-Part-E cli (no error/peers fields) still decodes + classifies. func testRowWithoutDebugFieldsDecodesAndClassifies() throws { let json = #""" {"id":1,"name":"X","percentDone":0.5,"status":4,"doneDate":0,"addedDate":1, "haveValid":1,"sizeWhenDone":2,"rateDownload":1000,"rateUpload":0, "uploadRatio":0,"eta":60,"downloadDir":"/d"} """# let row = try JSONDecoder().decode(TorrentRow.self, from: Data(json.utf8)) XCTAssertNil(row.error) XCTAssertNil(row.peersConnected) XCTAssertEqual(row.health(), .downloading) } }