forgetest: the update drill reads the daemon's reply, and a queue start waits for the fixture probe

update.slots-and-signature's apply section required 200 from
POST /update/apply, where the daemon answers 202 with started, like
every job endpoint, so its first bench run on image 20260902144848
ended before the job did; the cleanup then deleted the staged archive
under the running job. The drill requires 202 and started, and looks
for the daemon's refusal ("archive is not signed with the ForgeFIRM
release key"). Bench: the apply started, the job ended with that
refusal, PASS.

A queue started 4 s after a forgetest restart ran 7 tests instead of
10. The bench page's /state poll had a fixture probe in flight (an mDNS
answer), probe_fixture stamped its time at its start, and the queue
start read the stale fixture, none, so the three operator tests the
fixture runs in the unattended queue were routed to nobody. The probe
now runs under a lock and is stamped when it completes: a caller that
arrives during a probe waits for its answer. tests/test_fixture.py
holds the race with a slow scripted probe; it fails on the old code.

Catalog consequence: the update implementation hash moves; the runner
change is dev-only.
This commit is contained in:
ScottW514
2026-09-02 17:02:33 -04:00
parent 1fef9c6f51
commit 7605a90946
3 changed files with 46 additions and 20 deletions
+20
View File
@@ -416,6 +416,26 @@ class RoutingTests(unittest.TestCase):
self.assertEqual(av["unattended"], ["r.auto"])
self.assertIsNone(summary)
def test_a_queue_started_during_a_probe_waits_for_the_fixture(self):
# The daemon just came up: nothing probed yet, a page poll starts
# the first probe (slow: an mDNS answer), and the queue start
# lands while it is in flight. The start must see the fixture.
stub = self.stub
def slow_probe(log, path=None, resolver=None):
time.sleep(0.5)
return stub
fx.probe = slow_probe
self.runner._fixture_probed = 0.0
self.runner.fixture = None
poll = threading.Thread(target=self.runner.probe_fixture)
poll.start()
time.sleep(0.1)
order = self.runner.batch_selection("unattended")
poll.join()
self.assertIn("r.lid", order)
self.assertIn("r.btn", order)
def run_queue(self, group, ack_live=False):
ok, msg, order = self.runner.start_batch(group, ack_live=ack_live)
self.assertTrue(ok, msg)