tv-anarchy/Tests/TVAnarchyCoreTests/DeviceConfigDecodeTests.swift

116 lines
5.7 KiB
Swift

import XCTest
@testable import TVAnarchyCore
/// GAP-5 regression: the schema must stay strictly additive, because
/// `loadOrSeed()` OVERWRITES hosts.json on any decode failure. Old configs must
/// still decode; new ones decode with defaults; round-trips preserve fields.
final class DeviceConfigDecodeTests: XCTestCase {
func testLegacyConfigStillDecodes() throws {
let json = #"""
{"hosts":[
{"id":"plum-vlc","name":"Plum VLC","kind":"vlc","vlc":{"host":"127.0.0.1","port":8080}},
{"id":"black","name":"Black TV","kind":"blacktv",
"ssh":{"endpoints":["lilith@10.0.0.11","lilith@10.9.0.4"],"bin":"/usr/local/bin/black-tv"}}
]}
"""#
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
XCTAssertEqual(cfg.devices.count, 2)
XCTAssertEqual(cfg.devices[0].kind, .vlc)
XCTAssertEqual(cfg.devices[1].kind, .blacktv)
XCTAssertEqual(cfg.devices[1].ssh?.bin, "/usr/local/bin/black-tv")
}
func testMpvIPCConfigDecodesWithDefaults() throws {
// Minimal mpv block socket/sudo/socat/volumeScale should default.
let json = #"""
{"hosts":[
{"id":"black","name":"Black TV","kind":"mpv-ipc",
"mpv":{"endpoints":["lilith@10.9.0.4"]},
"commands":{"launchShow":["btv","play-show","{query}"],"stop":["btv","stop"]}}
]}
"""#
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
let h = cfg.devices[0]
XCTAssertEqual(h.kind, .mpvIPC)
XCTAssertEqual(h.mpv?.socket, "/tmp/mpv.sock")
XCTAssertEqual(h.mpv?.sudo, true)
XCTAssertEqual(h.mpv?.socat, "socat")
XCTAssertEqual(h.mpv?.volumeScale, 130)
XCTAssertEqual(h.commands?.stop, ["btv", "stop"])
XCTAssertNil(h.commands?.releases) // unspecified capability nil
}
@MainActor
func testLegacyBlacktvMigratesToMpvTarget() {
// An old `blacktv` host must still build a working target the generic
// MpvTarget, with delegated commands derived from the old `bin`.
let h = DeviceConfig(id: "black", name: "Black", kind: .blacktv,
ssh: SSHConn(endpoints: ["lilith@10.9.0.4"], bin: "/usr/local/bin/black-tv"))
let t = PlayerController.makeTarget(h)
XCTAssertNotNil(t)
XCTAssertEqual(t?.kind, .mpvIPC)
XCTAssertTrue(t is MpvTarget)
}
func testSeedRoundTrips() throws {
let seed = DevicesConfig.seeded()
let data = try JSONEncoder().encode(seed)
let back = try JSONDecoder().decode(DevicesConfig.self, from: data)
XCTAssertEqual(back.devices.map(\.kind), seed.devices.map(\.kind))
XCTAssertEqual(back.devices.first(where: { $0.kind == .mpvIPC })?.commands?.launchFile,
["/usr/local/bin/black-tv", "play", "{path}"])
XCTAssertEqual(back.devices.map(\.type), [.laptop, .storage])
}
// MARK: device type + services (Part B)
/// A pre-`type` config must infer each device's type from its player backend,
/// NOT silently default to the first enum case: a local VLC laptop, an
/// mpv/blacktv (black) storage. Services follow the inferred type's preset.
func testLegacyConfigInfersDeviceType() throws {
let json = #"""
{"hosts":[
{"id":"plum-vlc","name":"Plum VLC","kind":"vlc","vlc":{"host":"127.0.0.1","port":8080}},
{"id":"black","name":"Black TV","kind":"mpv-ipc","mpv":{"endpoints":["lilith@10.0.0.11"]}}
]}
"""#
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
XCTAssertEqual(cfg.devices[0].type, .laptop) // local player
XCTAssertEqual(cfg.devices[1].type, .storage) // mpv-over-ssh server
XCTAssertTrue(cfg.devices[0].services.stream) // laptop streams
XCTAssertTrue(cfg.devices[1].services.stream) // storage ALSO streams (black)
XCTAssertTrue(cfg.devices[1].services.custody) // and holds copies
}
func testDeviceTypePresets() {
XCTAssertEqual(DeviceType.cellphone.defaultServices,
DeviceServices(stream: true, offlineCache: true))
XCTAssertEqual(DeviceType.laptop.defaultServices,
DeviceServices(stream: true, offlineCache: true, ttlSeed: true))
// storage + seed: custody and stream are independent, not either/or.
XCTAssertTrue(DeviceType.storage.defaultServices.stream)
XCTAssertTrue(DeviceType.storage.defaultServices.custody)
XCTAssertFalse(DeviceType.seed.defaultServices.stream)
XCTAssertTrue(DeviceType.seed.defaultServices.publicSwarmFace)
// broadcast = the mesh anchor: registry + relay + swarm face, no streaming.
let b = DeviceType.broadcast.defaultServices
XCTAssertTrue(b.meshAnchor && b.f2fRelay && b.publicSwarmFace)
XCTAssertFalse(b.stream)
XCTAssertEqual(DeviceType.broadcast.fleetClass, "broadcast")
}
/// An explicit, overridden services set is preserved (not re-derived from type).
func testExplicitServicesOverrideSurvivesDecode() throws {
let json = #"""
{"devices":[
{"id":"phone","name":"Phone","kind":"quicktime","type":"cellphone",
"services":{"stream":true,"offlineCache":false,"ttlSeed":true}}
]}
"""#
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
let s = cfg.devices[0].services
XCTAssertEqual(cfg.devices[0].type, .cellphone)
XCTAssertFalse(s.offlineCache) // override beat the preset's `true`
XCTAssertTrue(s.ttlSeed)
}
}