@google/gemini-cli-core 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/google-gemini-cli-core-0.1.13.tgz +0 -0
- package/dist/src/code_assist/codeAssist.js +2 -2
- package/dist/src/code_assist/codeAssist.js.map +1 -1
- package/dist/src/code_assist/oauth2.js +9 -2
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.test.js +99 -7
- package/dist/src/code_assist/oauth2.test.js.map +1 -1
- package/dist/src/code_assist/server.d.ts +4 -6
- package/dist/src/code_assist/server.js +4 -69
- package/dist/src/code_assist/server.js.map +1 -1
- package/dist/src/code_assist/setup.d.ts +6 -1
- package/dist/src/code_assist/setup.js +4 -1
- package/dist/src/code_assist/setup.js.map +1 -1
- package/dist/src/code_assist/setup.test.js +4 -1
- package/dist/src/code_assist/setup.test.js.map +1 -1
- package/dist/src/code_assist/types.d.ts +2 -2
- package/dist/src/config/config.d.ts +28 -7
- package/dist/src/config/config.js +52 -16
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +1 -23
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/flashFallback.test.js +1 -1
- package/dist/src/config/flashFallback.test.js.map +1 -1
- package/dist/src/core/client.d.ts +5 -2
- package/dist/src/core/client.js +39 -17
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +51 -0
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/contentGenerator.d.ts +1 -1
- package/dist/src/core/contentGenerator.js +1 -1
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +4 -3
- package/dist/src/core/geminiChat.js +8 -11
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/geminiRequest.js +2 -37
- package/dist/src/core/geminiRequest.js.map +1 -1
- package/dist/src/core/logger.js +6 -0
- package/dist/src/core/logger.js.map +1 -1
- package/dist/src/core/logger.test.js +1 -1
- package/dist/src/core/logger.test.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.test.js +5 -5
- package/dist/src/core/prompts.js +42 -18
- package/dist/src/core/prompts.js.map +1 -1
- package/dist/src/core/prompts.test.js +121 -4
- package/dist/src/core/prompts.test.js.map +1 -1
- package/dist/src/core/turn.d.ts +7 -2
- package/dist/src/core/turn.js +9 -0
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/core/turn.test.js +129 -0
- package/dist/src/core/turn.test.js.map +1 -1
- package/dist/src/ide/ide-client.d.ts +28 -0
- package/dist/src/ide/ide-client.js +88 -0
- package/dist/src/ide/ide-client.js.map +1 -0
- package/dist/src/ide/ideContext.d.ts +174 -0
- package/dist/src/{services → ide}/ideContext.js +28 -25
- package/dist/src/ide/ideContext.js.map +1 -0
- package/dist/src/{services → ide}/ideContext.test.js +39 -39
- package/dist/src/ide/ideContext.test.js.map +1 -0
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.js +11 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/google-auth-provider.d.ts +23 -0
- package/dist/src/mcp/google-auth-provider.js +63 -0
- package/dist/src/mcp/google-auth-provider.js.map +1 -0
- package/dist/src/mcp/google-auth-provider.test.js +54 -0
- package/dist/src/mcp/google-auth-provider.test.js.map +1 -0
- package/dist/src/mcp/oauth-provider.d.ts +5 -1
- package/dist/src/mcp/oauth-provider.js +36 -11
- package/dist/src/mcp/oauth-provider.js.map +1 -1
- package/dist/src/mcp/oauth-provider.test.js +2 -2
- package/dist/src/mcp/oauth-provider.test.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.d.ts +3 -1
- package/dist/src/mcp/oauth-token-storage.js +3 -1
- package/dist/src/mcp/oauth-token-storage.js.map +1 -1
- package/dist/src/prompts/mcp-prompts.d.ts +8 -0
- package/dist/src/prompts/mcp-prompts.js +13 -0
- package/dist/src/prompts/mcp-prompts.js.map +1 -0
- package/dist/src/prompts/prompt-registry.d.ts +26 -0
- package/dist/src/prompts/prompt-registry.js +47 -0
- package/dist/src/prompts/prompt-registry.js.map +1 -0
- package/dist/src/services/fileDiscoveryService.test.js +101 -60
- package/dist/src/services/fileDiscoveryService.test.js.map +1 -1
- package/dist/src/services/gitService.test.js +67 -86
- package/dist/src/services/gitService.test.js.map +1 -1
- package/dist/src/services/loopDetectionService.d.ts +48 -5
- package/dist/src/services/loopDetectionService.js +124 -38
- package/dist/src/services/loopDetectionService.js.map +1 -1
- package/dist/src/services/loopDetectionService.test.js +39 -112
- package/dist/src/services/loopDetectionService.test.js.map +1 -1
- package/dist/src/services/shellExecutionService.d.ts +70 -0
- package/dist/src/services/shellExecutionService.js +152 -0
- package/dist/src/services/shellExecutionService.js.map +1 -0
- package/dist/src/services/shellExecutionService.test.d.ts +6 -0
- package/dist/src/services/shellExecutionService.test.js +258 -0
- package/dist/src/services/shellExecutionService.test.js.map +1 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +2 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +17 -2
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/constants.d.ts +1 -0
- package/dist/src/telemetry/constants.js +1 -0
- package/dist/src/telemetry/constants.js.map +1 -1
- package/dist/src/telemetry/file-exporters.d.ts +28 -0
- package/dist/src/telemetry/file-exporters.js +62 -0
- package/dist/src/telemetry/file-exporters.js.map +1 -0
- package/dist/src/telemetry/loggers.d.ts +2 -1
- package/dist/src/telemetry/loggers.js +17 -1
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/sdk.js +17 -6
- package/dist/src/telemetry/sdk.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +9 -2
- package/dist/src/telemetry/types.js +13 -1
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/edit.js +10 -4
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/edit.test.js +12 -0
- package/dist/src/tools/edit.test.js.map +1 -1
- package/dist/src/tools/glob.test.js +7 -4
- package/dist/src/tools/glob.test.js.map +1 -1
- package/dist/src/tools/grep.test.js +5 -5
- package/dist/src/tools/grep.test.js.map +1 -1
- package/dist/src/tools/ls.d.ts +5 -2
- package/dist/src/tools/ls.js +39 -10
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/mcp-client.d.ts +31 -3
- package/dist/src/tools/mcp-client.js +478 -38
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +99 -7
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/mcp-tool.js +1 -1
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/mcp-tool.test.js +34 -0
- package/dist/src/tools/mcp-tool.test.js.map +1 -1
- package/dist/src/tools/modifiable-tool.test.js +51 -62
- package/dist/src/tools/modifiable-tool.test.js.map +1 -1
- package/dist/src/tools/read-file.test.js +98 -69
- package/dist/src/tools/read-file.test.js.map +1 -1
- package/dist/src/tools/read-many-files.d.ts +5 -3
- package/dist/src/tools/read-many-files.js +62 -22
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/read-many-files.test.js +5 -2
- package/dist/src/tools/read-many-files.test.js.map +1 -1
- package/dist/src/tools/shell.d.ts +3 -23
- package/dist/src/tools/shell.js +165 -296
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/shell.test.js +254 -392
- package/dist/src/tools/shell.test.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +13 -1
- package/dist/src/tools/tool-registry.js +46 -2
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tool-registry.test.js +5 -5
- package/dist/src/tools/tool-registry.test.js.map +1 -1
- package/dist/src/utils/bfsFileSearch.d.ts +2 -0
- package/dist/src/utils/bfsFileSearch.js +4 -1
- package/dist/src/utils/bfsFileSearch.js.map +1 -1
- package/dist/src/utils/bfsFileSearch.test.js +108 -105
- package/dist/src/utils/bfsFileSearch.test.js.map +1 -1
- package/dist/src/utils/editCorrector.js +4 -4
- package/dist/src/utils/editCorrector.js.map +1 -1
- package/dist/src/utils/editCorrector.test.js +1 -1
- package/dist/src/utils/editor.js +16 -10
- package/dist/src/utils/editor.js.map +1 -1
- package/dist/src/utils/editor.test.js +128 -28
- package/dist/src/utils/editor.test.js.map +1 -1
- package/dist/src/utils/errorReporting.d.ts +1 -1
- package/dist/src/utils/errorReporting.js +2 -2
- package/dist/src/utils/errorReporting.js.map +1 -1
- package/dist/src/utils/errorReporting.test.js +44 -38
- package/dist/src/utils/errorReporting.test.js.map +1 -1
- package/dist/src/utils/fileUtils.d.ts +4 -4
- package/dist/src/utils/fileUtils.js +31 -15
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/fileUtils.test.js +37 -37
- package/dist/src/utils/fileUtils.test.js.map +1 -1
- package/dist/src/utils/formatters.d.ts +6 -0
- package/dist/src/utils/formatters.js +16 -0
- package/dist/src/utils/formatters.js.map +1 -0
- package/dist/src/utils/getFolderStructure.d.ts +3 -2
- package/dist/src/utils/getFolderStructure.js +27 -28
- package/dist/src/utils/getFolderStructure.js.map +1 -1
- package/dist/src/utils/getFolderStructure.test.js +169 -187
- package/dist/src/utils/getFolderStructure.test.js.map +1 -1
- package/dist/src/utils/gitIgnoreParser.js +4 -7
- package/dist/src/utils/gitIgnoreParser.js.map +1 -1
- package/dist/src/utils/gitIgnoreParser.test.js +70 -61
- package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.d.ts +2 -1
- package/dist/src/utils/memoryDiscovery.js +11 -5
- package/dist/src/utils/memoryDiscovery.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.test.js +160 -371
- package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
- package/dist/src/utils/partUtils.d.ts +14 -0
- package/dist/src/utils/partUtils.js +65 -0
- package/dist/src/utils/partUtils.js.map +1 -0
- package/dist/src/utils/partUtils.test.d.ts +6 -0
- package/dist/src/utils/partUtils.test.js +130 -0
- package/dist/src/utils/partUtils.test.js.map +1 -0
- package/dist/src/utils/paths.d.ts +11 -0
- package/dist/src/utils/paths.js +17 -1
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/src/utils/quotaErrorDetection.js +0 -2
- package/dist/src/utils/quotaErrorDetection.js.map +1 -1
- package/dist/src/utils/retry.js +1 -1
- package/dist/src/utils/retry.js.map +1 -1
- package/dist/src/utils/schemaValidator.d.ts +1 -1
- package/dist/src/utils/schemaValidator.js +6 -3
- package/dist/src/utils/schemaValidator.js.map +1 -1
- package/dist/src/utils/shell-utils.d.ts +78 -0
- package/dist/src/utils/shell-utils.js +306 -0
- package/dist/src/utils/shell-utils.js.map +1 -0
- package/dist/src/utils/shell-utils.test.d.ts +6 -0
- package/dist/src/utils/shell-utils.test.js +200 -0
- package/dist/src/utils/shell-utils.test.js.map +1 -0
- package/dist/src/utils/summarizer.js +1 -30
- package/dist/src/utils/summarizer.js.map +1 -1
- package/dist/src/utils/systemEncoding.d.ts +40 -0
- package/dist/src/utils/systemEncoding.js +149 -0
- package/dist/src/utils/systemEncoding.js.map +1 -0
- package/dist/src/utils/systemEncoding.test.d.ts +6 -0
- package/dist/src/utils/systemEncoding.test.js +368 -0
- package/dist/src/utils/systemEncoding.test.js.map +1 -0
- package/dist/src/utils/textUtils.d.ts +13 -0
- package/dist/src/utils/textUtils.js +28 -0
- package/dist/src/utils/textUtils.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/dist/google-gemini-cli-core-0.1.12.tgz +0 -0
- package/dist/src/core/geminiRequest.test.js +0 -72
- package/dist/src/core/geminiRequest.test.js.map +0 -1
- package/dist/src/services/ideContext.d.ts +0 -126
- package/dist/src/services/ideContext.js.map +0 -1
- package/dist/src/services/ideContext.test.js.map +0 -1
- /package/dist/src/{services → ide}/ideContext.test.d.ts +0 -0
- /package/dist/src/{core/geminiRequest.test.d.ts → mcp/google-auth-provider.test.d.ts} +0 -0
|
@@ -6,22 +6,13 @@
|
|
|
6
6
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
7
|
import { GitService } from './gitService.js';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
import * as fs from 'fs/promises';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import { getProjectHash, GEMINI_DIR } from '../utils/paths.js';
|
|
9
12
|
const hoistedMockExec = vi.hoisted(() => vi.fn());
|
|
10
13
|
vi.mock('node:child_process', () => ({
|
|
11
14
|
exec: hoistedMockExec,
|
|
12
15
|
}));
|
|
13
|
-
const hoistedMockMkdir = vi.hoisted(() => vi.fn());
|
|
14
|
-
const hoistedMockReadFile = vi.hoisted(() => vi.fn());
|
|
15
|
-
const hoistedMockWriteFile = vi.hoisted(() => vi.fn());
|
|
16
|
-
vi.mock('fs/promises', async (importOriginal) => {
|
|
17
|
-
const actual = (await importOriginal());
|
|
18
|
-
return {
|
|
19
|
-
...actual,
|
|
20
|
-
mkdir: hoistedMockMkdir,
|
|
21
|
-
readFile: hoistedMockReadFile,
|
|
22
|
-
writeFile: hoistedMockWriteFile,
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
16
|
const hoistedMockEnv = vi.hoisted(() => vi.fn());
|
|
26
17
|
const hoistedMockSimpleGit = vi.hoisted(() => vi.fn());
|
|
27
18
|
const hoistedMockCheckIsRepo = vi.hoisted(() => vi.fn());
|
|
@@ -44,34 +35,26 @@ const hoistedIsGitRepositoryMock = vi.hoisted(() => vi.fn());
|
|
|
44
35
|
vi.mock('../utils/gitUtils.js', () => ({
|
|
45
36
|
isGitRepository: hoistedIsGitRepositoryMock,
|
|
46
37
|
}));
|
|
47
|
-
const hoistedMockIsNodeError = vi.hoisted(() => vi.fn());
|
|
48
|
-
vi.mock('../utils/errors.js', () => ({
|
|
49
|
-
isNodeError: hoistedMockIsNodeError,
|
|
50
|
-
}));
|
|
51
38
|
const hoistedMockHomedir = vi.hoisted(() => vi.fn());
|
|
52
|
-
vi.mock('os', () =>
|
|
53
|
-
|
|
54
|
-
}));
|
|
55
|
-
const hoistedMockCreateHash = vi.hoisted(() => {
|
|
56
|
-
const mockUpdate = vi.fn().mockReturnThis();
|
|
57
|
-
const mockDigest = vi.fn();
|
|
39
|
+
vi.mock('os', async (importOriginal) => {
|
|
40
|
+
const actual = await importOriginal();
|
|
58
41
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
digest: mockDigest,
|
|
62
|
-
})),
|
|
63
|
-
mockUpdate,
|
|
64
|
-
mockDigest,
|
|
42
|
+
...actual,
|
|
43
|
+
homedir: hoistedMockHomedir,
|
|
65
44
|
};
|
|
66
45
|
});
|
|
67
|
-
vi.mock('crypto', () => ({
|
|
68
|
-
createHash: hoistedMockCreateHash.createHash,
|
|
69
|
-
}));
|
|
70
46
|
describe('GitService', () => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
47
|
+
let testRootDir;
|
|
48
|
+
let projectRoot;
|
|
49
|
+
let homedir;
|
|
50
|
+
let hash;
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
testRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'git-service-test-'));
|
|
53
|
+
projectRoot = path.join(testRootDir, 'project');
|
|
54
|
+
homedir = path.join(testRootDir, 'home');
|
|
55
|
+
await fs.mkdir(projectRoot, { recursive: true });
|
|
56
|
+
await fs.mkdir(homedir, { recursive: true });
|
|
57
|
+
hash = getProjectHash(projectRoot);
|
|
75
58
|
vi.clearAllMocks();
|
|
76
59
|
hoistedIsGitRepositoryMock.mockReturnValue(true);
|
|
77
60
|
hoistedMockExec.mockImplementation((command, callback) => {
|
|
@@ -83,13 +66,7 @@ describe('GitService', () => {
|
|
|
83
66
|
}
|
|
84
67
|
return {};
|
|
85
68
|
});
|
|
86
|
-
|
|
87
|
-
hoistedMockReadFile.mockResolvedValue('');
|
|
88
|
-
hoistedMockWriteFile.mockResolvedValue(undefined);
|
|
89
|
-
hoistedMockIsNodeError.mockImplementation((e) => e instanceof Error);
|
|
90
|
-
hoistedMockHomedir.mockReturnValue(mockHomedir);
|
|
91
|
-
hoistedMockCreateHash.mockUpdate.mockReturnThis();
|
|
92
|
-
hoistedMockCreateHash.mockDigest.mockReturnValue(mockHash);
|
|
69
|
+
hoistedMockHomedir.mockReturnValue(homedir);
|
|
93
70
|
hoistedMockEnv.mockImplementation(() => ({
|
|
94
71
|
checkIsRepo: hoistedMockCheckIsRepo,
|
|
95
72
|
init: hoistedMockInit,
|
|
@@ -113,17 +90,18 @@ describe('GitService', () => {
|
|
|
113
90
|
commit: 'initial',
|
|
114
91
|
});
|
|
115
92
|
});
|
|
116
|
-
afterEach(() => {
|
|
93
|
+
afterEach(async () => {
|
|
117
94
|
vi.restoreAllMocks();
|
|
95
|
+
await fs.rm(testRootDir, { recursive: true, force: true });
|
|
118
96
|
});
|
|
119
97
|
describe('constructor', () => {
|
|
120
|
-
it('should successfully create an instance
|
|
121
|
-
expect(() => new GitService(
|
|
98
|
+
it('should successfully create an instance', () => {
|
|
99
|
+
expect(() => new GitService(projectRoot)).not.toThrow();
|
|
122
100
|
});
|
|
123
101
|
});
|
|
124
102
|
describe('verifyGitAvailability', () => {
|
|
125
103
|
it('should resolve true if git --version command succeeds', async () => {
|
|
126
|
-
const service = new GitService(
|
|
104
|
+
const service = new GitService(projectRoot);
|
|
127
105
|
await expect(service.verifyGitAvailability()).resolves.toBe(true);
|
|
128
106
|
});
|
|
129
107
|
it('should resolve false if git --version command fails', async () => {
|
|
@@ -131,7 +109,7 @@ describe('GitService', () => {
|
|
|
131
109
|
callback(new Error('git not found'));
|
|
132
110
|
return {};
|
|
133
111
|
});
|
|
134
|
-
const service = new GitService(
|
|
112
|
+
const service = new GitService(projectRoot);
|
|
135
113
|
await expect(service.verifyGitAvailability()).resolves.toBe(false);
|
|
136
114
|
});
|
|
137
115
|
});
|
|
@@ -141,11 +119,11 @@ describe('GitService', () => {
|
|
|
141
119
|
callback(new Error('git not found'));
|
|
142
120
|
return {};
|
|
143
121
|
});
|
|
144
|
-
const service = new GitService(
|
|
122
|
+
const service = new GitService(projectRoot);
|
|
145
123
|
await expect(service.initialize()).rejects.toThrow('Checkpointing is enabled, but Git is not installed. Please install Git or disable checkpointing to continue.');
|
|
146
124
|
});
|
|
147
125
|
it('should call setupShadowGitRepository if Git is available', async () => {
|
|
148
|
-
const service = new GitService(
|
|
126
|
+
const service = new GitService(projectRoot);
|
|
149
127
|
const setupSpy = vi
|
|
150
128
|
.spyOn(service, 'setupShadowGitRepository')
|
|
151
129
|
.mockResolvedValue(undefined);
|
|
@@ -154,64 +132,67 @@ describe('GitService', () => {
|
|
|
154
132
|
});
|
|
155
133
|
});
|
|
156
134
|
describe('setupShadowGitRepository', () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const service = new GitService(mockProjectRoot);
|
|
163
|
-
await service.setupShadowGitRepository();
|
|
164
|
-
const expectedConfigContent = '[user]\n name = Gemini CLI\n email = gemini-cli@google.com\n[commit]\n gpgsign = false\n';
|
|
165
|
-
expect(hoistedMockWriteFile).toHaveBeenCalledWith(gitConfigPath, expectedConfigContent);
|
|
135
|
+
let repoDir;
|
|
136
|
+
let gitConfigPath;
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
repoDir = path.join(homedir, GEMINI_DIR, 'history', hash);
|
|
139
|
+
gitConfigPath = path.join(repoDir, '.gitconfig');
|
|
166
140
|
});
|
|
167
141
|
it('should create history and repository directories', async () => {
|
|
168
|
-
const service = new GitService(
|
|
142
|
+
const service = new GitService(projectRoot);
|
|
169
143
|
await service.setupShadowGitRepository();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
144
|
+
const stats = await fs.stat(repoDir);
|
|
145
|
+
expect(stats.isDirectory()).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
it('should create a .gitconfig file with the correct content', async () => {
|
|
148
|
+
const service = new GitService(projectRoot);
|
|
149
|
+
await service.setupShadowGitRepository();
|
|
150
|
+
const expectedConfigContent = '[user]\n name = Gemini CLI\n email = gemini-cli@google.com\n[commit]\n gpgsign = false\n';
|
|
151
|
+
const actualConfigContent = await fs.readFile(gitConfigPath, 'utf-8');
|
|
152
|
+
expect(actualConfigContent).toBe(expectedConfigContent);
|
|
173
153
|
});
|
|
174
154
|
it('should initialize git repo in historyDir if not already initialized', async () => {
|
|
175
155
|
hoistedMockCheckIsRepo.mockResolvedValue(false);
|
|
176
|
-
const service = new GitService(
|
|
156
|
+
const service = new GitService(projectRoot);
|
|
177
157
|
await service.setupShadowGitRepository();
|
|
178
158
|
expect(hoistedMockSimpleGit).toHaveBeenCalledWith(repoDir);
|
|
179
159
|
expect(hoistedMockInit).toHaveBeenCalled();
|
|
180
160
|
});
|
|
181
161
|
it('should not initialize git repo if already initialized', async () => {
|
|
182
162
|
hoistedMockCheckIsRepo.mockResolvedValue(true);
|
|
183
|
-
const service = new GitService(
|
|
163
|
+
const service = new GitService(projectRoot);
|
|
184
164
|
await service.setupShadowGitRepository();
|
|
185
165
|
expect(hoistedMockInit).not.toHaveBeenCalled();
|
|
186
166
|
});
|
|
187
167
|
it('should copy .gitignore from projectRoot if it exists', async () => {
|
|
188
|
-
const gitignoreContent =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
return '';
|
|
194
|
-
});
|
|
195
|
-
const service = new GitService(mockProjectRoot);
|
|
168
|
+
const gitignoreContent = 'node_modules/\n.env';
|
|
169
|
+
const visibleGitIgnorePath = path.join(projectRoot, '.gitignore');
|
|
170
|
+
await fs.writeFile(visibleGitIgnorePath, gitignoreContent);
|
|
171
|
+
const service = new GitService(projectRoot);
|
|
196
172
|
await service.setupShadowGitRepository();
|
|
197
|
-
|
|
198
|
-
|
|
173
|
+
const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
|
|
174
|
+
const copiedContent = await fs.readFile(hiddenGitIgnorePath, 'utf-8');
|
|
175
|
+
expect(copiedContent).toBe(gitignoreContent);
|
|
176
|
+
});
|
|
177
|
+
it('should not create a .gitignore in shadow repo if project .gitignore does not exist', async () => {
|
|
178
|
+
const service = new GitService(projectRoot);
|
|
179
|
+
await service.setupShadowGitRepository();
|
|
180
|
+
const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
|
|
181
|
+
// An empty string is written if the file doesn't exist.
|
|
182
|
+
const content = await fs.readFile(hiddenGitIgnorePath, 'utf-8');
|
|
183
|
+
expect(content).toBe('');
|
|
199
184
|
});
|
|
200
185
|
it('should throw an error if reading projectRoot .gitignore fails with other errors', async () => {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
hoistedMockIsNodeError.mockImplementation((e) => e instanceof Error);
|
|
209
|
-
const service = new GitService(mockProjectRoot);
|
|
210
|
-
await expect(service.setupShadowGitRepository()).rejects.toThrow('Read permission denied');
|
|
186
|
+
const visibleGitIgnorePath = path.join(projectRoot, '.gitignore');
|
|
187
|
+
// Create a directory instead of a file to cause a read error
|
|
188
|
+
await fs.mkdir(visibleGitIgnorePath);
|
|
189
|
+
const service = new GitService(projectRoot);
|
|
190
|
+
// EISDIR is the expected error code on Unix-like systems
|
|
191
|
+
await expect(service.setupShadowGitRepository()).rejects.toThrow(/EISDIR: illegal operation on a directory, read|EBUSY: resource busy or locked, read/);
|
|
211
192
|
});
|
|
212
193
|
it('should make an initial commit if no commits exist in history repo', async () => {
|
|
213
194
|
hoistedMockCheckIsRepo.mockResolvedValue(false);
|
|
214
|
-
const service = new GitService(
|
|
195
|
+
const service = new GitService(projectRoot);
|
|
215
196
|
await service.setupShadowGitRepository();
|
|
216
197
|
expect(hoistedMockCommit).toHaveBeenCalledWith('Initial commit', {
|
|
217
198
|
'--allow-empty': null,
|
|
@@ -219,7 +200,7 @@ describe('GitService', () => {
|
|
|
219
200
|
});
|
|
220
201
|
it('should not make an initial commit if commits already exist', async () => {
|
|
221
202
|
hoistedMockCheckIsRepo.mockResolvedValue(true);
|
|
222
|
-
const service = new GitService(
|
|
203
|
+
const service = new GitService(projectRoot);
|
|
223
204
|
await service.setupShadowGitRepository();
|
|
224
205
|
expect(hoistedMockCommit).not.toHaveBeenCalled();
|
|
225
206
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitService.test.js","sourceRoot":"","sources":["../../../src/services/gitService.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"gitService.test.js","sourceRoot":"","sources":["../../../src/services/gitService.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,11 +11,13 @@ import { Config } from '../config/config.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export declare class LoopDetectionService {
|
|
13
13
|
private readonly config;
|
|
14
|
+
private promptId;
|
|
14
15
|
private lastToolCallKey;
|
|
15
16
|
private toolCallRepetitionCount;
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
17
|
+
private streamContentHistory;
|
|
18
|
+
private contentStats;
|
|
19
|
+
private lastContentIndex;
|
|
20
|
+
private loopDetected;
|
|
19
21
|
private turnsInCurrentPrompt;
|
|
20
22
|
private llmCheckInterval;
|
|
21
23
|
private lastCheckTurn;
|
|
@@ -39,13 +41,54 @@ export declare class LoopDetectionService {
|
|
|
39
41
|
*/
|
|
40
42
|
turnStarted(signal: AbortSignal): Promise<boolean>;
|
|
41
43
|
private checkToolCallLoop;
|
|
44
|
+
/**
|
|
45
|
+
* Detects content loops by analyzing streaming text for repetitive patterns.
|
|
46
|
+
*
|
|
47
|
+
* The algorithm works by:
|
|
48
|
+
* 1. Appending new content to the streaming history
|
|
49
|
+
* 2. Truncating history if it exceeds the maximum length
|
|
50
|
+
* 3. Analyzing content chunks for repetitive patterns using hashing
|
|
51
|
+
* 4. Detecting loops when identical chunks appear frequently within a short distance
|
|
52
|
+
*/
|
|
42
53
|
private checkContentLoop;
|
|
54
|
+
/**
|
|
55
|
+
* Truncates the content history to prevent unbounded memory growth.
|
|
56
|
+
* When truncating, adjusts all stored indices to maintain their relative positions.
|
|
57
|
+
*/
|
|
58
|
+
private truncateAndUpdate;
|
|
59
|
+
/**
|
|
60
|
+
* Analyzes content in fixed-size chunks to detect repetitive patterns.
|
|
61
|
+
*
|
|
62
|
+
* Uses a sliding window approach:
|
|
63
|
+
* 1. Extract chunks of fixed size (CONTENT_CHUNK_SIZE)
|
|
64
|
+
* 2. Hash each chunk for efficient comparison
|
|
65
|
+
* 3. Track positions where identical chunks appear
|
|
66
|
+
* 4. Detect loops when chunks repeat frequently within a short distance
|
|
67
|
+
*/
|
|
68
|
+
private analyzeContentChunksForLoop;
|
|
69
|
+
private hasMoreChunksToProcess;
|
|
70
|
+
/**
|
|
71
|
+
* Determines if a content chunk indicates a loop pattern.
|
|
72
|
+
*
|
|
73
|
+
* Loop detection logic:
|
|
74
|
+
* 1. Check if we've seen this hash before (new chunks are stored for future comparison)
|
|
75
|
+
* 2. Verify actual content matches to prevent hash collisions
|
|
76
|
+
* 3. Track all positions where this chunk appears
|
|
77
|
+
* 4. A loop is detected when the same chunk appears CONTENT_LOOP_THRESHOLD times
|
|
78
|
+
* within a small average distance (≤ 1.5 * chunk size)
|
|
79
|
+
*/
|
|
80
|
+
private isLoopDetectedForChunk;
|
|
81
|
+
/**
|
|
82
|
+
* Verifies that two chunks with the same hash actually contain identical content.
|
|
83
|
+
* This prevents false positives from hash collisions.
|
|
84
|
+
*/
|
|
85
|
+
private isActualContentMatch;
|
|
43
86
|
private checkForLoopWithLLM;
|
|
44
87
|
/**
|
|
45
88
|
* Resets all loop detection state.
|
|
46
89
|
*/
|
|
47
|
-
reset(): void;
|
|
90
|
+
reset(promptId: string): void;
|
|
48
91
|
private resetToolCallCount;
|
|
49
|
-
private
|
|
92
|
+
private resetContentTracking;
|
|
50
93
|
private resetLlmCheckTracking;
|
|
51
94
|
}
|
|
@@ -11,6 +11,8 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/config.js';
|
|
|
11
11
|
import { Type } from '@google/genai';
|
|
12
12
|
const TOOL_CALL_LOOP_THRESHOLD = 5;
|
|
13
13
|
const CONTENT_LOOP_THRESHOLD = 10;
|
|
14
|
+
const CONTENT_CHUNK_SIZE = 50;
|
|
15
|
+
const MAX_HISTORY_LENGTH = 1000;
|
|
14
16
|
/**
|
|
15
17
|
* The number of recent conversation turns to include in the history when asking the LLM to check for a loop.
|
|
16
18
|
*/
|
|
@@ -34,20 +36,21 @@ const MIN_LLM_CHECK_INTERVAL = 5;
|
|
|
34
36
|
* This is used when the confidence of a loop is low, to check less frequently.
|
|
35
37
|
*/
|
|
36
38
|
const MAX_LLM_CHECK_INTERVAL = 15;
|
|
37
|
-
const SENTENCE_ENDING_PUNCTUATION_REGEX = /[.!?]+(?=\s|$)/;
|
|
38
39
|
/**
|
|
39
40
|
* Service for detecting and preventing infinite loops in AI responses.
|
|
40
41
|
* Monitors tool call repetitions and content sentence repetitions.
|
|
41
42
|
*/
|
|
42
43
|
export class LoopDetectionService {
|
|
43
44
|
config;
|
|
45
|
+
promptId = '';
|
|
44
46
|
// Tool call tracking
|
|
45
47
|
lastToolCallKey = null;
|
|
46
48
|
toolCallRepetitionCount = 0;
|
|
47
49
|
// Content streaming tracking
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
streamContentHistory = '';
|
|
51
|
+
contentStats = new Map();
|
|
52
|
+
lastContentIndex = 0;
|
|
53
|
+
loopDetected = false;
|
|
51
54
|
// LLM loop track tracking
|
|
52
55
|
turnsInCurrentPrompt = 0;
|
|
53
56
|
llmCheckInterval = DEFAULT_LLM_CHECK_INTERVAL;
|
|
@@ -66,17 +69,23 @@ export class LoopDetectionService {
|
|
|
66
69
|
* @returns true if a loop is detected, false otherwise
|
|
67
70
|
*/
|
|
68
71
|
addAndCheck(event) {
|
|
72
|
+
if (this.loopDetected) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
69
75
|
switch (event.type) {
|
|
70
76
|
case GeminiEventType.ToolCallRequest:
|
|
71
77
|
// content chanting only happens in one single stream, reset if there
|
|
72
78
|
// is a tool call in between
|
|
73
|
-
this.
|
|
74
|
-
|
|
79
|
+
this.resetContentTracking();
|
|
80
|
+
this.loopDetected = this.checkToolCallLoop(event.value);
|
|
81
|
+
break;
|
|
75
82
|
case GeminiEventType.Content:
|
|
76
|
-
|
|
83
|
+
this.loopDetected = this.checkContentLoop(event.value);
|
|
84
|
+
break;
|
|
77
85
|
default:
|
|
78
|
-
|
|
86
|
+
break;
|
|
79
87
|
}
|
|
88
|
+
return this.loopDetected;
|
|
80
89
|
}
|
|
81
90
|
/**
|
|
82
91
|
* Signals the start of a new turn in the conversation.
|
|
@@ -107,43 +116,116 @@ export class LoopDetectionService {
|
|
|
107
116
|
this.toolCallRepetitionCount = 1;
|
|
108
117
|
}
|
|
109
118
|
if (this.toolCallRepetitionCount >= TOOL_CALL_LOOP_THRESHOLD) {
|
|
110
|
-
logLoopDetected(this.config, new LoopDetectedEvent(LoopType.CONSECUTIVE_IDENTICAL_TOOL_CALLS));
|
|
119
|
+
logLoopDetected(this.config, new LoopDetectedEvent(LoopType.CONSECUTIVE_IDENTICAL_TOOL_CALLS, this.promptId));
|
|
111
120
|
return true;
|
|
112
121
|
}
|
|
113
122
|
return false;
|
|
114
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Detects content loops by analyzing streaming text for repetitive patterns.
|
|
126
|
+
*
|
|
127
|
+
* The algorithm works by:
|
|
128
|
+
* 1. Appending new content to the streaming history
|
|
129
|
+
* 2. Truncating history if it exceeds the maximum length
|
|
130
|
+
* 3. Analyzing content chunks for repetitive patterns using hashing
|
|
131
|
+
* 4. Detecting loops when identical chunks appear frequently within a short distance
|
|
132
|
+
*/
|
|
115
133
|
checkContentLoop(content) {
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
this.streamContentHistory += content;
|
|
135
|
+
this.truncateAndUpdate();
|
|
136
|
+
return this.analyzeContentChunksForLoop();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Truncates the content history to prevent unbounded memory growth.
|
|
140
|
+
* When truncating, adjusts all stored indices to maintain their relative positions.
|
|
141
|
+
*/
|
|
142
|
+
truncateAndUpdate() {
|
|
143
|
+
if (this.streamContentHistory.length <= MAX_HISTORY_LENGTH) {
|
|
144
|
+
return;
|
|
123
145
|
}
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
// Calculate how much content to remove from the beginning
|
|
147
|
+
const truncationAmount = this.streamContentHistory.length - MAX_HISTORY_LENGTH;
|
|
148
|
+
this.streamContentHistory =
|
|
149
|
+
this.streamContentHistory.slice(truncationAmount);
|
|
150
|
+
this.lastContentIndex = Math.max(0, this.lastContentIndex - truncationAmount);
|
|
151
|
+
// Update all stored chunk indices to account for the truncation
|
|
152
|
+
for (const [hash, oldIndices] of this.contentStats.entries()) {
|
|
153
|
+
const adjustedIndices = oldIndices
|
|
154
|
+
.map((index) => index - truncationAmount)
|
|
155
|
+
.filter((index) => index >= 0);
|
|
156
|
+
if (adjustedIndices.length > 0) {
|
|
157
|
+
this.contentStats.set(hash, adjustedIndices);
|
|
135
158
|
}
|
|
136
159
|
else {
|
|
137
|
-
this.
|
|
138
|
-
this.sentenceRepetitionCount = 1;
|
|
160
|
+
this.contentStats.delete(hash);
|
|
139
161
|
}
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Analyzes content in fixed-size chunks to detect repetitive patterns.
|
|
166
|
+
*
|
|
167
|
+
* Uses a sliding window approach:
|
|
168
|
+
* 1. Extract chunks of fixed size (CONTENT_CHUNK_SIZE)
|
|
169
|
+
* 2. Hash each chunk for efficient comparison
|
|
170
|
+
* 3. Track positions where identical chunks appear
|
|
171
|
+
* 4. Detect loops when chunks repeat frequently within a short distance
|
|
172
|
+
*/
|
|
173
|
+
analyzeContentChunksForLoop() {
|
|
174
|
+
while (this.hasMoreChunksToProcess()) {
|
|
175
|
+
// Extract current chunk of text
|
|
176
|
+
const currentChunk = this.streamContentHistory.substring(this.lastContentIndex, this.lastContentIndex + CONTENT_CHUNK_SIZE);
|
|
177
|
+
const chunkHash = createHash('sha256').update(currentChunk).digest('hex');
|
|
178
|
+
if (this.isLoopDetectedForChunk(currentChunk, chunkHash)) {
|
|
179
|
+
logLoopDetected(this.config, new LoopDetectedEvent(LoopType.CHANTING_IDENTICAL_SENTENCES, this.promptId));
|
|
142
180
|
return true;
|
|
143
181
|
}
|
|
182
|
+
// Move to next position in the sliding window
|
|
183
|
+
this.lastContentIndex++;
|
|
144
184
|
}
|
|
145
185
|
return false;
|
|
146
186
|
}
|
|
187
|
+
hasMoreChunksToProcess() {
|
|
188
|
+
return (this.lastContentIndex + CONTENT_CHUNK_SIZE <=
|
|
189
|
+
this.streamContentHistory.length);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Determines if a content chunk indicates a loop pattern.
|
|
193
|
+
*
|
|
194
|
+
* Loop detection logic:
|
|
195
|
+
* 1. Check if we've seen this hash before (new chunks are stored for future comparison)
|
|
196
|
+
* 2. Verify actual content matches to prevent hash collisions
|
|
197
|
+
* 3. Track all positions where this chunk appears
|
|
198
|
+
* 4. A loop is detected when the same chunk appears CONTENT_LOOP_THRESHOLD times
|
|
199
|
+
* within a small average distance (≤ 1.5 * chunk size)
|
|
200
|
+
*/
|
|
201
|
+
isLoopDetectedForChunk(chunk, hash) {
|
|
202
|
+
const existingIndices = this.contentStats.get(hash);
|
|
203
|
+
if (!existingIndices) {
|
|
204
|
+
this.contentStats.set(hash, [this.lastContentIndex]);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (!this.isActualContentMatch(chunk, existingIndices[0])) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
existingIndices.push(this.lastContentIndex);
|
|
211
|
+
if (existingIndices.length < CONTENT_LOOP_THRESHOLD) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
// Analyze the most recent occurrences to see if they're clustered closely together
|
|
215
|
+
const recentIndices = existingIndices.slice(-CONTENT_LOOP_THRESHOLD);
|
|
216
|
+
const totalDistance = recentIndices[recentIndices.length - 1] - recentIndices[0];
|
|
217
|
+
const averageDistance = totalDistance / (CONTENT_LOOP_THRESHOLD - 1);
|
|
218
|
+
const maxAllowedDistance = CONTENT_CHUNK_SIZE * 1.5;
|
|
219
|
+
return averageDistance <= maxAllowedDistance;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Verifies that two chunks with the same hash actually contain identical content.
|
|
223
|
+
* This prevents false positives from hash collisions.
|
|
224
|
+
*/
|
|
225
|
+
isActualContentMatch(currentChunk, originalIndex) {
|
|
226
|
+
const originalChunk = this.streamContentHistory.substring(originalIndex, originalIndex + CONTENT_CHUNK_SIZE);
|
|
227
|
+
return originalChunk === currentChunk;
|
|
228
|
+
}
|
|
147
229
|
async checkForLoopWithLLM(signal) {
|
|
148
230
|
const recentHistory = this.config
|
|
149
231
|
.getGeminiClient()
|
|
@@ -195,7 +277,7 @@ Please analyze the conversation history to determine the possibility that the co
|
|
|
195
277
|
if (typeof result.reasoning === 'string' && result.reasoning) {
|
|
196
278
|
console.warn(result.reasoning);
|
|
197
279
|
}
|
|
198
|
-
logLoopDetected(this.config, new LoopDetectedEvent(LoopType.LLM_DETECTED_LOOP));
|
|
280
|
+
logLoopDetected(this.config, new LoopDetectedEvent(LoopType.LLM_DETECTED_LOOP, this.promptId));
|
|
199
281
|
return true;
|
|
200
282
|
}
|
|
201
283
|
else {
|
|
@@ -209,19 +291,23 @@ Please analyze the conversation history to determine the possibility that the co
|
|
|
209
291
|
/**
|
|
210
292
|
* Resets all loop detection state.
|
|
211
293
|
*/
|
|
212
|
-
reset() {
|
|
294
|
+
reset(promptId) {
|
|
295
|
+
this.promptId = promptId;
|
|
213
296
|
this.resetToolCallCount();
|
|
214
|
-
this.
|
|
297
|
+
this.resetContentTracking();
|
|
215
298
|
this.resetLlmCheckTracking();
|
|
299
|
+
this.loopDetected = false;
|
|
216
300
|
}
|
|
217
301
|
resetToolCallCount() {
|
|
218
302
|
this.lastToolCallKey = null;
|
|
219
303
|
this.toolCallRepetitionCount = 0;
|
|
220
304
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
305
|
+
resetContentTracking(resetHistory = true) {
|
|
306
|
+
if (resetHistory) {
|
|
307
|
+
this.streamContentHistory = '';
|
|
308
|
+
}
|
|
309
|
+
this.contentStats.clear();
|
|
310
|
+
this.lastContentIndex = 0;
|
|
225
311
|
}
|
|
226
312
|
resetLlmCheckTracking() {
|
|
227
313
|
this.turnsInCurrentPrompt = 0;
|