Files
gstack/browse/test/cdp-inspector-history-cap.test.ts
T
Garry Tan 4f42c7bf08 cap inspector modificationHistory at 200 entries
Pre-cap, modificationHistory was an unbounded module-scoped array that
grew for every CSS edit through $B css across the entire session.
Small per-entry footprint but no upper bound, the kind of slow leak
that compounds over multi-day inspector use.

Cap is 200, oldest evicted on push past the cap. modHistoryTotalPushed
stays monotonic across the session so undoModification can tell the
user when their target index has been evicted, instead of just the
opaque pre-cap "No modification at index 500" with no context.

__testInternals export lets the cap + eviction error be unit-tested
without spinning up a CDP-driven Page. Production code must continue
to go through modifyStyle / undoModification / resetModifications.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 07:26:21 -07:00

96 lines
3.6 KiB
TypeScript

import { describe, test, expect, beforeEach } from 'bun:test';
import type { Page } from 'playwright';
import {
__testInternals,
undoModification,
} from '../src/cdp-inspector';
// Regression tests for the modificationHistory cap (D6 / smoking gun #2).
// Pre-cap, the module-scoped array grew unbounded across the session. Cap is
// 200 entries, oldest evicted on push past the cap. undoModification reports
// "evicted at the cap" in the error message so a user who asks for a
// no-longer-available index understands what happened (instead of seeing the
// pre-cap "No modification at index 500" with no context).
const { pushModification, MOD_HISTORY_CAP, getRawHistory, getTotalPushed, resetForTest } = __testInternals;
function fakeMod(id: number) {
return {
selector: `#node-${id}`,
property: 'color',
oldValue: 'red',
newValue: 'blue',
source: 'inline' as const,
timestamp: id,
method: 'setProperty' as 'setProperty',
};
}
beforeEach(() => {
resetForTest();
});
describe('modificationHistory cap', () => {
test('1. push under cap keeps every entry', () => {
for (let i = 0; i < 50; i++) pushModification(fakeMod(i));
expect(getRawHistory().length).toBe(50);
expect(getTotalPushed()).toBe(50);
expect(getRawHistory()[0].timestamp).toBe(0);
expect(getRawHistory()[49].timestamp).toBe(49);
});
test('2. push exactly cap keeps every entry', () => {
for (let i = 0; i < MOD_HISTORY_CAP; i++) pushModification(fakeMod(i));
expect(getRawHistory().length).toBe(MOD_HISTORY_CAP);
expect(getTotalPushed()).toBe(MOD_HISTORY_CAP);
expect(getRawHistory()[0].timestamp).toBe(0);
});
test('3. push past cap evicts oldest, keeps length at cap', () => {
const total = MOD_HISTORY_CAP + 50;
for (let i = 0; i < total; i++) pushModification(fakeMod(i));
expect(getRawHistory().length).toBe(MOD_HISTORY_CAP);
expect(getTotalPushed()).toBe(total);
// Oldest 50 dropped — entry that was #0 is gone; new oldest is #50.
expect(getRawHistory()[0].timestamp).toBe(50);
expect(getRawHistory()[MOD_HISTORY_CAP - 1].timestamp).toBe(total - 1);
});
test('4. resetForTest clears both buffer and totalPushed', () => {
for (let i = 0; i < 10; i++) pushModification(fakeMod(i));
resetForTest();
expect(getRawHistory().length).toBe(0);
expect(getTotalPushed()).toBe(0);
});
});
describe('undoModification eviction-aware error', () => {
// Stub Page: undoModification throws before any await when idx is out of
// range, so the stub never actually gets called.
const stubPage = {} as unknown as Page;
test('5. out-of-range BEFORE any eviction → no evicted note', async () => {
for (let i = 0; i < 5; i++) pushModification(fakeMod(i));
await expect(undoModification(stubPage, 99)).rejects.toThrow(
'No modification at index 99. History has 5 entries.',
);
});
test('6. out-of-range AFTER eviction → message names the evicted count', async () => {
const total = MOD_HISTORY_CAP + 73;
for (let i = 0; i < total; i++) pushModification(fakeMod(i));
// 273 pushed, 200 in buffer, 73 evicted. Ask for idx=400 (above buffer).
await expect(undoModification(stubPage, 400)).rejects.toThrow(
`No modification at index 400. History has ${MOD_HISTORY_CAP} entries ` +
`(most recent ${MOD_HISTORY_CAP} only — 73 earlier entries evicted at the cap).`,
);
});
test('7. negative explicit index throws cleanly (no NaN propagation)', async () => {
for (let i = 0; i < 10; i++) pushModification(fakeMod(i));
await expect(undoModification(stubPage, -1)).rejects.toThrow(
'No modification at index -1.',
);
});
});