feat(apple/library): experimental game-library browser (flagged off)
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 31s
apple / swift (push) Successful in 1m15s
ci / rust (push) Successful in 2m4s
ci / bench (push) Successful in 1m38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m23s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m55s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m28s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 31s
apple / swift (push) Successful in 1m15s
ci / rust (push) Successful in 2m4s
ci / bench (push) Successful in 1m38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m23s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m55s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m28s
Plan step 3 — the Apple client surfaces the host's game library, behind a feature flag (`DefaultsKey.libraryEnabled`, default OFF). Browsing only; launching a chosen title is step 4. - PunktfunkKit `LibraryClient`: Codable GameEntry/Artwork/LaunchSpec mirroring crates/punktfunk-host/src/library.rs, and an async fetch of GET /api/v1/library with a bearer token. Typed LibraryError guides setup (the common case is "needs a --mgmt-token"). `Artwork.posterCandidates` = portrait → header → hero. - `LibraryView`: cross-platform poster grid (LazyVGrid, AsyncImage that walks the art candidates past load failures to a text placeholder), a store badge, and an inline Connection form (mgmt port + token) that surfaces when the API is unreachable / 401 / no token set. Read-only. - StoredHost gains `mgmtPort`/`mgmtToken` (the mgmt API is a distinct port from the data plane and needs a token off-loopback). Both OPTIONAL — synthesized Decodable ignores property defaults but treats a missing Optional as nil, so older saved hosts decode unchanged (a defaulted non-optional would wipe the list). HostStore.setMgmt. - Entry point: a flag-gated "Browse Library…" host-card context action → LibraryView (sheet on macOS/iOS, pushed on tvOS), mirroring the pair/speed-test plumbing. Plus a Settings "Experimental" toggle. Can't compile Swift on the Linux dev box; CI (apple.yml: swift build + swift test on the mac mini) verifies the macOS path. Added LibraryClientTests (decode + art order) for `swift test`. iOS/tvOS-only branches mirror existing patterns. Live-verify on the Mac pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// Unit tests for the game-library models — decoding the management API's GET /api/v1/library
|
||||
// payload and the poster-art fallback order. (The network fetch itself isn't unit-tested; it's
|
||||
// exercised live against a host.)
|
||||
|
||||
import XCTest
|
||||
@testable import PunktfunkKit
|
||||
|
||||
final class LibraryClientTests: XCTestCase {
|
||||
func testDecodesLibraryPayload() throws {
|
||||
// A Steam entry (full art + launch) and a custom entry (sparse art, no launch) — the two
|
||||
// shapes the host's `GameEntry` serializes (note the host omits null fields).
|
||||
let json = """
|
||||
[
|
||||
{
|
||||
"id": "steam:570",
|
||||
"store": "steam",
|
||||
"title": "Dota 2",
|
||||
"art": {
|
||||
"portrait": "https://cdn.cloudflare.steamstatic.com/steam/apps/570/library_600x900.jpg",
|
||||
"hero": "https://cdn.cloudflare.steamstatic.com/steam/apps/570/library_hero.jpg",
|
||||
"logo": "https://cdn.cloudflare.steamstatic.com/steam/apps/570/logo.png",
|
||||
"header": "https://cdn.cloudflare.steamstatic.com/steam/apps/570/header.jpg"
|
||||
},
|
||||
"launch": { "kind": "steam_appid", "value": "570" }
|
||||
},
|
||||
{
|
||||
"id": "custom:abc123",
|
||||
"store": "custom",
|
||||
"title": "Dolphin",
|
||||
"art": { "header": "https://example.com/dolphin.jpg" }
|
||||
}
|
||||
]
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let games = try JSONDecoder().decode([GameEntry].self, from: json)
|
||||
XCTAssertEqual(games.count, 2)
|
||||
|
||||
let steam = games[0]
|
||||
XCTAssertEqual(steam.id, "steam:570")
|
||||
XCTAssertFalse(steam.isCustom)
|
||||
XCTAssertEqual(steam.launch?.kind, "steam_appid")
|
||||
XCTAssertEqual(steam.launch?.value, "570")
|
||||
|
||||
let custom = games[1]
|
||||
XCTAssertTrue(custom.isCustom)
|
||||
XCTAssertNil(custom.launch)
|
||||
XCTAssertNil(custom.art.portrait)
|
||||
}
|
||||
|
||||
func testPosterCandidatesPreferPortraitThenHeader() {
|
||||
let full = Artwork(
|
||||
portrait: "https://x/p.jpg", hero: "https://x/hero.jpg",
|
||||
logo: "https://x/logo.png", header: "https://x/h.jpg")
|
||||
XCTAssertEqual(full.posterCandidates.map(\.absoluteString),
|
||||
["https://x/p.jpg", "https://x/h.jpg", "https://x/hero.jpg"])
|
||||
|
||||
// No portrait → header leads; absent fields are skipped, not nil-padded.
|
||||
let sparse = Artwork(portrait: nil, hero: nil, logo: nil, header: "https://x/h.jpg")
|
||||
XCTAssertEqual(sparse.posterCandidates.map(\.absoluteString), ["https://x/h.jpg"])
|
||||
|
||||
XCTAssertTrue(Artwork().posterCandidates.isEmpty)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user