File size: 4,105 Bytes
52efc7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import path from 'node:path';
import { bugMemoryCommand } from './bugMemoryCommand.js';
import { captureHeapSnapshot } from '../utils/memorySnapshot.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
import type { Config } from '@google/gemini-cli-core';

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: 1234 }),
  };
});

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    debugLogger: {
      error: vi.fn(),
      log: vi.fn(),
      debug: vi.fn(),
      warn: vi.fn(),
    },
  };
});

function makeContextWithTempDir(tempDir: string | undefined) {
  return createMockCommandContext({
    services: {
      agentContext: {
        config: {
          storage: tempDir ? { getProjectTempDir: () => tempDir } : undefined,
        } as unknown as Config,
      },
    },
  });
}

describe('bugMemoryCommand', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
  });

  afterEach(() => {
    vi.clearAllMocks();
    vi.useRealTimers();
  });

  it('declares itself as a non-auto-executing built-in command', () => {
    expect(bugMemoryCommand.name).toBe('bug-memory');
    expect(bugMemoryCommand.autoExecute).toBe(false);
    expect(bugMemoryCommand.description).toBeTruthy();
  });

  it('captures a heap snapshot and reports the file path', async () => {
    const tempDir = path.join('/tmp', 'gemini-test');
    const context = makeContextWithTempDir(tempDir);
    vi.mocked(captureHeapSnapshot).mockResolvedValueOnce(undefined);

    if (!bugMemoryCommand.action) throw new Error('Action missing');
    await bugMemoryCommand.action(context, '');

    const expectedPath = path.join(
      tempDir,
      `bug-memory-${new Date('2024-01-01T00:00:00Z').getTime()}.heapsnapshot`,
    );
    expect(captureHeapSnapshot).toHaveBeenCalledWith(expectedPath);

    const addItemCalls = vi.mocked(context.ui.addItem).mock.calls;
    expect(addItemCalls).toHaveLength(2);
    expect(addItemCalls[0][0]).toMatchObject({ type: MessageType.INFO });
    expect(addItemCalls[0][0].text).toContain(expectedPath);
    expect(addItemCalls[1][0]).toMatchObject({ type: MessageType.INFO });
    expect(addItemCalls[1][0].text).toContain('Heap snapshot saved');
    expect(addItemCalls[1][0].text).toContain(expectedPath);
  });

  it('surfaces an error if capture fails', async () => {
    const context = makeContextWithTempDir('/tmp/gemini-test');
    vi.mocked(captureHeapSnapshot).mockRejectedValueOnce(
      new Error('inspector disconnected'),
    );

    if (!bugMemoryCommand.action) throw new Error('Action missing');
    await bugMemoryCommand.action(context, '');

    const addItemCalls = vi.mocked(context.ui.addItem).mock.calls;
    const lastCall = addItemCalls[addItemCalls.length - 1][0];
    expect(lastCall.type).toBe(MessageType.ERROR);
    expect(lastCall.text).toContain('inspector disconnected');
  });

  it('emits an error when no project temp directory is available', async () => {
    const context = makeContextWithTempDir(undefined);

    if (!bugMemoryCommand.action) throw new Error('Action missing');
    await bugMemoryCommand.action(context, '');

    expect(captureHeapSnapshot).not.toHaveBeenCalled();
    const addItemCalls = vi.mocked(context.ui.addItem).mock.calls;
    expect(addItemCalls).toHaveLength(1);
    expect(addItemCalls[0][0].type).toBe(MessageType.ERROR);
    expect(addItemCalls[0][0].text).toContain('temp directory');
  });
});