From 3966e19a839dc763dc7de9a0d33be08e9352369b Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Mon, 31 Aug 2026 08:04:17 +0200 Subject: [PATCH] test: two tests still describe the registry as it was before 1477c53 Both failed the first time CI was able to run this suite, and neither is a new break. They assert semantics that two deliberate changes replaced, and nothing noticed because nothing had ever executed them. 1477c53 (2026-08-09) made a dormant track associable only if it had been identified: an unowned dormant track has no actor to re-attach to, so it only enlarges the matcher's comparison set and invites a new face landing on an anonymous stub. Its message records the trade -- "a small recall cost (~1-3pp on some films, e.g. Lord of War -2.5) for a cleaner, bounded pool. Kept deliberately." Both tests build tracks that are never identified, so since that commit they are dropped from the pool one line before the extinction horizon they were written to exercise. The tests date from 2026-07-31 and 2026-08-08 -- they are older than the gate. Fixed by owning the track while it is still on screen, which is what a real run does and what makes the horizon reachable at all. The second test needed more than that, and the rename says why. It was built on association and reaping answering on DIFFERENT clocks -- the tracker's for "may this associate?", the watermark for "is this finished?" -- which is exactly the arrangement track_registry.hpp:184-203 now rules out: one clock, one threshold, "nothing is offered past its reap horizon, nothing is reaped while still offerable". So there is no longer an interval in which a track is retired from association but still alive, and a test named for that interval cannot pass. Rewritten to assert what the unified clock actually promises: a tracker racing 50 s past the horizon retires nothing while the matcher lags, the votes in flight still land, and the watermark passing the horizon retires, reaps and emits in one step. Kept: the AR-008 tag, the late-vote check and dropped_votes() == 0, which were the point of the test and still hold. Full suite now 151/151 in sae-builder-cpu. TRACES: AR-008, AR-013 | SR-002 --- tests/test_face_tracker.cpp | 7 ++++++ tests/test_track_registry.cpp | 46 ++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/tests/test_face_tracker.cpp b/tests/test_face_tracker.cpp index 587a7b1..ca8cc32 100644 --- a/tests/test_face_tracker.cpp +++ b/tests/test_face_tracker.cpp @@ -192,6 +192,13 @@ TEST_CASE("a track within the extinction window is still a candidate", Embedding person = at_sim(0, 1, 0.99f); int id_pre = r.track_of(frame(0.0, 10, 10, person)); + + // Identify it while it is still on screen. Since 1477c53 a DORMANT track is + // only associable if it was owned -- an anonymous one has no actor to + // re-attach to, so it is dropped from the pool a line before the horizon is + // ever consulted. Without this the test cannot reach what it is about. + r.reg->observe(id_pre, /*actor=*/0, /*posterior=*/0.99f, person); + r.track_of(frame(1.0, 300, 300, axis(7), /*is_cut=*/true)); // Back inside the window: the same person continues the same track, so the diff --git a/tests/test_track_registry.cpp b/tests/test_track_registry.cpp index 4c99178..20a9deb 100644 --- a/tests/test_track_registry.cpp +++ b/tests/test_track_registry.cpp @@ -415,15 +415,21 @@ TEST_CASE("a track is not reaped until the evidence clock passes it", } /// TRACES: UT-001 | AR-008, AR-013 | SR-002 -TEST_CASE("a track retired from association is still open to evidence", +TEST_CASE("association and reaping retire a track on the same clock", "[registry][AR-008]") { - // The two clocks answer different questions and must not share an answer. - // Association asks "may this detection link to that track?" on the tracker's - // clock; reaping asks "is that track finished?" and cannot answer until the - // votes are in. Deferring both to the evidence clock was the second half of - // this bug: retired tracks lingered in the candidate pool for as long as the - // matcher lagged, so a new face re-associated onto a long-dead track and two - // people merged into one window. + // Renamed and rewritten for the unified clock. This test used to assert the + // opposite arrangement -- association answered on the tracker's clock while + // reaping answered on the evidence watermark -- and it was written when that + // was true. It is not true now: candidates() and reap_locked() take the SAME + // clock and the SAME threshold, so "retired from association" and "reaped" + // are one event, and the interval the old name described does not exist. + // + // Both halves of the AR-013 bug are visible here. Offering on a LOOSER + // horizon than the reap left retired tracks in the candidate pool for as + // long as the matcher lagged, so a new face re-associated onto a long-dead + // track and two people merged into one window. Reaping on a looser horizon + // than the offer killed tracks whose votes were still in flight. One clock + // makes both unrepresentable. TrackRegistry reg(cfg(/*extinction=*/5.0), disc()); Sink sink; sink.attach(reg); reg.expect_evidence(); @@ -432,21 +438,37 @@ TEST_CASE("a track retired from association is still open to evidence", { auto s = reg.begin_frame(0.0); id = s.create(0.0, axis(1)); + } + // Owned before it goes dormant, for the reason above: an unidentified + // dormant track is not a candidate at any horizon (1477c53). + reg.observe(id, 7, 0.99f, axis(1)); + { + auto s = reg.begin_frame(0.0); s.mark_lost(id, 0.0); } { - auto s = reg.begin_frame(3.0); // inside the window + auto s = reg.begin_frame(3.0); // watermark inside the window CHECK(s.candidates().size() == 1); // still associable } + // The tracker races far past the horizon while the matcher lags. This + // retires nothing, and that is the point of the watermark: the clock that + // decides has not moved, so the track is still offered. { - auto s = reg.begin_frame(50.0); // far outside it - CHECK(s.candidates().empty()); // retired from association... + auto s = reg.begin_frame(50.0); + CHECK(s.candidates().size() == 1); } - // ...but not gone, and still able to receive the votes in flight for it. + // Which is what keeps it able to receive the votes still in flight for it. reg.observe(id, 7, 0.99f, axis(1)); CHECK(reg.dropped_votes() == 0); + + // Only the watermark passing last_seen + extinction retires it -- and the + // same call reaps and emits it, so there is no interval in between. reg.advance_evidence(50.0); + { + auto s = reg.begin_frame(50.0); + CHECK(s.candidates().empty()); + } REQUIRE(sink.claims.size() == 1); CHECK(sink.claims[0].actor_idx == 7); }