// TRACES: AR-018, AR-019, AR-024 | SR-005 // // Unit tests for TrackGallery (gallery/track_gallery.hpp): per-film gallery // expansion driven by track continuity. Pure, GPU-free, model-free — exercises // the AR-018 banded admission at both bounds, the promotion-time coherence // gate, the diversity-buffer eviction policy, plurality ownership, and // idempotent promotion, all through the public interface. // // The band is defined in PROBABILITY space (AR-024), so every case below states // its own cosine → probability map instead of inheriting the header's fallback. // A test that never names the mapping is not testing the band, it is testing a // coincidence: with the fallback the two spaces happen to coincide, and a gate // that silently reverted to raw cosine would still pass. #include #include #include "config.hpp" #include "gallery/track_gallery.hpp" #include "types.hpp" #include #include namespace { constexpr float kPi = 3.14159265358979323846f; // The band as the tests drive it. Kept in one place so a change to the shipped // defaults does not silently invalidate the arithmetic in each case. constexpr float kBandLo = 0.90f; constexpr float kBandHi = 0.95f; // Identity map: probability == cosine, so a case can place an embedding at an // exact probability. cosine_similarity is a bare dot product over unit vectors // (types.hpp), so the placements below are bit-exact, not approximate. float identity_cal(float c) { return c; } // Unit-norm embedding pointing along one axis. Cosine sim to another one-hot is // 0, to itself 1. Embedding one_hot(int slot) { Embedding e{}; e[slot] = 1.0f; return e; } // Unit-norm embedding in the plane of axes i,j at cosine `cos_t` from axis i. // Cosine sim to one_hot(i) is exactly cos_t. Embedding at_sim(int i, int j, float cos_t) { Embedding e{}; e[i] = cos_t; e[j] = std::sqrt(std::max(0.f, 1.f - cos_t * cos_t)); return e; } // A spoke: shares axis 0 with every other spoke, and is otherwise unique. Any // two DISTINCT spokes have cosine similarity exactly cos_t², so one constant // places a whole mutually-in-band store. A spoke against itself is 1.0 — above // the band's ceiling, i.e. redundant, which is the intended reading. Embedding spoke(int k, float cos_t) { return at_sim(0, k, cos_t); } // cos_t chosen so pairwise similarity between distinct spokes is 0.9197 — // comfortably inside [0.90, 0.95], clear of both bounds. constexpr float kSpokeCos = 0.959f; // Two embeddings `deg` apart in the plane of axes 0,1. Cosine is cos(deg), so a // chain of these can step through the band while its endpoints fall outside it. Embedding on_circle(float deg) { Embedding e{}; e[0] = std::cos(deg * kPi / 180.f); e[1] = std::sin(deg * kPi / 180.f); return e; } Config expand_cfg() { Config cfg; cfg.expand_gallery = true; cfg.expand_buffer_size = 3; cfg.expand_band_lo = kBandLo; cfg.expand_band_hi = kBandHi; cfg.expand_min_anchor_frames = 3; return cfg; } const cv::Mat kNoCrop; // debug dumping off → crop unused } // namespace TEST_CASE("disabled: no annex growth when expand_gallery is off", "[track_gallery]") { // expand_gallery defaults to true (see config.hpp) as of the rep4 bake-off — // set it explicitly false here since this test exercises the disabled path, // not whatever the struct's current default happens to be. Config cfg; cfg.expand_gallery = false; TrackGallery tg(cfg); REQUIRE_FALSE(tg.enabled()); for (int f = 0; f < 10; ++f) tg.observe(1, one_hot(1), /*actor*/ 0, /*sim*/ 0.2f, /*accept*/ true, kNoCrop); CHECK(tg.annex().empty()); } // ── AR-018: the band ───────────────────────────────────────────────────────── TEST_CASE("band bounds come from config, not a hardcoded default", "[track_gallery][AR-018]") { // The bounds were declared in Config and read nowhere, so the gate ran at // whatever the header happened to initialise. Drive them somewhere the // defaults are not and require the gate to follow. Config cfg = expand_cfg(); cfg.expand_band_lo = 0.40f; cfg.expand_band_hi = 0.60f; TrackGallery tg(cfg); tg.set_calibration(identity_cal); tg.observe(1, one_hot(0), 0, 0.30f, true, kNoCrop); // P = 0.50: inside the configured band, far below the shipped default lo. tg.observe(1, at_sim(0, 1, 0.50f), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 0); // P = 0.92: inside the shipped default band, above the configured ceiling. tg.observe(1, at_sim(0, 2, 0.92f), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 1); } TEST_CASE("band admits at each bound exactly", "[track_gallery][AR-018]") { // The verification plan asks for the bounds themselves, not a point safely // inside them: an off-by-one in the comparison is invisible anywhere else. // Both bounds are inclusive. SECTION("lower bound exactly") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); tg.observe(1, one_hot(0), 0, 0.30f, true, kNoCrop); tg.observe(1, at_sim(0, 1, kBandLo), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 0); } SECTION("upper bound exactly") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); tg.observe(1, one_hot(0), 0, 0.30f, true, kNoCrop); tg.observe(1, at_sim(0, 1, kBandHi), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 0); } } TEST_CASE("store never admits below the lower bound", "[track_gallery][AR-018]") { // The lower bound is the poisoning guard: an embedding unlike everything // already on the track is evidence the track is not one person. TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); tg.observe(1, one_hot(0), 0, 0.30f, true, kNoCrop); tg.observe(1, at_sim(0, 1, kBandLo - 0.01f), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 1); tg.observe(1, one_hot(400), 0, 0.30f, true, kNoCrop); // orthogonal: P = 0 CHECK(tg.band_rejected() == 2); } TEST_CASE("store never admits above the upper bound", "[track_gallery][AR-018]") { // The upper bound is the redundancy guard: another look at a pose the store // already covers teaches the annex nothing and costs a slot. TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); tg.observe(1, one_hot(0), 0, 0.30f, true, kNoCrop); tg.observe(1, at_sim(0, 1, kBandHi + 0.01f), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 1); tg.observe(1, one_hot(0), 0, 0.30f, true, kNoCrop); // identical: P = 1 CHECK(tg.band_rejected() == 2); } TEST_CASE("band thresholds probability, not cosine", "[track_gallery][AR-018][AR-024]") { // The invariant's actual claim, and the one a raw-cosine gate passes by // accident under an identity calibration. With a calibration that shifts by // +0.10, two embeddings get the OPPOSITE verdict from the one their bare // cosines would earn — so admission here can only come from the calibrated // value having been used. TrackGallery tg(expand_cfg()); tg.set_calibration([](float c) { return c + 0.10f; }); tg.observe(1, one_hot(0), 0, 0.30f, true, kNoCrop); // cosine 0.84 (below lo, would be refused raw) → P = 0.94, inside the band. tg.observe(1, at_sim(0, 1, 0.84f), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 0); // cosine 0.92 (inside the band, would be admitted raw) → P = 1.02, above it. tg.observe(1, at_sim(0, 2, 0.92f), 0, 0.30f, true, kNoCrop); CHECK(tg.band_rejected() == 1); } TEST_CASE("a two-person track never poisons the annex", "[track_gallery][AR-018]") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); // Two orthogonal identities under one track ID — a track-ID collision. // The band refuses the outsider at the door, so the store never becomes // two-person in the first place. tg.observe(3, spoke(1, kSpokeCos), 0, 0.30f, true, kNoCrop); tg.observe(3, spoke(2, kSpokeCos), 0, 0.30f, true, kNoCrop); tg.observe(3, one_hot(400), 0, 0.30f, true, kNoCrop); // orthogonal outlier CHECK(tg.band_rejected() == 1); // refused at the door REQUIRE_FALSE(tg.annex().empty()); // the legitimate views still promote for (const auto& e : tg.annex()) CHECK(cosine_similarity(e.emb, one_hot(400)) < 0.5f); } TEST_CASE("a track that drifts through the band is refused at promotion", "[track_gallery][AR-018]") { // `admit` compares a newcomer against its CLOSEST existing member, so a // gradual drift chains past it: each step is in-band while the endpoints are // strangers. This is the shape a collision takes over a slow pan, and the // reason the lower bound is re-asked across every pair before promotion. TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); tg.observe(5, on_circle(0.f), 0, 0.30f, true, kNoCrop); tg.observe(5, on_circle(25.f), 0, 0.30f, true, kNoCrop); // P=0.906 vs 0° → in band tg.observe(5, on_circle(50.f), 0, 0.30f, true, kNoCrop); // P=0.906 vs 25° → in band CHECK(tg.band_rejected() == 0); // every step passed the door... // ...but 0° and 50° are P=0.643 apart, below the floor: the whole track goes. CHECK(tg.annex().empty()); } // ── AR-019: ownership and promotion ────────────────────────────────────────── TEST_CASE("confirmed track promotes its store", "[track_gallery][AR-019]") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); REQUIRE(tg.enabled()); // A track owned by actor 0: every frame accepted, every view mutually // in-band (P = 0.9197 between distinct spokes) and gallery-far (0.30). for (int k = 1; k <= 3; ++k) tg.observe(7, spoke(k, kSpokeCos), 0, 0.30f, true, kNoCrop); // 3 accepted frames == min_anchor_frames → confirmed and promoted. CHECK(tg.annex().size() == 3); for (const auto& ae : tg.annex()) CHECK(ae.actor_idx == 0); } TEST_CASE("registry ownership overrides the local tally", "[track_gallery][AR-019]") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); // Local accepted-frame plurality says actor 5; the registry's accumulated // posterior says actor 9. The registry is authoritative. tg.set_owner(11, 9); for (int k = 1; k <= 3; ++k) tg.observe(11, spoke(k, kSpokeCos), 5, 0.30f, true, kNoCrop); REQUIRE_FALSE(tg.annex().empty()); for (const auto& ae : tg.annex()) CHECK(ae.actor_idx == 9); } TEST_CASE("unconfirmed track (too few accepts) does not promote", "[track_gallery][AR-019]") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); // Only 2 accepted frames < min_anchor_frames 3; the third fills the buffer // but doesn't count toward ownership. tg.observe(4, spoke(1, kSpokeCos), 0, 0.30f, true, kNoCrop); tg.observe(4, spoke(2, kSpokeCos), 0, 0.30f, true, kNoCrop); tg.observe(4, spoke(3, kSpokeCos), 0, 0.30f, false, kNoCrop); CHECK(tg.annex().empty()); } TEST_CASE("plurality actor wins a mixed-vote track", "[track_gallery][AR-019]") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); // No registry attached (unit-test path): actor 5 accepted twice, actor 6 // once → plurality is 5. tg.observe(8, spoke(1, kSpokeCos), 5, 0.30f, true, kNoCrop); tg.observe(8, spoke(2, kSpokeCos), 5, 0.30f, true, kNoCrop); tg.observe(8, spoke(3, kSpokeCos), 6, 0.30f, true, kNoCrop); REQUIRE_FALSE(tg.annex().empty()); for (const auto& ae : tg.annex()) CHECK(ae.actor_idx == 5); } TEST_CASE("promotion is idempotent across a long track", "[track_gallery][AR-019]") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); for (int k = 1; k <= 3; ++k) tg.observe(9, spoke(k, kSpokeCos), 0, 0.30f, true, kNoCrop); size_t after_confirm = tg.annex().size(); REQUIRE(after_confirm > 0); // Keep feeding the confirmed track: annex must not grow again. for (int f = 0; f < 10; ++f) tg.observe(9, spoke(4 + f, kSpokeCos), 0, 0.30f, true, kNoCrop); CHECK(tg.annex().size() == after_confirm); } TEST_CASE("clear_tracks drops buffers before confirmation", "[track_gallery][AR-019]") { TrackGallery tg(expand_cfg()); tg.set_calibration(identity_cal); // Two accepts, then a cut clears buffers; the third accept starts fresh and // can't reach the anchor threshold on its own. tg.observe(1, spoke(1, kSpokeCos), 0, 0.30f, true, kNoCrop); tg.observe(1, spoke(2, kSpokeCos), 0, 0.30f, true, kNoCrop); tg.clear_tracks(); tg.observe(1, spoke(3, kSpokeCos), 0, 0.30f, true, kNoCrop); CHECK(tg.annex().empty()); } TEST_CASE("eviction keeps the gallery-far views", "[track_gallery][AR-018]") { // Novelty is no longer a threshold — it is this ordering. With the buffer // full, a more gallery-far newcomer must displace the best-recognised // member, and a less novel one must be dropped rather than displace a // better sample. Config cfg = expand_cfg(); cfg.expand_buffer_size = 2; cfg.expand_min_anchor_frames = 4; TrackGallery tg(cfg); tg.set_calibration(identity_cal); tg.observe(6, spoke(1, kSpokeCos), 0, 0.80f, true, kNoCrop); // well recognised tg.observe(6, spoke(2, kSpokeCos), 0, 0.40f, true, kNoCrop); tg.observe(6, spoke(3, kSpokeCos), 0, 0.20f, true, kNoCrop); // novel: evicts the 0.80 tg.observe(6, spoke(4, kSpokeCos), 0, 0.90f, true, kNoCrop); // least novel: dropped REQUIRE(tg.annex().size() == 2); // Survivors are the two most gallery-far views: spokes 2 and 3. for (const auto& ae : tg.annex()) { const bool is_2 = cosine_similarity(ae.emb, spoke(2, kSpokeCos)) > 0.99f; const bool is_3 = cosine_similarity(ae.emb, spoke(3, kSpokeCos)) > 0.99f; CHECK((is_2 || is_3)); } }