diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index b994de98..99cf76c4 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -4,9 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getCoreSystemPrompt } from './prompts.js'; import { isGitRepository } from '../utils/gitUtils.js'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { GEMINI_CONFIG_DIR } from '../tools/memoryTool.js'; // Mock tool names if they are dynamically generated or complex vi.mock('../tools/ls', () => ({ LSTool: { Name: 'list_directory' } })); @@ -26,8 +30,15 @@ vi.mock('../tools/write-file', () => ({ vi.mock('../utils/gitUtils', () => ({ isGitRepository: vi.fn(), })); +vi.mock('node:fs'); describe('Core System Prompt (prompts.ts)', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.stubEnv('GEMINI_SYSTEM_MD', undefined); + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', undefined); + }); + it('should return the base prompt when no userMemory is provided', () => { vi.stubEnv('SANDBOX', undefined); const prompt = getCoreSystemPrompt(); @@ -105,4 +116,157 @@ describe('Core System Prompt (prompts.ts)', () => { expect(prompt).not.toContain('# Git Repository'); expect(prompt).toMatchSnapshot(); }); + + describe('GEMINI_SYSTEM_MD environment variable', () => { + it('should use default prompt when GEMINI_SYSTEM_MD is "false"', () => { + vi.stubEnv('GEMINI_SYSTEM_MD', 'false'); + const prompt = getCoreSystemPrompt(); + expect(fs.readFileSync).not.toHaveBeenCalled(); + expect(prompt).not.toContain('custom system prompt'); + }); + + it('should use default prompt when GEMINI_SYSTEM_MD is "0"', () => { + vi.stubEnv('GEMINI_SYSTEM_MD', '0'); + const prompt = getCoreSystemPrompt(); + expect(fs.readFileSync).not.toHaveBeenCalled(); + expect(prompt).not.toContain('custom system prompt'); + }); + + it('should throw error if GEMINI_SYSTEM_MD points to a non-existent file', () => { + const customPath = '/non/existent/path/system.md'; + vi.stubEnv('GEMINI_SYSTEM_MD', customPath); + vi.mocked(fs.existsSync).mockReturnValue(false); + expect(() => getCoreSystemPrompt()).toThrow( + `missing system prompt file '${path.resolve(customPath)}'`, + ); + }); + + it('should read from default path when GEMINI_SYSTEM_MD is "true"', () => { + const defaultPath = path.resolve( + path.join(GEMINI_CONFIG_DIR, 'system.md'), + ); + vi.stubEnv('GEMINI_SYSTEM_MD', 'true'); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt'); + + const prompt = getCoreSystemPrompt(); + expect(fs.readFileSync).toHaveBeenCalledWith(defaultPath, 'utf8'); + expect(prompt).toBe('custom system prompt'); + }); + + it('should read from default path when GEMINI_SYSTEM_MD is "1"', () => { + const defaultPath = path.resolve( + path.join(GEMINI_CONFIG_DIR, 'system.md'), + ); + vi.stubEnv('GEMINI_SYSTEM_MD', '1'); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt'); + + const prompt = getCoreSystemPrompt(); + expect(fs.readFileSync).toHaveBeenCalledWith(defaultPath, 'utf8'); + expect(prompt).toBe('custom system prompt'); + }); + + it('should read from custom path when GEMINI_SYSTEM_MD provides one, preserving case', () => { + const customPath = path.resolve('/custom/path/SyStEm.Md'); + vi.stubEnv('GEMINI_SYSTEM_MD', customPath); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt'); + + const prompt = getCoreSystemPrompt(); + expect(fs.readFileSync).toHaveBeenCalledWith(customPath, 'utf8'); + expect(prompt).toBe('custom system prompt'); + }); + + it('should expand tilde in custom path when GEMINI_SYSTEM_MD is set', () => { + const homeDir = '/Users/test'; + vi.spyOn(os, 'homedir').mockReturnValue(homeDir); + const customPath = '~/custom/system.md'; + const expectedPath = path.join(homeDir, 'custom/system.md'); + vi.stubEnv('GEMINI_SYSTEM_MD', customPath); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt'); + + const prompt = getCoreSystemPrompt(); + expect(fs.readFileSync).toHaveBeenCalledWith( + path.resolve(expectedPath), + 'utf8', + ); + expect(prompt).toBe('custom system prompt'); + }); + }); + + describe('GEMINI_WRITE_SYSTEM_MD environment variable', () => { + it('should not write to file when GEMINI_WRITE_SYSTEM_MD is "false"', () => { + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', 'false'); + getCoreSystemPrompt(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + it('should not write to file when GEMINI_WRITE_SYSTEM_MD is "0"', () => { + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', '0'); + getCoreSystemPrompt(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + it('should write to default path when GEMINI_WRITE_SYSTEM_MD is "true"', () => { + const defaultPath = path.resolve( + path.join(GEMINI_CONFIG_DIR, 'system.md'), + ); + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', 'true'); + getCoreSystemPrompt(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + defaultPath, + expect.any(String), + ); + }); + + it('should write to default path when GEMINI_WRITE_SYSTEM_MD is "1"', () => { + const defaultPath = path.resolve( + path.join(GEMINI_CONFIG_DIR, 'system.md'), + ); + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', '1'); + getCoreSystemPrompt(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + defaultPath, + expect.any(String), + ); + }); + + it('should write to custom path when GEMINI_WRITE_SYSTEM_MD provides one', () => { + const customPath = path.resolve('/custom/path/system.md'); + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', customPath); + getCoreSystemPrompt(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + customPath, + expect.any(String), + ); + }); + + it('should expand tilde in custom path when GEMINI_WRITE_SYSTEM_MD is set', () => { + const homeDir = '/Users/test'; + vi.spyOn(os, 'homedir').mockReturnValue(homeDir); + const customPath = '~/custom/system.md'; + const expectedPath = path.join(homeDir, 'custom/system.md'); + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', customPath); + getCoreSystemPrompt(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.resolve(expectedPath), + expect.any(String), + ); + }); + + it('should expand tilde in custom path when GEMINI_WRITE_SYSTEM_MD is just ~', () => { + const homeDir = '/Users/test'; + vi.spyOn(os, 'homedir').mockReturnValue(homeDir); + const customPath = '~'; + const expectedPath = homeDir; + vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', customPath); + getCoreSystemPrompt(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.resolve(expectedPath), + expect.any(String), + ); + }); + }); }); diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index 08df3ba2..b97264d7 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -6,6 +6,7 @@ import path from 'node:path'; import fs from 'node:fs'; +import os from 'node:os'; import { LSTool } from '../tools/ls.js'; import { EditTool } from '../tools/edit.js'; import { GlobTool } from '../tools/glob.js'; @@ -23,15 +24,24 @@ export function getCoreSystemPrompt(userMemory?: string): string { // default path is .gemini/system.md but can be modified via custom path in GEMINI_SYSTEM_MD let systemMdEnabled = false; let systemMdPath = path.resolve(path.join(GEMINI_CONFIG_DIR, 'system.md')); - const systemMdVar = process.env.GEMINI_SYSTEM_MD?.toLowerCase(); - if (systemMdVar && !['0', 'false'].includes(systemMdVar)) { - systemMdEnabled = true; // enable system prompt override - if (!['1', 'true'].includes(systemMdVar)) { - systemMdPath = path.resolve(systemMdVar); // use custom path from GEMINI_SYSTEM_MD - } - // require file to exist when override is enabled - if (!fs.existsSync(systemMdPath)) { - throw new Error(`missing system prompt file '${systemMdPath}'`); + const systemMdVar = process.env.GEMINI_SYSTEM_MD; + if (systemMdVar) { + const systemMdVarLower = systemMdVar.toLowerCase(); + if (!['0', 'false'].includes(systemMdVarLower)) { + systemMdEnabled = true; // enable system prompt override + if (!['1', 'true'].includes(systemMdVarLower)) { + let customPath = systemMdVar; + if (customPath.startsWith('~/')) { + customPath = path.join(os.homedir(), customPath.slice(2)); + } else if (customPath === '~') { + customPath = os.homedir(); + } + systemMdPath = path.resolve(customPath); // use custom path from GEMINI_SYSTEM_MD + } + // require file to exist when override is enabled + if (!fs.existsSync(systemMdPath)) { + throw new Error(`missing system prompt file '${systemMdPath}'`); + } } } const basePrompt = systemMdEnabled @@ -256,12 +266,24 @@ Your core function is efficient and safe assistance. Balance extreme conciseness `.trim(); // if GEMINI_WRITE_SYSTEM_MD is set (and not 0|false), write base system prompt to file - const writeSystemMdVar = process.env.GEMINI_WRITE_SYSTEM_MD?.toLowerCase(); - if (writeSystemMdVar && !['0', 'false'].includes(writeSystemMdVar)) { - if (['1', 'true'].includes(writeSystemMdVar)) { - fs.writeFileSync(systemMdPath, basePrompt); // write to default path, can be modified via GEMINI_SYSTEM_MD - } else { - fs.writeFileSync(path.resolve(writeSystemMdVar), basePrompt); // write to custom path from GEMINI_WRITE_SYSTEM_MD + const writeSystemMdVar = process.env.GEMINI_WRITE_SYSTEM_MD; + if (writeSystemMdVar) { + const writeSystemMdVarLower = writeSystemMdVar.toLowerCase(); + if (!['0', 'false'].includes(writeSystemMdVarLower)) { + if (['1', 'true'].includes(writeSystemMdVarLower)) { + fs.mkdirSync(path.dirname(systemMdPath), { recursive: true }); + fs.writeFileSync(systemMdPath, basePrompt); // write to default path, can be modified via GEMINI_SYSTEM_MD + } else { + let customPath = writeSystemMdVar; + if (customPath.startsWith('~/')) { + customPath = path.join(os.homedir(), customPath.slice(2)); + } else if (customPath === '~') { + customPath = os.homedir(); + } + const resolvedPath = path.resolve(customPath); + fs.mkdirSync(path.dirname(resolvedPath), { recursive: true }); + fs.writeFileSync(resolvedPath, basePrompt); // write to custom path from GEMINI_WRITE_SYSTEM_MD + } } }