Skip to content

Commit 3407188

Browse files
committed
test(core): make wait-queue's re-arm test prove the mechanism, not just the timing (H5)
The test titled "re-arms only the time actually remaining on a re-enqueue before the deadline" asserted only on waiter.state at various clock ticks and when its promise finally rejected -- every one of those observations comes out identical whether #armTimeout actually cancels the original timer and starts a fresh one for the recomputed remainder, or simply leaves the still-live original timer alone. Both hit the same fixed deadlineAt. Retitled it and added clock.setTimer/cancel spies asserting on the mechanism directly: exactly one timer is armed across the whole queued -> processing -> queued cycle, never two. Documented in #armTimeout's own comment why that is the only thing that can happen: its `waiter.timer !== undefined` guard means a genuine partial-value recompute-and-re-arm is unreachable through this class's public API -- every later call either finds the original timer still live (early return, left untouched) or already past its deadline (enqueue's own upfront check rejects before #armTimeout runs again). Verified by mutating #armTimeout to unconditionally cancel and re-arm on every call: the retitled test then fails with a named assertion (setTimer called 2 times, expected 1), proving it can actually tell the two mechanisms apart. Re-ran the worker's own lease-acquisition suite, the gateway boundary/fleet-coordinator suites, and the full unit suite (1775 passed) since this is shared core on the worker's lease path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MA98m7ua7qvDFZjxFaww6Z
1 parent 78ebae1 commit 3407188

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

src/core/wait-queue.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,39 @@ describe("WaitQueue", () => {
151151
expect(timedOut).toHaveBeenCalledWith(waiter);
152152
});
153153

154-
it("re-arms only the time actually remaining on a re-enqueue before the deadline, so the total wait never exceeds the original timeoutMs", async () => {
154+
// H5 (round 3 review): this test used to claim it proved a re-enqueue "re-arms only the time
155+
// actually remaining" -- but every assertion in it (the waiter's `state` at various clock
156+
// ticks, and when its promise finally rejects) would come out identical whether `#armTimeout`
157+
// actually cancelled the original timer and started a fresh one for the recomputed remainder,
158+
// or simply left that still-live timer alone. Both mechanisms hit the same fixed `deadlineAt`.
159+
// The `setTimer` spy below is what tells them apart: it asserts directly on the *mechanism* --
160+
// exactly one timer is ever armed across the whole `queued -> processing -> queued` cycle, not
161+
// two -- which is what `#armTimeout`'s own doc comment now documents as the only thing that
162+
// can happen (its `waiter.timer !== undefined` guard makes a genuine recompute-and-re-arm
163+
// unreachable through this class's public API). The externally observed timing this test also
164+
// pins is real and worth keeping; it just is not, on its own, evidence of which mechanism
165+
// produced it.
166+
it("leaves a still-live timer alone across a queued -> processing -> queued cycle, rather than cancelling and re-arming it, so the total wait never exceeds the original timeoutMs (H5, round 3 review)", async () => {
155167
const { clock, queue } = createQueue();
156168
const waiter = createWaiter(queue, "agent", { timeoutMs: 100 });
169+
const setTimerSpy = vi.spyOn(clock, "setTimer");
170+
const cancelSpy = vi.spyOn(clock, "cancel");
157171

158172
queue.enqueue(waiter);
173+
expect(setTimerSpy).toHaveBeenCalledTimes(1); // the first (and, this test proves, only) arm
159174
clock.advance(30);
160175
queue.markProcessing(waiter);
161176
clock.advance(20); // t=50: still well before the t=100 deadline
162177
expect(queue.enqueue(waiter)).toBe(true); // re-queued, e.g. a stale-view NO_CAPACITY
163178
expect(waiter.state).toBe("queued");
164179

180+
// The proof this test exists for: re-enqueuing well before the deadline armed nothing new
181+
// and cancelled nothing -- `#armTimeout`'s guard found the original timer still live and
182+
// returned immediately. A recompute-and-re-arm implementation would have cancelled that
183+
// timer and called `setTimer` a second time here, for the ~50ms actually remaining.
184+
expect(setTimerSpy).toHaveBeenCalledTimes(1);
185+
expect(cancelSpy).not.toHaveBeenCalled();
186+
165187
// If this cycle had re-armed a fresh 100ms window, the waiter would still be pending here
166188
// (t=50 + 49ms = 99ms of its own window, or 149ms of total elapsed time either way). It
167189
// does not: the original deadline was t=100, and only 50ms of elapsed time remain from it.
@@ -170,6 +192,9 @@ describe("WaitQueue", () => {
170192
clock.advance(1); // t=100: the original deadline, reached exactly once, not once per cycle
171193
await expect(waiter.promise).rejects.toEqual(expect.any(QueueTimeoutError));
172194
expect(queue.depth).toBe(0);
195+
// Still just the one timer, start to finish -- the fixed deadline was honoured by leaving it
196+
// running, not by any second arm.
197+
expect(setTimerSpy).toHaveBeenCalledTimes(1);
173198
});
174199

175200
it("never delivers a progress push to a waiter that already settled, even the very same tick (P3, round 2 review)", async () => {

src/core/wait-queue.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,25 @@ export class WaitQueue {
318318
* the request is actively being handled, and this class does not know whether that attempt is
319319
* about to succeed. `enqueue`'s own deadline check is what catches this case at the next
320320
* re-queue, since `waiter.timer` is already cleared by the time this branch returns.
321+
*
322+
* H5 (round 3 review): the `waiter.timer !== undefined` guard above means `remainingMs` is
323+
* only ever actually computed with a genuinely partial value in theory, never in practice --
324+
* every real call into this method lands in one of exactly two cases. A fresh arm (`waiter.
325+
* timer` was never set) always computes the *full* `timeoutMs`, since `deadlineAt` is being
326+
* fixed in the same line. Every later call finds one of two things: the original timer is
327+
* still counting down (`waiter.timer !== undefined`), so the guard above returns immediately
328+
* and the still-live timer -- already targeting the correct fixed `deadlineAt` -- is left
329+
* untouched, no second `setTimer` call, no recompute, no re-arm; or that timer already fired
330+
* exactly at `deadlineAt` (clearing itself, above), in which case `enqueue`'s own upfront
331+
* `clock.now() >= deadlineAt` check rejects the waiter before this method is even called
332+
* again. Nothing in this class's public API cancels an armed timer without settling its
333+
* waiter, so there is no third case where `waiter.timer === undefined`, a `deadlineAt` is
334+
* already set, and `clock.now()` is still short of it -- the one shape that would make this
335+
* arithmetic produce something other than the full budget or an already-expired one. The
336+
* "remaining budget" behaviour this method's own name promises is real (a re-enqueue can never
337+
* push the total wait past the original `timeoutMs`), it is just delivered by leaving the
338+
* first timer alone rather than by this line ever recomputing a partial value -- see this
339+
* method's own test for what that means for what a test here can and cannot prove.
321340
*/
322341
#armTimeout(waiter: MutableWaiter): void {
323342
if (waiter.timer !== undefined || waiter.options.timeoutMs === undefined) return;

0 commit comments

Comments
 (0)