feat(cli): add /bug-memory command and auto-capture heap snapshot in /bug (#25639)

This commit is contained in:
Anjaligarhwal
2026-05-05 02:47:36 +05:30
committed by GitHub
parent f87072f4e3
commit 5dfbb739e5
8 changed files with 503 additions and 1 deletions
+124 -1
View File
@@ -12,10 +12,33 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
import { getVersion, type Config } from '@google/gemini-cli-core';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatBytes } from '../utils/formatters.js';
import { MessageType } from '../types.js';
import { captureHeapSnapshot } from '../utils/memorySnapshot.js';
const { memoryUsageMock } = vi.hoisted(() => ({
memoryUsageMock: vi.fn(() => ({
rss: 0,
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0,
})),
}));
// Mock dependencies
vi.mock('open');
vi.mock('../utils/formatters.js');
vi.mock('../utils/memorySnapshot.js', () => ({
captureHeapSnapshot: vi.fn(),
MEMORY_SNAPSHOT_AUTO_THRESHOLD_BYTES: 2 * 1024 * 1024 * 1024,
}));
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>();
return {
...actual,
stat: vi.fn().mockResolvedValue({ size: 4096 }),
};
});
vi.mock('../utils/historyExportUtils.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../utils/historyExportUtils.js')>();
@@ -53,7 +76,7 @@ vi.mock('node:process', () => ({
version: 'v20.0.0',
// Keep other necessary process properties if needed by other parts of the code
env: process.env,
memoryUsage: () => ({ rss: 0 }),
memoryUsage: memoryUsageMock,
},
}));
@@ -69,6 +92,13 @@ describe('bugCommand', () => {
beforeEach(() => {
vi.mocked(getVersion).mockResolvedValue('0.1.0');
vi.mocked(formatBytes).mockReturnValue('100 MB');
memoryUsageMock.mockReturnValue({
rss: 0,
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0,
});
vi.stubEnv('SANDBOX', 'gemini-test');
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
@@ -218,4 +248,97 @@ describe('bugCommand', () => {
expect(open).toHaveBeenCalledWith(expectedUrl);
});
const buildHighMemoryContext = (tempDir: string | undefined) =>
createMockCommandContext({
services: {
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => false,
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
storage: tempDir ? { getProjectTempDir: () => tempDir } : undefined,
getSessionId: vi.fn().mockReturnValue('test-session-id'),
} as unknown as Config,
geminiClient: { getChat: () => ({ getHistory: () => [] }) },
},
},
});
it('captures a heap snapshot AFTER opening the bug URL when RSS exceeds 2 GB', async () => {
memoryUsageMock.mockReturnValue({
rss: 3 * 1024 * 1024 * 1024,
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0,
});
vi.mocked(captureHeapSnapshot).mockResolvedValueOnce(undefined);
const tempDir = path.join('/tmp', 'gemini-test');
const context = buildHighMemoryContext(tempDir);
if (!bugCommand.action) throw new Error('Action is not defined');
await bugCommand.action(context, 'A memory bug');
const now = new Date('2024-01-01T00:00:00Z').getTime();
const expectedSnapshotPath = path.join(
tempDir,
`bug-memory-${now}.heapsnapshot`,
);
expect(captureHeapSnapshot).toHaveBeenCalledWith(expectedSnapshotPath);
const addItem = vi.mocked(context.ui.addItem);
const callOrder = addItem.mock.invocationCallOrder;
const openOrder = vi.mocked(open).mock.invocationCallOrder[0];
// The URL message must precede the "capturing" message so the user sees
// the URL before the 20+ second snapshot starts.
expect(callOrder[0]).toBeLessThan(openOrder);
expect(callOrder[1]).toBeGreaterThan(openOrder);
expect(addItem.mock.calls[1][0].text).toContain('High memory usage');
expect(addItem.mock.calls[2][0].text).toContain('Heap snapshot saved');
expect(addItem.mock.calls[2][0].text).toContain(expectedSnapshotPath);
expect(addItem.mock.calls[2][0].type).toBe(MessageType.INFO);
});
it('skips auto-capture when RSS is below the 2 GB threshold', async () => {
memoryUsageMock.mockReturnValue({
rss: 1 * 1024 * 1024 * 1024,
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0,
});
const context = buildHighMemoryContext('/tmp/gemini-test');
if (!bugCommand.action) throw new Error('Action is not defined');
await bugCommand.action(context, 'A light bug');
expect(captureHeapSnapshot).not.toHaveBeenCalled();
});
it('reports an error if the auto-capture fails but does not throw', async () => {
memoryUsageMock.mockReturnValue({
rss: 3 * 1024 * 1024 * 1024,
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0,
});
vi.mocked(captureHeapSnapshot).mockRejectedValueOnce(
new Error('inspector failure'),
);
const context = buildHighMemoryContext('/tmp/gemini-test');
if (!bugCommand.action) throw new Error('Action is not defined');
await expect(
bugCommand.action(context, 'A memory bug'),
).resolves.toBeUndefined();
const addItem = vi.mocked(context.ui.addItem).mock.calls;
const lastCall = addItem[addItem.length - 1][0];
expect(lastCall.type).toBe(MessageType.ERROR);
expect(lastCall.text).toContain('inspector failure');
});
});