test: add regression test for safePushAll stack-overflow boundary

Export safePushAll so it can be directly tested, and add a dedicated
test file that:

- Verifies element order and edge cases (empty source/target)
- Drives a 200k-element accumulation to prove it doesn't overflow

This ensures CI will catch any future reintroduction of the spread
pattern in the chunk-accumulation path.
This commit is contained in:
Hardik Jain 2026-04-05 21:34:38 +05:30
parent 58885c1187
commit 6cbd3b9a7b
2 changed files with 52 additions and 2 deletions

View file

@ -76,9 +76,11 @@ const isDev = process.env.NODE_ENV === 'development';
* Append all elements from `source` into `target` without using the spread
* operator. `target.push(...source)` converts every element into a function
* argument which exceeds V8's call-stack limit when `source` has more than
* ~65 000 entries (common in large Ruby/Rails monoliths).
* ~65 000 entries (common in large monoliths with many symbols).
*
* Exported for regression testing — see safe-push-all.test.ts.
*/
function safePushAll<T>(target: T[], source: readonly T[]): void {
export function safePushAll<T>(target: T[], source: readonly T[]): void {
for (let i = 0; i < source.length; i++) {
target.push(source[i]);
}

View file

@ -0,0 +1,48 @@
/**
* Regression test for safePushAll — the helper introduced to replace
* `array.push(...largeArray)` in the pipeline chunk-accumulation loop.
*
* The spread operator converts every element into a V8 call-stack argument.
* At ~65k+ elements a single `push(...arr)` call overflows the stack.
* This test ensures safePushAll handles that size without crashing and
* will catch any future reintroduction of the spread pattern.
*/
import { describe, it, expect } from 'vitest';
import { safePushAll } from '../../src/core/ingestion/pipeline.js';
/** Size that reliably triggers the V8 stack overflow with push(...arr). */
const OVERFLOW_SIZE = 200_000;
describe('safePushAll', () => {
it('appends elements in order for a small array', () => {
const target = [1, 2, 3];
safePushAll(target, [4, 5, 6]);
expect(target).toEqual([1, 2, 3, 4, 5, 6]);
});
it('handles an empty source array', () => {
const target = [1];
safePushAll(target, []);
expect(target).toEqual([1]);
});
it('handles an empty target array', () => {
const target: number[] = [];
safePushAll(target, [1, 2]);
expect(target).toEqual([1, 2]);
});
it(`handles ${OVERFLOW_SIZE.toLocaleString()} elements without stack overflow`, () => {
const source = new Array<number>(OVERFLOW_SIZE);
for (let i = 0; i < OVERFLOW_SIZE; i++) source[i] = i;
const target: number[] = [];
// This must not throw — push(...source) would RangeError here.
safePushAll(target, source);
expect(target.length).toBe(OVERFLOW_SIZE);
// Spot-check order is preserved at boundaries
expect(target[0]).toBe(0);
expect(target[OVERFLOW_SIZE - 1]).toBe(OVERFLOW_SIZE - 1);
});
});