Reconcile eviction comment wording; add handleSourceEnded cascade test (Phase 21.1)
The inclusive <= bound is correct; comments now say 'at or behind'. New test drives eviction through the real onended trigger with a live mid-array source pinning the frontier.
This commit is contained in:
@@ -253,6 +253,78 @@ test('eviction does not drop buffers under live sources or past the schedule cur
|
||||
assertEqual(dropped, 0, 'no eviction while front sources are live');
|
||||
});
|
||||
|
||||
// handleSourceEnded cascade: eviction fires from the real production trigger (onended), not
|
||||
// via a direct evictPlayedBuffers() call. Confirms the anchor/index invariants hold end-to-end
|
||||
// through the scheduler's own event handling while playback is still active with a live source.
|
||||
//
|
||||
// Setup: 0.3s buffers so the 500ms lookahead window fits exactly two sources after
|
||||
// playFromPosition(0). Buffer 0 ends at ~0.31s, buffer 1 ends at ~0.61s — both are scheduled.
|
||||
// Clock is then advanced to t=0.6 so buffer 0's end (0.31) < evictBefore (0.6) while the live
|
||||
// source on buffer 1 pins firstLiveIndex=1, blocking further eviction. This is the mid-array
|
||||
// pinning scenario that later waves (21.2/21.3) build on.
|
||||
test('eviction via handleSourceEnded: position exact, live bufferIndex decremented, frontier respected', () => {
|
||||
const cm = new FakeContextManager();
|
||||
const s = makeScheduler(cm);
|
||||
// Retain nothing behind the playhead — evict aggressively so the cascade fires.
|
||||
s.setBackRetainSeconds(0);
|
||||
|
||||
// Eight 0.3s buffers. scheduleBuffersFrom with lookaheadTarget=0.5s at t=0:
|
||||
// after buf 0: nextScheduleTime≈0.31, lookahead=0.31 < 0.5 → continues
|
||||
// after buf 1: nextScheduleTime≈0.61, lookahead=0.61 > 0.5 → breaks
|
||||
// → exactly two sources are live after playFromPosition.
|
||||
for (let i = 0; i < 8; i++) s.addBuffer(buf(0.3));
|
||||
|
||||
cm.now = 0;
|
||||
s.playFromPosition(0);
|
||||
|
||||
// Reach inside to see which sources were scheduled and what bufferIndex they hold.
|
||||
const priv = s as unknown as {
|
||||
scheduledSources: Array<{ source: FakeSource; bufferIndex: number; startTime: number; endTime: number }>;
|
||||
nextBufferIndex: number;
|
||||
};
|
||||
|
||||
// Confirm two sources are live — the setup guarantee.
|
||||
if (priv.scheduledSources.length < 2) {
|
||||
throw new Error(`Expected ≥2 scheduled sources after playFromPosition, got ${priv.scheduledSources.length}`);
|
||||
}
|
||||
|
||||
// Identify the first and second scheduled sources by bufferIndex order.
|
||||
const sorted = [...priv.scheduledSources].sort((a, b) => a.bufferIndex - b.bufferIndex);
|
||||
const firstScheduled = sorted[0]; // bufferIndex 0
|
||||
const secondScheduled = sorted[1]; // bufferIndex 1
|
||||
const secondBufferIndexBefore = secondScheduled.bufferIndex; // must be 1
|
||||
|
||||
// Record the second FakeSource so we can assert it was not stopped by eviction.
|
||||
const secondFakeSource = secondScheduled.source as unknown as FakeSource;
|
||||
|
||||
// Advance clock to 0.6s. Buffer 0 ends at ~0.31s → evictBefore=0.6, end=0.31 ≤ 0.6 →
|
||||
// droppable. Buffer 1 ends at ~0.61s → its live source pins firstLiveIndex=1 → NOT dropped.
|
||||
cm.now = 0.6;
|
||||
|
||||
// Confirm playback is still active before firing the cascade.
|
||||
assertEqual(s.isActive(), true, 'isActive must be true before cascade');
|
||||
|
||||
// Fire the cascade via the production trigger: stop the first source, which calls onended,
|
||||
// which calls handleSourceEnded, which calls evictPlayedBuffers internally.
|
||||
(firstScheduled.source as unknown as FakeSource).stop();
|
||||
|
||||
// (a) Absolute position must remain exactly 0.6.
|
||||
assertClose(s.getCurrentPosition(), 0.6, 'position after handleSourceEnded cascade');
|
||||
|
||||
// (b) The second live source's bufferIndex must have been decremented by 1 (the one evicted
|
||||
// front buffer), shifting it from absolute index 1 to absolute index 0.
|
||||
const expectedSecondIndex = secondBufferIndexBefore - 1;
|
||||
assertEqual(secondScheduled.bufferIndex, expectedSecondIndex, 'live source bufferIndex decremented');
|
||||
|
||||
// (c) Eviction stopped at firstLiveIndex=1, not nextBufferIndex — the second buffer was
|
||||
// NOT dropped. Verify the second source was not stopped (it remained live throughout).
|
||||
assertEqual(secondFakeSource.stopped, false, 'live second source not stopped by eviction');
|
||||
// And the scheduler still has buffers (the array was not wiped past the frontier).
|
||||
if (s.getBufferCount() === 0) {
|
||||
throw new Error('eviction wiped all buffers — should have stopped at firstLiveIndex');
|
||||
}
|
||||
});
|
||||
|
||||
// --- run -------------------------------------------------------------------------------------
|
||||
if (failures.length > 0) {
|
||||
console.error(failures.join('\n'));
|
||||
|
||||
Reference in New Issue
Block a user