Zum Inhalt

apple/PhysarumApp/Tests/ThumbnailStorageTests.swift

import XCTest
@testable import Physarum

final class ThumbnailStorageTests: XCTestCase {
    private func tmpStore() -> PresetStore {
        let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        return PresetStore(baseDir: dir)
    }

    func testWriteAndResolveCustomThumbnail() throws {
        let store = tmpStore()
        let png = Data([0x89, 0x50, 0x4E, 0x47])
        try store.writeThumbnail(png, for: "custom-x")
        let url = try XCTUnwrap(store.thumbnailURL(for: "custom-x"))
        XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
    }

    func testCatalogPrefersCustomOverBundle() throws {
        let store = tmpStore()
        try store.writeThumbnail(Data([1, 2, 3]), for: "custom-y")
        let url = PresetCatalog.thumbnailURL(for: "custom-y", store: store)
        XCTAssertEqual(url?.lastPathComponent, "custom-y.png")
    }

    func testPresetsMissingThumbnail() throws {
        let store = tmpStore()
        try store.writeThumbnail(Data([1]), for: "has")
        let missing = store.presetsMissingThumbnail(ids: ["has", "nope"])
        XCTAssertEqual(missing, ["nope"])
    }

    func testBackfillTargetsOnlyMissing() throws {
        let store = tmpStore()
        try store.writeThumbnail(Data([1]), for: "a")
        XCTAssertEqual(store.presetsMissingThumbnail(ids: ["a", "b", "c"]), ["b", "c"])
    }

    // A factory slot's preset id is `slot-NN` after the cutover, but it carries the original content
    // id in `thumbnail`, so the curated bundled art still resolves (no slot-NN.png in the bundle).
    func testFactorySlotResolvesBundledArtViaThumbnailKey() {
        var p = PresetCatalog.fallbackPreset
        p.id = "slot-00"; p.thumbnail = "aurora-drift"
        let url = PresetCatalog.thumbnailURL(for: p, store: tmpStore())   // empty store, no slot-00.png
        XCTAssertEqual(url?.lastPathComponent, "aurora-drift.png")
    }

    // Once the user saves/edits a slot, the local render (keyed by content hash) wins over the bundled key.
    func testUserSlotRenderWinsOverBundledKey() throws {
        let store = tmpStore()
        var p = PresetCatalog.fallbackPreset
        p.id = "slot-00"; p.thumbnail = "aurora-drift"
        let key = PresetCatalog.thumbnailKey(for: p)
        try store.writeThumbnail(Data([0x89, 0x50]), for: key)
        let url = PresetCatalog.thumbnailURL(for: p, store: store)
        XCTAssertEqual(url?.lastPathComponent, "\(key).png")   // content-hash render wins
    }

    func testThumbnailKeyIsStableAndContentSensitive() {
        let a = PresetCatalog.fallbackPreset.params
        var b = a; b.bloomIntensity += 0.5
        XCTAssertEqual(PresetCatalog.thumbnailKey(forParams: a), PresetCatalog.thumbnailKey(forParams: a))   // stable
        XCTAssertNotEqual(PresetCatalog.thumbnailKey(forParams: a), PresetCatalog.thumbnailKey(forParams: b)) // any change → new key
    }

    // The core fix: a changed look invalidates the old render (content-addressed), so no stale preview.
    func testChangedParamsInvalidateThumbnail() throws {
        let store = tmpStore()
        var p = PresetCatalog.fallbackPreset; p.id = "slot-00"; p.thumbnail = nil   // a user slot
        try store.writeThumbnail(Data([1]), for: PresetCatalog.thumbnailKey(for: p))
        XCTAssertNotNil(PresetCatalog.thumbnailURL(for: p, store: store))    // resolves to its render
        p.params.bloomIntensity += 0.5                                       // change the look
        XCTAssertNil(PresetCatalog.thumbnailURL(for: p, store: store))       // stale render no longer resolves
    }

    func testPruneThumbnailsDropsOrphans() throws {
        let store = tmpStore()
        try store.writeThumbnail(Data([1]), for: "h-keep")
        try store.writeThumbnail(Data([2]), for: "h-orphan")
        store.pruneThumbnails(keeping: ["h-keep"])
        XCTAssertNotNil(store.thumbnailURL(for: "h-keep"))
        XCTAssertNil(store.thumbnailURL(for: "h-orphan"))
    }
}