// Unit tests for gallery (de)serialisation: HDF5 round-trip fidelity (the only // format save_gallery writes), legacy JSON read back-compat (optional field // defaults, the legacy "jellyfin_person_id" fallback). GPU-free, model-free. #include #include "gallery/gallery_store.hpp" #include "types.hpp" #include #include #include #include namespace { struct TempFile { std::string path; explicit TempFile(const std::string& name) : path(std::string(std::tmpnam(nullptr)) + name) {} ~TempFile() { std::remove(path.c_str()); } }; Embedding make_embedding(float base) { Embedding e{}; for (int i = 0; i < 512; ++i) e[i] = base + i * 1e-4f; return e; } } // namespace TEST_CASE("gallery save/load round-trips actors and embeddings", "[gallery]") { ActorGallery g; ActorGallery::Actor a; a.imdb_id = "nm0000093"; a.tmdb_id = "287"; a.jellyfin_id = "guid-abc"; a.name = "Brad Pitt"; a.source_images = {"img1.jpg", "img2.jpg"}; a.embeddings = {make_embedding(0.1f), make_embedding(-0.2f)}; g.actors.push_back(a); ActorGallery::Actor b; b.name = "Edward Norton"; b.embeddings = {make_embedding(0.5f)}; g.actors.push_back(b); // save_gallery always writes HDF5 (see gallery_store.cpp); use a .h5 path // directly rather than relying on the "wrong extension gets rewritten" // fallback, which is a compatibility shim, not the intended usage. TempFile tf("gallery.h5"); save_gallery(tf.path, g); ActorGallery loaded = load_gallery(tf.path); REQUIRE(loaded.actors.size() == 2); const auto& la = loaded.actors[0]; CHECK(la.imdb_id == "nm0000093"); CHECK(la.tmdb_id == "287"); CHECK(la.jellyfin_id == "guid-abc"); CHECK(la.name == "Brad Pitt"); CHECK(la.source_images == std::vector{"img1.jpg", "img2.jpg"}); REQUIRE(la.embeddings.size() == 2); for (int i = 0; i < 512; ++i) { CHECK(la.embeddings[0][i] == a.embeddings[0][i]); CHECK(la.embeddings[1][i] == a.embeddings[1][i]); } CHECK(loaded.actors[1].name == "Edward Norton"); CHECK(loaded.actors[1].embeddings.size() == 1); } TEST_CASE("gallery save/load round-trips calibration", "[gallery]") { ActorGallery g; ActorGallery::Actor a; a.name = "Solo Actor"; a.embeddings = {make_embedding(0.1f)}; g.actors.push_back(a); g.calib_a = 12.5f; g.calib_b = -3.25f; g.calib_valid = true; g.calib_hash = 0xDEADBEEFULL; TempFile tf("gallery_calib.h5"); save_gallery(tf.path, g); ActorGallery loaded = load_gallery(tf.path); CHECK(loaded.calib_a == g.calib_a); CHECK(loaded.calib_b == g.calib_b); CHECK(loaded.calib_valid == g.calib_valid); CHECK(loaded.calib_hash == g.calib_hash); } TEST_CASE("gallery with no calibration loads with calib_hash 0", "[gallery]") { ActorGallery g; ActorGallery::Actor a; a.name = "No Calibration"; a.embeddings = {make_embedding(0.f)}; g.actors.push_back(a); // calib_hash left at its default (0) — save_gallery skips writing the // /calibration group entirely when there's nothing to persist yet. TempFile tf("gallery_nocalib.h5"); save_gallery(tf.path, g); ActorGallery loaded = load_gallery(tf.path); CHECK(loaded.calib_hash == 0); CHECK_FALSE(loaded.calib_valid); } TEST_CASE("load_gallery defaults optional string fields to empty", "[gallery]") { // Minimal actor: only the required name + embeddings. nlohmann::json j; j["actors"] = nlohmann::json::array(); nlohmann::json ja; ja["name"] = "Minimal"; ja["embeddings"] = nlohmann::json::array(); ja["embeddings"].push_back(std::vector(512, 0.25f)); j["actors"].push_back(ja); TempFile tf("gallery_min.json"); { std::ofstream out(tf.path); out << j.dump(); } ActorGallery g = load_gallery(tf.path); REQUIRE(g.actors.size() == 1); CHECK(g.actors[0].name == "Minimal"); CHECK(g.actors[0].imdb_id.empty()); CHECK(g.actors[0].tmdb_id.empty()); CHECK(g.actors[0].jellyfin_id.empty()); CHECK(g.actors[0].source_images.empty()); REQUIRE(g.actors[0].embeddings.size() == 1); CHECK(g.actors[0].embeddings[0][0] == 0.25f); } TEST_CASE("load_gallery reads the legacy jellyfin_person_id key", "[gallery]") { nlohmann::json j; j["actors"] = nlohmann::json::array(); nlohmann::json ja; ja["name"] = "Legacy"; ja["jellyfin_person_id"] = "old-guid"; // pre-rename key ja["embeddings"] = nlohmann::json::array(); ja["embeddings"].push_back(std::vector(512, 0.f)); j["actors"].push_back(ja); TempFile tf("gallery_legacy.json"); { std::ofstream out(tf.path); out << j.dump(); } ActorGallery g = load_gallery(tf.path); REQUIRE(g.actors.size() == 1); CHECK(g.actors[0].jellyfin_id == "old-guid"); } TEST_CASE("load_gallery throws on a missing file", "[gallery]") { CHECK_THROWS(load_gallery("/nonexistent/path/gallery.json")); }