From 6cbd3b9a7bb985c11f013e6c4d4ce238877d5a49 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Sun, 5 Apr 2026 21:34:38 +0530 Subject: [PATCH] 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. --- gitnexus/src/core/ingestion/pipeline.ts | 6 ++- gitnexus/test/unit/safe-push-all.test.ts | 48 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 gitnexus/test/unit/safe-push-all.test.ts diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index a51952b37..1e1193b68 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -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(target: T[], source: readonly T[]): void { +export function safePushAll(target: T[], source: readonly T[]): void { for (let i = 0; i < source.length; i++) { target.push(source[i]); } diff --git a/gitnexus/test/unit/safe-push-all.test.ts b/gitnexus/test/unit/safe-push-all.test.ts new file mode 100644 index 000000000..5e8ca698b --- /dev/null +++ b/gitnexus/test/unit/safe-push-all.test.ts @@ -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(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); + }); +});