mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-03 13:41:05 -07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c5594486c | |||
| 1a340be449 | |||
| b8fc6b6a57 | |||
| 40c7ec1157 | |||
| 84dfd8b277 | |||
| 5fcfd972ed | |||
| 71b4083e96 | |||
| 52f44516c5 | |||
| 9e2407993e | |||
| db724097b0 | |||
| 043a08807b |
@@ -57,7 +57,7 @@ they appear in the UI.
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
|
||||
| Hide Workspace Path | `ui.footer.hideCWD` | Hide the workspace path in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window usage percentage. | `true` |
|
||||
|
||||
@@ -90,8 +90,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Sub-commands:**
|
||||
- **`reload`**:
|
||||
- **Description:** Reload custom command definitions from all sources
|
||||
(user-level `~/.gemini/commands/`, project-level
|
||||
`<project>/.gemini/commands/`, MCP prompts, and extensions). Use this to
|
||||
(user-level `~/.gemini/commands/`, workspace-level
|
||||
`<workspace>/.gemini/commands/`, MCP prompts, and extensions). Use this to
|
||||
pick up new or modified `.toml` files without restarting the CLI.
|
||||
- **Usage:** `/commands reload`
|
||||
|
||||
@@ -115,7 +115,7 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- On macOS, it requires `pbcopy`, and on Windows, it requires `clip`. These
|
||||
tools are typically pre-installed on their respective systems.
|
||||
|
||||
### `/directory` (or `/dir`)
|
||||
### `/workspace` (or `/dir`, `/directory`)
|
||||
|
||||
- **Description:** Manage workspace directories for multi-directory support.
|
||||
- **Sub-commands:**
|
||||
@@ -123,13 +123,13 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Description:** Add a directory to the workspace. The path can be
|
||||
absolute or relative to the current working directory. Moreover, the
|
||||
reference from home directory is supported as well.
|
||||
- **Usage:** `/directory add <path1>,<path2>`
|
||||
- **Usage:** `/workspace add <path1>,<path2>`
|
||||
- **Note:** Disabled in restrictive sandbox profiles. If you're using that,
|
||||
use `--include-directories` when starting the session instead.
|
||||
- **`show`**:
|
||||
- **Description:** Display all directories added by `/directory add` and
|
||||
- **Description:** Display all directories added by `/workspace add` and
|
||||
`--include-directories`.
|
||||
- **Usage:** `/directory show`
|
||||
- **Usage:** `/workspace show`
|
||||
|
||||
### `/docs`
|
||||
|
||||
@@ -303,7 +303,7 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
### `/restore`
|
||||
|
||||
- **Description:** Restores the project files to the state they were in just
|
||||
- **Description:** Restores the workspace files to the state they were in just
|
||||
before a tool was executed. This is particularly useful for undoing file edits
|
||||
made by a tool. If run without a tool call ID, it will list available
|
||||
checkpoints to restore from.
|
||||
|
||||
@@ -13,7 +13,7 @@ overridden by higher numbers):
|
||||
2. **System defaults file:** System-wide default settings that can be
|
||||
overridden by other settings files.
|
||||
3. **User settings file:** Global settings for the current user.
|
||||
4. **Project settings file:** Project-specific settings.
|
||||
4. **Workspace settings file:** Workspace-specific settings.
|
||||
5. **System settings file:** System-wide settings that override all other
|
||||
settings files.
|
||||
6. **Environment variables:** System-wide or session-specific variables,
|
||||
@@ -43,10 +43,11 @@ locations for these files:
|
||||
- **Location:** `~/.gemini/settings.json` (where `~` is your home directory).
|
||||
- **Scope:** Applies to all Gemini CLI sessions for the current user. User
|
||||
settings override system defaults.
|
||||
- **Project settings file:**
|
||||
- **Location:** `.gemini/settings.json` within your project's root directory.
|
||||
- **Scope:** Applies only when running Gemini CLI from that specific project.
|
||||
Project settings override user settings and system defaults.
|
||||
- **Workspace settings file:**
|
||||
- **Location:** `.gemini/settings.json` within your workspace's root
|
||||
directory.
|
||||
- **Scope:** Applies only when running Gemini CLI from that specific
|
||||
workspace. Workspace settings override user settings and system defaults.
|
||||
- **System settings file:**
|
||||
- **Location:** `/etc/gemini-cli/settings.json` (Linux),
|
||||
`C:\ProgramData\gemini-cli\settings.json` (Windows) or
|
||||
@@ -70,10 +71,11 @@ this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own
|
||||
> CLI in a corporate environment, please see the
|
||||
> [Enterprise Configuration](../cli/enterprise.md) documentation.
|
||||
|
||||
### The `.gemini` directory in your project
|
||||
### The `.gemini` directory in your workspace
|
||||
|
||||
In addition to a project settings file, a project's `.gemini` directory can
|
||||
contain other project-specific files related to Gemini CLI's operation, such as:
|
||||
In addition to a workspace settings file, a workspace's `.gemini` directory can
|
||||
contain other workspace-specific files related to Gemini CLI's operation, such
|
||||
as:
|
||||
|
||||
- [Custom sandbox profiles](#sandboxing) (e.g.,
|
||||
`.gemini/sandbox-macos-custom.sb`, `.gemini/sandbox.Dockerfile`).
|
||||
@@ -261,7 +263,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.footer.hideCWD`** (boolean):
|
||||
- **Description:** Hide the current working directory in the footer.
|
||||
- **Description:** Hide the workspace path in the footer.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.footer.hideSandboxStatus`** (boolean):
|
||||
@@ -1303,12 +1305,12 @@ of v0.3.0:
|
||||
## Shell history
|
||||
|
||||
The CLI keeps a history of shell commands you run. To avoid conflicts between
|
||||
different projects, this history is stored in a project-specific directory
|
||||
different workspaces, this history is stored in a workspace-specific directory
|
||||
within your user's home folder.
|
||||
|
||||
- **Location:** `~/.gemini/tmp/<project_hash>/shell_history`
|
||||
- `<project_hash>` is a unique identifier generated from your project's root
|
||||
path.
|
||||
- **Location:** `~/.gemini/tmp/<workspace_hash>/shell_history`
|
||||
- `<workspace_hash>` is a unique identifier generated from your workspace's
|
||||
root path.
|
||||
- The history is stored in a file named `shell_history`.
|
||||
|
||||
## Environment variables and `.env` files
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('Hierarchical Memory', () => {
|
||||
},
|
||||
},
|
||||
// We simulate the hierarchical memory by including the tags in the prompt
|
||||
// since setting up real global/extension/project files in the eval rig is complex.
|
||||
// since setting up real global/extension/workspace files in the eval rig is complex.
|
||||
// The system prompt logic will append these tags when it finds them in userMemory.
|
||||
prompt: `
|
||||
<global_context>
|
||||
@@ -32,9 +32,9 @@ When asked for my favorite fruit, always say "Apple".
|
||||
When asked for my favorite fruit, always say "Banana".
|
||||
</extension_context>
|
||||
|
||||
<project_context>
|
||||
<workspace_context>
|
||||
When asked for my favorite fruit, always say "Cherry".
|
||||
</project_context>
|
||||
</workspace_context>
|
||||
|
||||
What is my favorite fruit? Tell me just the name of the fruit.`,
|
||||
assert: async (rig) => {
|
||||
@@ -65,23 +65,23 @@ Instruction A: Always be helpful.
|
||||
Instruction B: Use a professional tone.
|
||||
</extension_context>
|
||||
|
||||
<project_context>
|
||||
Instruction C: Adhere to the project's coding style.
|
||||
</project_context>
|
||||
<workspace_context>
|
||||
Instruction C: Adhere to the workspace's coding style.
|
||||
</workspace_context>
|
||||
|
||||
Which instruction came from the global context, which from the extension context, and which from the project context?
|
||||
Which instruction came from the global context, which from the extension context, and which from the workspace context?
|
||||
Provide the answer as an XML block like this:
|
||||
<results>
|
||||
<global>Instruction ...</global>
|
||||
<extension>Instruction ...</extension>
|
||||
<project>Instruction ...</project>
|
||||
<workspace>Instruction ...</workspace>
|
||||
</results>`,
|
||||
assert: async (rig) => {
|
||||
const stdout = rig._lastRunStdout!;
|
||||
assertModelHasOutput(stdout);
|
||||
expect(stdout).toMatch(/<global>.*Instruction A/i);
|
||||
expect(stdout).toMatch(/<extension>.*Instruction B/i);
|
||||
expect(stdout).toMatch(/<project>.*Instruction C/i);
|
||||
expect(stdout).toMatch(/<workspace>.*Instruction C/i);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -95,10 +95,10 @@ export function loadSettings(workspaceDir: string): Settings {
|
||||
// Load workspace settings
|
||||
try {
|
||||
if (fs.existsSync(workspaceSettingsPath)) {
|
||||
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
|
||||
const workspaceContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const parsedWorkspaceSettings = JSON.parse(
|
||||
stripJsonComments(projectContent),
|
||||
stripJsonComments(workspaceContent),
|
||||
) as Settings;
|
||||
workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
|
||||
}
|
||||
|
||||
@@ -1160,7 +1160,7 @@ export class Session {
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
this.debug(
|
||||
`Path ${pathName} is outside the project directory. Skipping.`,
|
||||
`Path ${pathName} is outside the workspace directory. Skipping.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('GeminiAgent Session Resume', () => {
|
||||
getChat: vi.fn().mockReturnValue({}),
|
||||
}),
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/workspace'),
|
||||
},
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
|
||||
@@ -41,7 +41,7 @@ async function addMcpServer(
|
||||
const settings = loadSettings(process.cwd());
|
||||
const inHome = settings.workspace.path === settings.user.path;
|
||||
|
||||
if (scope === 'project' && inHome) {
|
||||
if ((scope === 'workspace' || scope === 'project') && inHome) {
|
||||
debugLogger.error(
|
||||
'Error: Please use --scope user to edit settings in the home directory.',
|
||||
);
|
||||
@@ -159,10 +159,10 @@ export const addCommand: CommandModule = {
|
||||
})
|
||||
.option('scope', {
|
||||
alias: 's',
|
||||
describe: 'Configuration scope (user or project)',
|
||||
describe: 'Configuration scope (user or workspace)',
|
||||
type: 'string',
|
||||
default: 'project',
|
||||
choices: ['user', 'project'],
|
||||
default: 'workspace',
|
||||
choices: ['user', 'workspace', 'project'],
|
||||
})
|
||||
.option('transport', {
|
||||
alias: ['t', 'type'],
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('mcp remove command', () => {
|
||||
|
||||
expect(mockSetValue).not.toHaveBeenCalled();
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(
|
||||
'Server "non-existent-server" not found in project settings.',
|
||||
'Server "non-existent-server" not found in workspace settings.',
|
||||
);
|
||||
debugLogSpy.mockRestore();
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe('mcp remove command', () => {
|
||||
expect(updatedContent).not.toContain('"server-to-remove"');
|
||||
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(
|
||||
'Server "server-to-remove" removed from project settings.',
|
||||
'Server "server-to-remove" removed from workspace settings.',
|
||||
);
|
||||
|
||||
debugLogSpy.mockRestore();
|
||||
|
||||
@@ -49,10 +49,10 @@ export const removeCommand: CommandModule = {
|
||||
})
|
||||
.option('scope', {
|
||||
alias: 's',
|
||||
describe: 'Configuration scope (user or project)',
|
||||
describe: 'Configuration scope (user or workspace)',
|
||||
type: 'string',
|
||||
default: 'project',
|
||||
choices: ['user', 'project'],
|
||||
default: 'workspace',
|
||||
choices: ['user', 'workspace', 'project'],
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('validateAuthMethod', () => {
|
||||
'should return null for USE_VERTEX_AI if GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION are set',
|
||||
authType: AuthType.USE_VERTEX_AI,
|
||||
envs: {
|
||||
GOOGLE_CLOUD_PROJECT: 'test-project',
|
||||
GOOGLE_CLOUD_PROJECT: 'test-workspace',
|
||||
GOOGLE_CLOUD_LOCATION: 'test-location',
|
||||
},
|
||||
expected: null,
|
||||
|
||||
@@ -51,7 +51,7 @@ vi.mock('fs', async (importOriginal) => {
|
||||
const pathMod = await import('node:path');
|
||||
const mockHome = pathMod.resolve(pathMod.sep, 'mock', 'home', 'user');
|
||||
const MOCK_CWD1 = process.cwd();
|
||||
const MOCK_CWD2 = pathMod.resolve(pathMod.sep, 'home', 'user', 'project');
|
||||
const MOCK_CWD2 = pathMod.resolve(pathMod.sep, 'home', 'user', 'workspace');
|
||||
|
||||
const mockPaths = new Set([
|
||||
MOCK_CWD1,
|
||||
@@ -1890,7 +1890,7 @@ describe('loadCliConfig with includeDirectories', () => {
|
||||
);
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(
|
||||
path.resolve(path.sep, 'home', 'user', 'project'),
|
||||
path.resolve(path.sep, 'home', 'user', 'workspace'),
|
||||
);
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
@@ -1900,7 +1900,7 @@ describe('loadCliConfig with includeDirectories', () => {
|
||||
});
|
||||
|
||||
it.skip('should combine and resolve paths from settings and CLI arguments', async () => {
|
||||
const mockCwd = path.resolve(path.sep, 'home', 'user', 'project');
|
||||
const mockCwd = path.resolve(path.sep, 'home', 'user', 'workspace');
|
||||
process.argv = [
|
||||
'node',
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ export async function parseArguments(
|
||||
.option('list-sessions', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'List available sessions for the current project and exit.',
|
||||
'List available sessions for the current workspace and exit.',
|
||||
})
|
||||
.option('delete-session', {
|
||||
type: 'string',
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import '../../test-utils/customMatchers.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
|
||||
@@ -412,8 +412,8 @@ describe('Policy Engine Integration Tests', () => {
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
|
||||
'/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
|
||||
'C:\\Users\\user\\.gemini\\tmp\\project-id\\session-id\\plans\\plan.md',
|
||||
'D:\\gemini-cli\\.gemini\\tmp\\project-id\\session-1\\plans\\plan.md', // no session ID
|
||||
'C:\\Users\\user\\.gemini\\tmp\\workspace-id\\session-id\\plans\\plan.md',
|
||||
'D:\\gemini-cli\\.gemini\\tmp\\workspace-id\\session-1\\plans\\plan.md', // no session ID
|
||||
];
|
||||
|
||||
for (const file_path of validPaths) {
|
||||
@@ -437,7 +437,7 @@ describe('Policy Engine Integration Tests', () => {
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
const invalidPaths = [
|
||||
'/project/src/file.ts', // Workspace
|
||||
'/workspace/src/file.ts', // Workspace
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal (Unix)
|
||||
'C:\\Users\\user\\.gemini\\tmp\\id\\session\\plans\\..\\..\\..\\Windows\\System32\\config\\SAM', // Path traversal (Windows)
|
||||
|
||||
@@ -1666,7 +1666,7 @@ describe('Settings Loading and Merging', () => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should exclude DEBUG and DEBUG_MODE from project .env files by default', () => {
|
||||
it('should exclude DEBUG and DEBUG_MODE from workspace .env files by default', () => {
|
||||
// Create a workspace settings file with excludedProjectEnvVars
|
||||
const workspaceSettingsContent = {
|
||||
general: {},
|
||||
@@ -1686,18 +1686,18 @@ describe('Settings Loading and Merging', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// Mock findEnvFile to return a project .env file
|
||||
// Mock findEnvFile to return a workspace .env file
|
||||
const originalFindEnvFile = (
|
||||
loadSettings as unknown as { findEnvFile: () => string }
|
||||
).findEnvFile;
|
||||
(loadSettings as unknown as { findEnvFile: () => string }).findEnvFile =
|
||||
() => path.resolve('/mock/project/.env');
|
||||
() => '/mock/workspace/.env';
|
||||
|
||||
// Mock fs.readFileSync for .env file content
|
||||
const originalReadFileSync = fs.readFileSync;
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === path.resolve('/mock/project/.env')) {
|
||||
if (p === '/mock/workspace/.env') {
|
||||
return 'DEBUG=true\nDEBUG_MODE=1\nGEMINI_API_KEY=test-key';
|
||||
}
|
||||
if (
|
||||
@@ -3080,7 +3080,9 @@ MALICIOUS_VAR=allowed-because-trusted
|
||||
it('should sanitize value in sanitizeEnvVar helper', () => {
|
||||
expect(sanitizeEnvVar('$(calc)')).toBe('calc');
|
||||
expect(sanitizeEnvVar('`rm -rf /`')).toBe('rm-rf/');
|
||||
expect(sanitizeEnvVar('normal-project-123')).toBe('normal-project-123');
|
||||
expect(sanitizeEnvVar('normal-workspace-123')).toBe(
|
||||
'normal-workspace-123',
|
||||
);
|
||||
expect(sanitizeEnvVar('us-central1')).toBe('us-central1');
|
||||
});
|
||||
});
|
||||
@@ -3114,7 +3116,7 @@ MALICIOUS_VAR=allowed-because-trusted
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||
'GOOGLE_CLOUD_PROJECT=attacker-project;inject',
|
||||
'GOOGLE_CLOUD_PROJECT=attacker-workspace;inject',
|
||||
);
|
||||
|
||||
loadEnvironment(
|
||||
@@ -3123,7 +3125,7 @@ MALICIOUS_VAR=allowed-because-trusted
|
||||
);
|
||||
|
||||
expect(process.env['GOOGLE_CLOUD_PROJECT']).toBe(
|
||||
'attacker-projectinject',
|
||||
'attacker-workspaceinject',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -588,11 +588,11 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
hideCWD: {
|
||||
type: 'boolean',
|
||||
label: 'Hide CWD',
|
||||
label: 'Hide Workspace Path',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Hide the current working directory in the footer.',
|
||||
description: 'Hide the workspace path in the footer.',
|
||||
showInDialog: true,
|
||||
},
|
||||
hideSandboxStatus: {
|
||||
|
||||
@@ -272,30 +272,30 @@ describe('Trusted Folders', () => {
|
||||
};
|
||||
|
||||
it('should return true for a directly trusted folder', () => {
|
||||
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
|
||||
const config = { '/workspaceA': TrustLevel.TRUST_FOLDER };
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true for a child of a trusted folder', () => {
|
||||
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
|
||||
const config = { '/workspaceA': TrustLevel.TRUST_FOLDER };
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectA/src')).toEqual({
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceA/src')).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true for a child of a trusted parent folder', () => {
|
||||
const config = { '/projectB/somefile.txt': TrustLevel.TRUST_PARENT };
|
||||
const config = { '/workspaceB/somefile.txt': TrustLevel.TRUST_PARENT };
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectB')).toEqual({
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceB')).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
@@ -329,20 +329,22 @@ describe('Trusted Folders', () => {
|
||||
|
||||
it('should prioritize specific distrust over parent trust', () => {
|
||||
const config = {
|
||||
'/projectA': TrustLevel.TRUST_FOLDER,
|
||||
'/projectA/untrusted': TrustLevel.DO_NOT_TRUST,
|
||||
'/workspaceA': TrustLevel.TRUST_FOLDER,
|
||||
'/workspaceA/untrusted': TrustLevel.DO_NOT_TRUST,
|
||||
};
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectA/untrusted')).toEqual({
|
||||
isTrusted: false,
|
||||
source: 'file',
|
||||
});
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceA/untrusted')).toEqual(
|
||||
{
|
||||
isTrusted: false,
|
||||
source: 'file',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use workspaceDir instead of process.cwd() when provided', () => {
|
||||
const config = {
|
||||
'/projectA': TrustLevel.TRUST_FOLDER,
|
||||
'/workspaceA': TrustLevel.TRUST_FOLDER,
|
||||
'/untrusted': TrustLevel.DO_NOT_TRUST,
|
||||
};
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
@@ -350,18 +352,18 @@ describe('Trusted Folders', () => {
|
||||
vi.spyOn(process, 'cwd').mockImplementation(() => '/untrusted');
|
||||
|
||||
// process.cwd() is untrusted, but workspaceDir is trusted
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle path normalization', () => {
|
||||
const config = { '/home/user/projectA': TrustLevel.TRUST_FOLDER };
|
||||
const config = { '/home/user/workspaceA': TrustLevel.TRUST_FOLDER };
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
expect(
|
||||
isWorkspaceTrusted(mockSettings, '/home/user/../user/projectA'),
|
||||
isWorkspaceTrusted(mockSettings, '/home/user/../user/workspaceA'),
|
||||
).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
@@ -369,13 +371,13 @@ describe('Trusted Folders', () => {
|
||||
});
|
||||
|
||||
it('should prioritize IDE override over file config', () => {
|
||||
const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
|
||||
const config = { '/workspaceA': TrustLevel.DO_NOT_TRUST };
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
ideContextStore.set({ workspaceState: { isTrusted: true } });
|
||||
|
||||
try {
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'ide',
|
||||
});
|
||||
@@ -385,13 +387,13 @@ describe('Trusted Folders', () => {
|
||||
});
|
||||
|
||||
it('should return false when IDE override is false', () => {
|
||||
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
|
||||
const config = { '/workspaceA': TrustLevel.TRUST_FOLDER };
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
ideContextStore.set({ workspaceState: { isTrusted: false } });
|
||||
|
||||
try {
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
|
||||
isTrusted: false,
|
||||
source: 'ide',
|
||||
});
|
||||
@@ -442,10 +444,10 @@ describe('Trusted Folders', () => {
|
||||
const geminiCore = await import('@google/gemini-cli-core');
|
||||
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(false);
|
||||
|
||||
const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
|
||||
const config = { '/workspaceA': TrustLevel.DO_NOT_TRUST };
|
||||
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
|
||||
|
||||
expect(isWorkspaceTrusted(mockSettings, '/projectA').isTrusted).toBe(
|
||||
expect(isWorkspaceTrusted(mockSettings, '/workspaceA').isTrusted).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type MockInstance,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import {
|
||||
runDeferredCommand,
|
||||
defer,
|
||||
@@ -14,7 +21,6 @@ import {
|
||||
import { ExitCodes } from '@google/gemini-cli-core';
|
||||
import type { ArgumentsCamelCase, CommandModule } from 'yargs';
|
||||
import { createMockSettings } from './test-utils/settings.js';
|
||||
import type { MockInstance } from 'vitest';
|
||||
|
||||
const { mockRunExitCleanup, mockCoreEvents } = vi.hoisted(() => ({
|
||||
mockRunExitCleanup: vi.fn(),
|
||||
|
||||
@@ -1646,7 +1646,7 @@ describe('runNonInteractive', () => {
|
||||
startTime: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
firstUserMessage: 'Previous message',
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
},
|
||||
filePath: '/path/to/session.json',
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import { compressCommand } from '../ui/commands/compressCommand.js';
|
||||
import { copyCommand } from '../ui/commands/copyCommand.js';
|
||||
import { corgiCommand } from '../ui/commands/corgiCommand.js';
|
||||
import { docsCommand } from '../ui/commands/docsCommand.js';
|
||||
import { directoryCommand } from '../ui/commands/directoryCommand.js';
|
||||
import { workspaceCommand } from '../ui/commands/workspaceCommand.js';
|
||||
import { editorCommand } from '../ui/commands/editorCommand.js';
|
||||
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
|
||||
import { footerCommand } from '../ui/commands/footerCommand.js';
|
||||
@@ -131,7 +131,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
copyCommand,
|
||||
corgiCommand,
|
||||
docsCommand,
|
||||
directoryCommand,
|
||||
workspaceCommand,
|
||||
editorCommand,
|
||||
...(this.config?.getExtensionsEnabled() === false
|
||||
? [
|
||||
|
||||
@@ -10,7 +10,15 @@ import type { Config } from '@google/gemini-cli-core';
|
||||
import { GEMINI_DIR, Storage } from '@google/gemini-cli-core';
|
||||
import mock from 'mock-fs';
|
||||
import { FileCommandLoader } from './FileCommandLoader.js';
|
||||
import { assert, vi } from 'vitest';
|
||||
import {
|
||||
assert,
|
||||
vi,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
} from 'vitest';
|
||||
import { createMockCommandContext } from '../test-utils/mockCommandContext.js';
|
||||
import {
|
||||
SHELL_INJECTION_TRIGGER,
|
||||
@@ -230,7 +238,7 @@ describe('FileCommandLoader', () => {
|
||||
},
|
||||
});
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => '/path/to/project'),
|
||||
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getFolderTrust: vi.fn(() => false),
|
||||
isTrustedFolder: vi.fn(() => false),
|
||||
@@ -260,22 +268,22 @@ describe('FileCommandLoader', () => {
|
||||
expect(command.name).toBe('git:commit');
|
||||
});
|
||||
|
||||
it('returns both user and project commands in order', async () => {
|
||||
it('returns both user and workspace commands in order', async () => {
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
const projectCommandsDir = new Storage(
|
||||
const workspaceCommandsDir = new Storage(
|
||||
process.cwd(),
|
||||
).getProjectCommandsDir();
|
||||
mock({
|
||||
[userCommandsDir]: {
|
||||
'test.toml': 'prompt = "User prompt"',
|
||||
},
|
||||
[projectCommandsDir]: {
|
||||
[workspaceCommandsDir]: {
|
||||
'test.toml': 'prompt = "Project prompt"',
|
||||
},
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getWorkspaceRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getFolderTrust: vi.fn(() => false),
|
||||
isTrustedFolder: vi.fn(() => false),
|
||||
@@ -299,7 +307,7 @@ describe('FileCommandLoader', () => {
|
||||
} else {
|
||||
assert.fail('Incorrect action type for user command');
|
||||
}
|
||||
const projectResult = await commands[1].action?.(
|
||||
const workspaceResult = await commands[1].action?.(
|
||||
createMockCommandContext({
|
||||
invocation: {
|
||||
raw: '/test',
|
||||
@@ -309,10 +317,10 @@ describe('FileCommandLoader', () => {
|
||||
}),
|
||||
'',
|
||||
);
|
||||
if (projectResult?.type === 'submit_prompt') {
|
||||
expect(projectResult.content).toEqual([{ text: 'Project prompt' }]);
|
||||
if (workspaceResult?.type === 'submit_prompt') {
|
||||
expect(workspaceResult.content).toEqual([{ text: 'Project prompt' }]);
|
||||
} else {
|
||||
assert.fail('Incorrect action type for project command');
|
||||
assert.fail('Incorrect action type for workspace command');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -532,7 +540,7 @@ describe('FileCommandLoader', () => {
|
||||
describe('Extension Command Loading', () => {
|
||||
it('loads commands from active extensions', async () => {
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
const projectCommandsDir = new Storage(
|
||||
const workspaceCommandsDir = new Storage(
|
||||
process.cwd(),
|
||||
).getProjectCommandsDir();
|
||||
const extensionDir = path.join(
|
||||
@@ -546,8 +554,8 @@ describe('FileCommandLoader', () => {
|
||||
[userCommandsDir]: {
|
||||
'user.toml': 'prompt = "User command"',
|
||||
},
|
||||
[projectCommandsDir]: {
|
||||
'project.toml': 'prompt = "Project command"',
|
||||
[workspaceCommandsDir]: {
|
||||
'workspace.toml': 'prompt = "Project command"',
|
||||
},
|
||||
[extensionDir]: {
|
||||
'gemini-extension.json': JSON.stringify({
|
||||
@@ -561,7 +569,7 @@ describe('FileCommandLoader', () => {
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getWorkspaceRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
name: 'test-ext',
|
||||
@@ -578,7 +586,7 @@ describe('FileCommandLoader', () => {
|
||||
|
||||
expect(commands).toHaveLength(3);
|
||||
const commandNames = commands.map((cmd) => cmd.name);
|
||||
expect(commandNames).toEqual(['user', 'project', 'ext']);
|
||||
expect(commandNames).toEqual(['user', 'workspace', 'ext']);
|
||||
|
||||
const extCommand = commands.find((cmd) => cmd.name === 'ext');
|
||||
expect(extCommand?.extensionName).toBe('test-ext');
|
||||
@@ -587,7 +595,7 @@ describe('FileCommandLoader', () => {
|
||||
|
||||
it('extension commands have extensionName metadata for conflict resolution', async () => {
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
const projectCommandsDir = new Storage(
|
||||
const workspaceCommandsDir = new Storage(
|
||||
process.cwd(),
|
||||
).getProjectCommandsDir();
|
||||
const extensionDir = path.join(
|
||||
@@ -610,13 +618,13 @@ describe('FileCommandLoader', () => {
|
||||
[userCommandsDir]: {
|
||||
'deploy.toml': 'prompt = "User deploy command"',
|
||||
},
|
||||
[projectCommandsDir]: {
|
||||
[workspaceCommandsDir]: {
|
||||
'deploy.toml': 'prompt = "Project deploy command"',
|
||||
},
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getWorkspaceRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
name: 'test-ext',
|
||||
@@ -723,7 +731,7 @@ describe('FileCommandLoader', () => {
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getWorkspaceRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
name: 'active-ext',
|
||||
@@ -769,7 +777,7 @@ describe('FileCommandLoader', () => {
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getWorkspaceRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
name: 'no-commands',
|
||||
@@ -813,7 +821,7 @@ describe('FileCommandLoader', () => {
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getWorkspaceRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => [
|
||||
{ name: 'a', version: '1.0.0', isActive: true, path: extensionDir },
|
||||
]),
|
||||
@@ -874,7 +882,7 @@ describe('FileCommandLoader', () => {
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getWorkspaceRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
name: 'my-test-ext',
|
||||
@@ -1256,7 +1264,7 @@ describe('FileCommandLoader', () => {
|
||||
describe('with folder trust enabled', () => {
|
||||
it('loads multiple commands', async () => {
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => '/path/to/project'),
|
||||
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
isTrustedFolder: vi.fn(() => true),
|
||||
@@ -1277,7 +1285,7 @@ describe('FileCommandLoader', () => {
|
||||
|
||||
it('does not load when folder is not trusted', async () => {
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => '/path/to/project'),
|
||||
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
isTrustedFolder: vi.fn(() => false),
|
||||
@@ -1307,7 +1315,7 @@ describe('FileCommandLoader', () => {
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => '/path/to/project'),
|
||||
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getFolderTrust: vi.fn(() => false),
|
||||
isTrustedFolder: vi.fn(() => false),
|
||||
|
||||
@@ -56,7 +56,7 @@ const TomlCommandDefSchema = z.object({
|
||||
|
||||
/**
|
||||
* Discovers and loads custom slash commands from .toml files in both the
|
||||
* user's global config directory and the current project's directory.
|
||||
* user's global config directory and the current workspace's directory.
|
||||
*
|
||||
* This loader is responsible for:
|
||||
* - Recursively scanning command directories.
|
||||
@@ -65,22 +65,22 @@ const TomlCommandDefSchema = z.object({
|
||||
* - Handling file system errors and malformed files gracefully.
|
||||
*/
|
||||
export class FileCommandLoader implements ICommandLoader {
|
||||
private readonly projectRoot: string;
|
||||
private readonly workspaceRoot: string;
|
||||
private readonly folderTrustEnabled: boolean;
|
||||
private readonly isTrustedFolder: boolean;
|
||||
|
||||
constructor(private readonly config: Config | null) {
|
||||
this.folderTrustEnabled = !!config?.getFolderTrust();
|
||||
this.isTrustedFolder = !!config?.isTrustedFolder();
|
||||
this.projectRoot = config?.getProjectRoot() || process.cwd();
|
||||
this.workspaceRoot = config?.getWorkspaceRoot() || process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all commands from user, project, and extension directories.
|
||||
* Returns commands in order: user → project → extensions (alphabetically).
|
||||
* Loads all commands from user, workspace, and extension directories.
|
||||
* Returns commands in order: user → workspace → extensions (alphabetically).
|
||||
*
|
||||
* Order is important for conflict resolution in CommandService:
|
||||
* - User/project commands (without extensionName) use "last wins" strategy
|
||||
* - User/workspace commands (without extensionName) use "last wins" strategy
|
||||
* - Extension commands (with extensionName) get renamed if conflicts exist
|
||||
*
|
||||
* @param signal An AbortSignal to cancel the loading process.
|
||||
@@ -150,7 +150,7 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
private getCommandDirectories(): CommandDirectory[] {
|
||||
const dirs: CommandDirectory[] = [];
|
||||
|
||||
const storage = this.config?.storage ?? new Storage(this.projectRoot);
|
||||
const storage = this.config?.storage ?? new Storage(this.workspaceRoot);
|
||||
|
||||
// 1. User commands
|
||||
dirs.push({
|
||||
@@ -158,9 +158,9 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
kind: CommandKind.USER_FILE,
|
||||
});
|
||||
|
||||
// 2. Project commands
|
||||
// 2. Workspace commands (override user commands)
|
||||
dirs.push({
|
||||
path: storage.getProjectCommandsDir(),
|
||||
path: storage.getWorkspaceCommandsDir(),
|
||||
kind: CommandKind.WORKSPACE_FILE,
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('createMockCommandContext', () => {
|
||||
it('should apply deeply nested overrides correctly', () => {
|
||||
// This is the most important test for factory's logic.
|
||||
const mockConfig = {
|
||||
getProjectRoot: () => '/test/project',
|
||||
getWorkspaceRoot: () => '/test/workspace',
|
||||
getModel: () => 'gemini-pro',
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('createMockCommandContext', () => {
|
||||
|
||||
expect(context.services.config).toBeDefined();
|
||||
expect(context.services.config?.getModel()).toBe('gemini-pro');
|
||||
expect(context.services.config?.getProjectRoot()).toBe('/test/project');
|
||||
expect(context.services.config?.getWorkspaceRoot()).toBe('/test/workspace');
|
||||
|
||||
// Verify a default property on the same nested object is still there
|
||||
expect(context.services.logger).toBeDefined();
|
||||
|
||||
@@ -21,7 +21,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
isInitialized: vi.fn(() => true),
|
||||
setTerminalBackground: vi.fn(),
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
|
||||
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
getDebugMode: vi.fn(() => false),
|
||||
|
||||
@@ -1107,7 +1107,7 @@ describe('AppContainer State Management', () => {
|
||||
const mockResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-session-123',
|
||||
projectHash: 'test-project-hash',
|
||||
workspaceHash: 'test-workspace-hash',
|
||||
startTime: '2024-01-01T00:00:00Z',
|
||||
lastUpdated: '2024-01-01T00:00:01Z',
|
||||
messages: [
|
||||
@@ -1296,7 +1296,7 @@ describe('AppContainer State Management', () => {
|
||||
const resumedData = {
|
||||
conversation: {
|
||||
sessionId: 'resumed-session-456',
|
||||
projectHash: 'project-hash',
|
||||
workspaceHash: 'workspace-hash',
|
||||
startTime: '2024-01-01T00:00:00Z',
|
||||
lastUpdated: '2024-01-01T00:01:00Z',
|
||||
messages: [
|
||||
@@ -1352,7 +1352,7 @@ describe('AppContainer State Management', () => {
|
||||
const resumedData = {
|
||||
conversation: {
|
||||
sessionId: 'test-session',
|
||||
projectHash: 'project-hash',
|
||||
workspaceHash: 'workspace-hash',
|
||||
startTime: '2024-01-01T00:00:00Z',
|
||||
lastUpdated: '2024-01-01T00:01:00Z',
|
||||
messages: [],
|
||||
|
||||
@@ -218,7 +218,7 @@ export function AuthDialog({
|
||||
</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.primary}>
|
||||
How would you like to authenticate for this project?
|
||||
How would you like to authenticate for this workspace?
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`AuthDialog > Snapshots > renders correctly with auth error 1`] = `
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ How would you like to authenticate for this workspace? │
|
||||
│ │
|
||||
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
|
||||
│ │
|
||||
@@ -26,7 +26,7 @@ exports[`AuthDialog > Snapshots > renders correctly with default props 1`] = `
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ How would you like to authenticate for this workspace? │
|
||||
│ │
|
||||
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
|
||||
│ │
|
||||
@@ -45,7 +45,7 @@ exports[`AuthDialog > Snapshots > renders correctly with enforced auth type 1`]
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ How would you like to authenticate for this workspace? │
|
||||
│ │
|
||||
│ (selected) Use Gemini API Key │
|
||||
│ │
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
|
||||
import { clearCommand } from './clearCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
|
||||
import { copyCommand } from './copyCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { MockInstance } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type MockInstance,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { ideCommand } from './ideCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { IDE_DEFINITIONS } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('initCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Empty GEMINI.md created. Now analyzing the project to populate it.',
|
||||
text: 'Empty GEMINI.md created. Now analyzing the workspace to populate it.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { performInit } from '@google/gemini-cli-core';
|
||||
|
||||
export const initCommand: SlashCommand = {
|
||||
name: 'init',
|
||||
description: 'Analyzes the project and creates a tailored GEMINI.md file',
|
||||
description: 'Analyzes the workspace and creates a tailored GEMINI.md file',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
@@ -42,7 +42,7 @@ export const initCommand: SlashCommand = {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Empty GEMINI.md created. Now analyzing the project to populate it.',
|
||||
text: 'Empty GEMINI.md created. Now analyzing the workspace to populate it.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
|
||||
import { memoryCommand } from './memoryCommand.js';
|
||||
import type { SlashCommand, CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('restoreCommand', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should restore a tool call and project state', async () => {
|
||||
it('should restore a tool call and workspace state', async () => {
|
||||
const toolCallData = {
|
||||
history: [{ type: 'user', text: 'do a thing', id: 123 }],
|
||||
clientHistory: [{ role: 'user', parts: [{ text: 'do a thing' }] }],
|
||||
@@ -181,7 +181,7 @@ describe('restoreCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Restored project to the state before the tool call.',
|
||||
text: 'Restored workspace to the state before the tool call.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { vi } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it, type vi } from 'vitest';
|
||||
import { toolsCommand } from './toolsCommand.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
+14
-7
@@ -4,9 +4,16 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
import { directoryCommand } from './directoryCommand.js';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { workspaceCommand } from './workspaceCommand.js';
|
||||
import {
|
||||
expandHomeDir,
|
||||
getDirectorySuggestions,
|
||||
@@ -38,14 +45,14 @@ vi.mock('../utils/directoryUtils.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
describe('directoryCommand', () => {
|
||||
describe('workspaceCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
let mockConfig: Config;
|
||||
let mockWorkspaceContext: WorkspaceContext;
|
||||
const addCommand = directoryCommand.subCommands?.find(
|
||||
const addCommand = workspaceCommand.subCommands?.find(
|
||||
(c) => c.name === 'add',
|
||||
);
|
||||
const showCommand = directoryCommand.subCommands?.find(
|
||||
const showCommand = workspaceCommand.subCommands?.find(
|
||||
(c) => c.name === 'show',
|
||||
);
|
||||
|
||||
@@ -126,7 +133,7 @@ describe('directoryCommand', () => {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
|
||||
'The /workspace add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -77,9 +77,9 @@ async function finishAddingDirectories(
|
||||
}
|
||||
}
|
||||
|
||||
export const directoryCommand: SlashCommand = {
|
||||
name: 'directory',
|
||||
altNames: ['dir'],
|
||||
export const workspaceCommand: SlashCommand = {
|
||||
name: 'workspace',
|
||||
altNames: ['directory', 'dir'],
|
||||
description: 'Manage workspace directories',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
@@ -156,7 +156,7 @@ export const directoryCommand: SlashCommand = {
|
||||
type: 'message' as const,
|
||||
messageType: 'error' as const,
|
||||
content:
|
||||
'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
|
||||
'The /workspace add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { AnsiOutputText } from './AnsiOutput.js';
|
||||
import type { AnsiOutput, AnsiToken } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -296,17 +296,17 @@ describe('FolderTrustDialog', () => {
|
||||
|
||||
describe('directory display', () => {
|
||||
it('should correctly display the folder name for a nested directory', async () => {
|
||||
mockedCwd.mockReturnValue('/home/user/project');
|
||||
mockedCwd.mockReturnValue('/home/user/workspace');
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Trust folder (project)');
|
||||
expect(lastFrame()).toContain('Trust folder (workspace)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should correctly display the parent folder name for a nested directory', async () => {
|
||||
mockedCwd.mockReturnValue('/home/user/project');
|
||||
mockedCwd.mockReturnValue('/home/user/workspace');
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import React, { act } from 'react';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { Text } from 'ink';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { StreamingContext } from '../contexts/StreamingContext.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { vi } from 'vitest';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
|
||||
// Mock GeminiRespondingSpinner
|
||||
|
||||
@@ -72,7 +72,8 @@ export const NewAgentsNotification = ({
|
||||
New Agents Discovered
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
The following agents were found in this project. Please review them:
|
||||
The following agents were found in this workspace. Please review
|
||||
them:
|
||||
</Text>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { RewindViewer } from './RewindViewer.js';
|
||||
@@ -60,7 +60,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
|
||||
const createConversation = (messages: MessageRecord[]): ConversationRecord => ({
|
||||
sessionId: 'test-session',
|
||||
projectHash: 'hash',
|
||||
workspaceHash: 'hash',
|
||||
startTime: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
messages,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
@@ -22,7 +27,8 @@
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import '../../test-utils/customMatchers.js';
|
||||
import { SettingsDialog } from './SettingsDialog.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import '../../test-utils/customMatchers.js';
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Table } from './Table.js';
|
||||
import { Text } from 'ink';
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`NewAgentsNotification > renders agent list 1`] = `
|
||||
" ╭────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ New Agents Discovered │
|
||||
│ The following agents were found in this project. Please review them: │
|
||||
│ The following agents were found in this workspace. Please review them: │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
@@ -24,7 +24,7 @@ exports[`NewAgentsNotification > truncates list if more than 5 agents 1`] = `
|
||||
" ╭────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ New Agents Discovered │
|
||||
│ The following agents were found in this project. Please review them: │
|
||||
│ The following agents were found in this workspace. Please review them: │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import * as CodeColorizer from '../../utils/CodeColorizer.js';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('<OverflowProvider><DiffRenderer /></OverflowProvider>', () => {
|
||||
const mockColorizeCode = vi.spyOn(CodeColorizer, 'colorizeCode');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { GeminiMessage } from './GeminiMessage.js';
|
||||
import { StreamingState } from '../../types.js';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import type {
|
||||
SerializableConfirmationDetails,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { ToolMessage, type ToolMessageProps } from './ToolMessage.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { StreamingState } from '../../types.js';
|
||||
import { Text } from 'ink';
|
||||
import {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import '../../../test-utils/customMatchers.js';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { ExpandableText, MAX_WIDTH } from './ExpandableText.js';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import '../../../test-utils/customMatchers.js';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { act } from 'react';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import '../../../test-utils/customMatchers.js';
|
||||
import { handleVimAction } from './vim-buffer-actions.js';
|
||||
import type { TextBufferState, VisualLayout } from './text-buffer.js';
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const INFORMATIVE_TIPS = [
|
||||
'Hide the startup banner for a cleaner launch (/settings)…',
|
||||
'Hide the context summary above the input (/settings)…',
|
||||
'Reclaim vertical space by hiding the footer (/settings)…',
|
||||
'Hide individual footer elements like CWD or sandbox status (/settings)…',
|
||||
'Hide individual footer elements like Workspace Path or sandbox status (/settings)…',
|
||||
'Hide the context window percentage in the footer (/settings)…',
|
||||
'Show memory usage for performance monitoring (/settings)…',
|
||||
'Show line numbers in the chat for easier reference (/settings)…',
|
||||
@@ -33,7 +33,7 @@ export const INFORMATIVE_TIPS = [
|
||||
'Control when chat history gets compressed based on context compression threshold (settings.json)…',
|
||||
'Define custom context file names, like CONTEXT.md (settings.json)…',
|
||||
'Set max directories to scan for context files (/settings)…',
|
||||
'Expand your workspace with additional directories (/directory)…',
|
||||
'Expand your workspace with additional directories (/workspace)…',
|
||||
'Control how /memory reload loads context files (/settings)…',
|
||||
'Toggle respect for .gitignore files in context (/settings)…',
|
||||
'Toggle respect for .geminiignore files in context (/settings)…',
|
||||
@@ -131,15 +131,15 @@ export const INFORMATIVE_TIPS = [
|
||||
'Save tokens by summarizing the context with /compress…',
|
||||
'Copy the last response to your clipboard with /copy…',
|
||||
'Open the full documentation in your browser with /docs…',
|
||||
'Add directories to your workspace with /directory add <path>…',
|
||||
'Show all directories in your workspace with /directory show…',
|
||||
'Use /dir as a shortcut for /directory…',
|
||||
'Add directories to your workspace with /workspace add <path>…',
|
||||
'Show all directories in your workspace with /workspace show…',
|
||||
'Use /dir or /directory as a shortcut for /workspace…',
|
||||
'Set your preferred external editor with /editor…',
|
||||
'List all active extensions with /extensions list…',
|
||||
'Update all or specific extensions with /extensions update…',
|
||||
'Get help on commands with /help…',
|
||||
'Manage IDE integration with /ide…',
|
||||
'Create a project-specific GEMINI.md file with /init…',
|
||||
'Create a workspace-specific GEMINI.md file with /init…',
|
||||
'List configured MCP servers and tools with /mcp list…',
|
||||
'Authenticate with an OAuth-enabled MCP server with /mcp auth…',
|
||||
'Reload MCP servers with /mcp reload…',
|
||||
@@ -149,7 +149,7 @@ export const INFORMATIVE_TIPS = [
|
||||
'List the paths of the GEMINI.md files in use with /memory list…',
|
||||
'Choose your Gemini model with /model…',
|
||||
'Display the privacy notice with /privacy…',
|
||||
'Restore project files to a previous state with /restore…',
|
||||
'Restore workspace files to a previous state with /restore…',
|
||||
'Exit the CLI with /quit or /exit…',
|
||||
'Check model-specific usage stats with /stats model…',
|
||||
'Check tool-specific usage stats with /stats tools…',
|
||||
|
||||
@@ -7,10 +7,19 @@
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import type React from 'react';
|
||||
import { act } from 'react';
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi, afterAll, beforeAll } from 'vitest';
|
||||
import type { Key } from './KeypressContext.js';
|
||||
import {
|
||||
KeypressProvider,
|
||||
|
||||
@@ -4,10 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { act } from 'react';
|
||||
import { MouseProvider, useMouseContext, useMouse } from './MouseContext.js';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
import type React from 'react';
|
||||
import { useStdin } from 'ink';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { handleAtCommand } from './atCommandProcessor.js';
|
||||
import type { Config, DiscoveredMCPResource } from '@google/gemini-cli-core';
|
||||
import {
|
||||
@@ -108,7 +115,7 @@ describe('handleAtCommand', () => {
|
||||
|
||||
const workspaceDirs = this.getWorkspaceContext().getDirectories();
|
||||
const projectTempDir = this.storage.getProjectTempDir();
|
||||
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
|
||||
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the workspace temp directory: ${projectTempDir}`;
|
||||
},
|
||||
getMcpServers: () => ({}),
|
||||
getMcpServerCommand: () => undefined,
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('handleAtCommand with Agents', () => {
|
||||
|
||||
const workspaceDirs = this.getWorkspaceContext().getDirectories();
|
||||
const projectTempDir = this.storage.getProjectTempDir();
|
||||
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
|
||||
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the workspace temp directory: ${projectTempDir}`;
|
||||
},
|
||||
getMcpServers: () => ({}),
|
||||
getMcpServerCommand: () => undefined,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { act, useCallback } from 'react';
|
||||
import { vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useConsoleMessages } from './useConsoleMessages.js';
|
||||
import { CoreEvent, type ConsoleLogPayload } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
import { useFlickerDetector } from './useFlickerDetector.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { recordFlickerFrame } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -4,10 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { useFocus } from './useFocus.js';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
import { useStdin, useStdout } from 'ink';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { act } from 'react';
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('useFolderTrust', () => {
|
||||
renderHook(() => useFolderTrust(mockSettings, onTrustChange, addItem));
|
||||
expect(addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
text: 'This folder is untrusted, project settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
|
||||
text: 'This folder is untrusted, workspace settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
|
||||
type: 'info',
|
||||
},
|
||||
expect.any(Number),
|
||||
|
||||
@@ -59,7 +59,7 @@ export const useFolderTrust = (
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'This folder is untrusted, project settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
|
||||
text: 'This folder is untrusted, workspace settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -5,8 +5,15 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Mock, MockInstance } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
type MockInstance,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHookWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { MockedFunction } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type MockedFunction,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { useIncludeDirsTrust } from './useIncludeDirsTrust.js';
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
*/
|
||||
|
||||
import { act } from 'react';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { useStdin } from 'ink';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
// Mock the 'ink' module to control stdin
|
||||
vi.mock('ink', async (importOriginal) => {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { vi } from 'vitest';
|
||||
import {
|
||||
useMemoryMonitor,
|
||||
MEMORY_CHECK_INTERVAL,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useMouse } from './useMouse.js';
|
||||
import { MouseProvider, useMouseContext } from '../contexts/MouseContext.js';
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('useRewindLogic', () => {
|
||||
|
||||
const mockConversation: ConversationRecord = {
|
||||
sessionId: 'conv-1',
|
||||
projectHash: 'hash-1',
|
||||
workspaceHash: 'hash-1',
|
||||
startTime: new Date(1000).toISOString(),
|
||||
lastUpdated: new Date(1001).toISOString(),
|
||||
messages: [mockUserMessage, mockModelMessage],
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('useSessionResume', () => {
|
||||
const resumedData: ResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [] as MessageRecord[],
|
||||
@@ -133,7 +133,7 @@ describe('useSessionResume', () => {
|
||||
const resumedData: ResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [] as MessageRecord[],
|
||||
@@ -160,7 +160,7 @@ describe('useSessionResume', () => {
|
||||
const resumedData: ResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [] as MessageRecord[],
|
||||
@@ -200,7 +200,7 @@ describe('useSessionResume', () => {
|
||||
const resumedData: ResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [] as MessageRecord[],
|
||||
@@ -240,7 +240,7 @@ describe('useSessionResume', () => {
|
||||
const resumedData: ResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [] as MessageRecord[],
|
||||
@@ -306,7 +306,7 @@ describe('useSessionResume', () => {
|
||||
it('should not resume when user is authenticating', () => {
|
||||
const conversation: ConversationRecord = {
|
||||
sessionId: 'auto-resume-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [
|
||||
@@ -338,7 +338,7 @@ describe('useSessionResume', () => {
|
||||
it('should not resume when Gemini client is not initialized', () => {
|
||||
const conversation: ConversationRecord = {
|
||||
sessionId: 'auto-resume-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [
|
||||
@@ -370,7 +370,7 @@ describe('useSessionResume', () => {
|
||||
it('should automatically resume session when resumedSessionData is provided', async () => {
|
||||
const conversation: ConversationRecord = {
|
||||
sessionId: 'auto-resume-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [
|
||||
@@ -425,7 +425,7 @@ describe('useSessionResume', () => {
|
||||
it('should only resume once even if props change', async () => {
|
||||
const conversation: ConversationRecord = {
|
||||
sessionId: 'auto-resume-123',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [
|
||||
@@ -480,7 +480,7 @@ describe('useSessionResume', () => {
|
||||
it('should convert session messages correctly during auto-resume', async () => {
|
||||
const conversation: ConversationRecord = {
|
||||
sessionId: 'auto-resume-with-tools',
|
||||
projectHash: 'project-123',
|
||||
workspaceHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import type React from 'react';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import '../../test-utils/customMatchers.js';
|
||||
import { TableRenderer } from './TableRenderer.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import '../../test-utils/customMatchers.js';
|
||||
import { getToolGroupBorderAppearance } from './borderStyles.js';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
@@ -52,7 +52,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
warn: vi.fn(),
|
||||
},
|
||||
Storage: class {
|
||||
getProjectTempDir = vi.fn(() => '/tmp/global');
|
||||
getWorkspaceTempDir = vi.fn(() => '/tmp/global');
|
||||
initialize = vi.fn(() => Promise.resolve(undefined));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -245,22 +245,22 @@ const saveFileWithXclip = async (tempFilePath: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the directory where clipboard images should be stored for a specific project.
|
||||
* Gets the directory where clipboard images should be stored for a specific workspace.
|
||||
*
|
||||
* This uses the global temporary directory but creates a project-specific subdirectory
|
||||
* based on the hash of the project path (via `Storage.getProjectTempDir()`).
|
||||
* This prevents path conflicts between different projects while keeping the images
|
||||
* outside of the user's project directory.
|
||||
* This uses the global temporary directory but creates a workspace-specific subdirectory
|
||||
* based on the hash of the workspace path (via `Storage.getProjectTempDir()`).
|
||||
* This prevents path conflicts between different workspaces while keeping the images
|
||||
* outside of the user's workspace directory.
|
||||
*
|
||||
* @param targetDir The root directory of the current project.
|
||||
* @param targetDir The root directory of the current workspace.
|
||||
* @returns The absolute path to the images directory.
|
||||
*/
|
||||
async function getProjectClipboardImagesDir(
|
||||
async function getWorkspaceClipboardImagesDir(
|
||||
targetDir: string,
|
||||
): Promise<string> {
|
||||
const storage = new Storage(targetDir);
|
||||
await storage.initialize();
|
||||
const baseDir = storage.getProjectTempDir();
|
||||
const baseDir = storage.getWorkspaceTempDir();
|
||||
return path.join(baseDir, 'images');
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export async function saveClipboardImage(
|
||||
targetDir: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const tempDir = await getProjectClipboardImagesDir(targetDir);
|
||||
const tempDir = await getWorkspaceClipboardImagesDir(targetDir);
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Generate a unique filename with timestamp
|
||||
@@ -398,7 +398,7 @@ export async function cleanupOldClipboardImages(
|
||||
targetDir: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const tempDir = await getProjectClipboardImagesDir(targetDir);
|
||||
const tempDir = await getWorkspaceClipboardImagesDir(targetDir);
|
||||
const files = await fs.readdir(tempDir);
|
||||
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
...actual,
|
||||
spawnAsync: vi.fn(),
|
||||
Storage: class {
|
||||
getProjectTempDir = vi.fn(() => "C:\\User's Files");
|
||||
getWorkspaceTempDir = vi.fn(() => "C:\\User's Files");
|
||||
initialize = vi.fn(() => Promise.resolve(undefined));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import clipboardy from 'clipboardy';
|
||||
import {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { couldBeSGRMouseSequence, SGR_MOUSE_REGEX, ESC } from './input.js';
|
||||
|
||||
describe('input utils', () => {
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
/// <reference types="vitest/globals" />
|
||||
|
||||
import type { MockInstance } from 'vitest';
|
||||
import { vi } from 'vitest';
|
||||
import { describe, it, expect, vi, type MockInstance } from 'vitest';
|
||||
import { TextOutput } from './textOutput.js';
|
||||
|
||||
describe('TextOutput', () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('agentUtils', () => {
|
||||
],
|
||||
};
|
||||
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
|
||||
'Agent "my-agent" enabled by setting it to enabled in [user:/path/to/user/settings] and [project:/path/to/workspace/settings] settings.',
|
||||
'Agent "my-agent" enabled by setting it to enabled in [user:/path/to/user/settings] and [workspace:/path/to/workspace/settings] settings.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('agentUtils', () => {
|
||||
],
|
||||
};
|
||||
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
|
||||
'Agent "my-agent" is now disabled in both [user:/path/to/user/settings] and [project:/path/to/workspace/settings] settings.',
|
||||
'Agent "my-agent" is now disabled in both [user:/path/to/user/settings] and [workspace:/path/to/workspace/settings] settings.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ export function renderAgentActionFeedback(
|
||||
|
||||
const formatScopeItem = (s: { scope: SettingScope; path: string }) => {
|
||||
const label =
|
||||
s.scope === SettingScope.Workspace ? 'project' : s.scope.toLowerCase();
|
||||
s.scope === SettingScope.Workspace ? 'workspace' : s.scope.toLowerCase();
|
||||
return formatScope(label, s.path);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as path from 'node:path';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
Storage: vi.fn().mockImplementation(() => ({
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/workspace'),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
shutdownTelemetry: vi.fn(),
|
||||
@@ -112,7 +112,7 @@ describe('cleanup', () => {
|
||||
it('should remove checkpoints directory', async () => {
|
||||
await cleanupCheckpoints();
|
||||
expect(fs.rm).toHaveBeenCalledWith(
|
||||
path.join('/tmp/project', 'checkpoints'),
|
||||
path.join('/tmp/workspace', 'checkpoints'),
|
||||
{
|
||||
recursive: true,
|
||||
force: true,
|
||||
|
||||
@@ -165,7 +165,7 @@ export function setupTtyCheck(): () => void {
|
||||
export async function cleanupCheckpoints() {
|
||||
const storage = new Storage(process.cwd());
|
||||
await storage.initialize();
|
||||
const tempDir = storage.getProjectTempDir();
|
||||
const tempDir = storage.getWorkspaceTempDir();
|
||||
const checkpointsDir = join(tempDir, 'checkpoints');
|
||||
try {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true });
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { getInstallationInfo, PackageManager } from './installationInfo.js';
|
||||
import { updateEventEmitter } from './updateEventEmitter.js';
|
||||
import type { UpdateObject } from '../ui/utils/updateCheck.js';
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function waitForUpdateCompletion(
|
||||
export function handleAutoUpdate(
|
||||
info: UpdateObject | null,
|
||||
settings: LoadedSettings,
|
||||
projectRoot: string,
|
||||
workspaceRoot: string,
|
||||
spawnFn: typeof spawn = spawnWrapper,
|
||||
) {
|
||||
if (!info) {
|
||||
@@ -83,7 +83,7 @@ export function handleAutoUpdate(
|
||||
}
|
||||
|
||||
const installationInfo = getInstallationInfo(
|
||||
projectRoot,
|
||||
workspaceRoot,
|
||||
settings.merged.general.enableAutoUpdate,
|
||||
);
|
||||
|
||||
|
||||
@@ -43,14 +43,14 @@ const mockedExistsSync = vi.mocked(fs.existsSync);
|
||||
const mockedExecSync = vi.mocked(childProcess.execSync);
|
||||
|
||||
describe('getInstallationInfo', () => {
|
||||
const projectRoot = '/path/to/project';
|
||||
const workspaceRoot = '/path/to/workspace';
|
||||
let originalArgv: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
originalArgv = [...process.argv];
|
||||
// Mock process.cwd() for isGitRepository
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(projectRoot);
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(workspaceRoot);
|
||||
vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('getInstallationInfo', () => {
|
||||
|
||||
it('should return UNKNOWN when cliPath is not available', () => {
|
||||
process.argv[1] = '';
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
|
||||
});
|
||||
|
||||
@@ -71,20 +71,20 @@ describe('getInstallationInfo', () => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should detect running from a local git clone', () => {
|
||||
process.argv[1] = `${projectRoot}/packages/cli/dist/index.js`;
|
||||
process.argv[1] = `${workspaceRoot}/packages/cli/dist/index.js`;
|
||||
mockedRealPathSync.mockReturnValue(
|
||||
`${projectRoot}/packages/cli/dist/index.js`,
|
||||
`${workspaceRoot}/packages/cli/dist/index.js`,
|
||||
);
|
||||
mockedIsGitRepository.mockReturnValue(true);
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
@@ -98,7 +98,7 @@ describe('getInstallationInfo', () => {
|
||||
process.argv[1] = npxPath;
|
||||
mockedRealPathSync.mockReturnValue(npxPath);
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.NPX);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
@@ -110,7 +110,7 @@ describe('getInstallationInfo', () => {
|
||||
process.argv[1] = pnpxPath;
|
||||
mockedRealPathSync.mockReturnValue(pnpxPath);
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.PNPX);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
@@ -125,7 +125,7 @@ describe('getInstallationInfo', () => {
|
||||
throw new Error('Command failed');
|
||||
});
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.BUNX);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
@@ -155,7 +155,7 @@ describe('getInstallationInfo', () => {
|
||||
return String(p);
|
||||
});
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(mockedExecSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('brew --prefix gemini-cli'),
|
||||
@@ -179,7 +179,7 @@ describe('getInstallationInfo', () => {
|
||||
throw new Error('Command failed');
|
||||
});
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(mockedExecSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('brew --prefix gemini-cli'),
|
||||
@@ -199,14 +199,14 @@ describe('getInstallationInfo', () => {
|
||||
});
|
||||
|
||||
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
expect(info.packageManager).toBe(PackageManager.PNPM);
|
||||
expect(info.isGlobal).toBe(true);
|
||||
expect(info.updateCommand).toBe('pnpm add -g @google/gemini-cli@latest');
|
||||
expect(info.updateMessage).toContain('Attempting to automatically update');
|
||||
|
||||
// isAutoUpdateEnabled = false -> "Please run..."
|
||||
const infoDisabled = getInstallationInfo(projectRoot, false);
|
||||
const infoDisabled = getInstallationInfo(workspaceRoot, false);
|
||||
expect(infoDisabled.updateMessage).toContain('Please run pnpm add');
|
||||
});
|
||||
|
||||
@@ -219,7 +219,7 @@ describe('getInstallationInfo', () => {
|
||||
});
|
||||
|
||||
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
expect(info.packageManager).toBe(PackageManager.YARN);
|
||||
expect(info.isGlobal).toBe(true);
|
||||
expect(info.updateCommand).toBe(
|
||||
@@ -228,7 +228,7 @@ describe('getInstallationInfo', () => {
|
||||
expect(info.updateMessage).toContain('Attempting to automatically update');
|
||||
|
||||
// isAutoUpdateEnabled = false -> "Please run..."
|
||||
const infoDisabled = getInstallationInfo(projectRoot, false);
|
||||
const infoDisabled = getInstallationInfo(workspaceRoot, false);
|
||||
expect(infoDisabled.updateMessage).toContain('Please run yarn global add');
|
||||
});
|
||||
|
||||
@@ -241,29 +241,29 @@ describe('getInstallationInfo', () => {
|
||||
});
|
||||
|
||||
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
expect(info.packageManager).toBe(PackageManager.BUN);
|
||||
expect(info.isGlobal).toBe(true);
|
||||
expect(info.updateCommand).toBe('bun add -g @google/gemini-cli@latest');
|
||||
expect(info.updateMessage).toContain('Attempting to automatically update');
|
||||
|
||||
// isAutoUpdateEnabled = false -> "Please run..."
|
||||
const infoDisabled = getInstallationInfo(projectRoot, false);
|
||||
const infoDisabled = getInstallationInfo(workspaceRoot, false);
|
||||
expect(infoDisabled.updateMessage).toContain('Please run bun add');
|
||||
});
|
||||
|
||||
it('should detect local installation and identify yarn from lockfile', () => {
|
||||
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
|
||||
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
|
||||
process.argv[1] = localPath;
|
||||
mockedRealPathSync.mockReturnValue(localPath);
|
||||
mockedExecSync.mockImplementation(() => {
|
||||
throw new Error('Command failed');
|
||||
});
|
||||
mockedExistsSync.mockImplementation(
|
||||
(p) => p === path.join(projectRoot, 'yarn.lock'),
|
||||
(p) => p === path.join(workspaceRoot, 'yarn.lock'),
|
||||
);
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.YARN);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
@@ -271,41 +271,41 @@ describe('getInstallationInfo', () => {
|
||||
});
|
||||
|
||||
it('should detect local installation and identify pnpm from lockfile', () => {
|
||||
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
|
||||
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
|
||||
process.argv[1] = localPath;
|
||||
mockedRealPathSync.mockReturnValue(localPath);
|
||||
mockedExecSync.mockImplementation(() => {
|
||||
throw new Error('Command failed');
|
||||
});
|
||||
mockedExistsSync.mockImplementation(
|
||||
(p) => p === path.join(projectRoot, 'pnpm-lock.yaml'),
|
||||
(p) => p === path.join(workspaceRoot, 'pnpm-lock.yaml'),
|
||||
);
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.PNPM);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect local installation and identify bun from lockfile', () => {
|
||||
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
|
||||
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
|
||||
process.argv[1] = localPath;
|
||||
mockedRealPathSync.mockReturnValue(localPath);
|
||||
mockedExecSync.mockImplementation(() => {
|
||||
throw new Error('Command failed');
|
||||
});
|
||||
mockedExistsSync.mockImplementation(
|
||||
(p) => p === path.join(projectRoot, 'bun.lockb'),
|
||||
(p) => p === path.join(workspaceRoot, 'bun.lockb'),
|
||||
);
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.BUN);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
});
|
||||
|
||||
it('should default to local npm installation if no lockfile is found', () => {
|
||||
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
|
||||
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
|
||||
process.argv[1] = localPath;
|
||||
mockedRealPathSync.mockReturnValue(localPath);
|
||||
mockedExecSync.mockImplementation(() => {
|
||||
@@ -313,7 +313,7 @@ describe('getInstallationInfo', () => {
|
||||
});
|
||||
mockedExistsSync.mockReturnValue(false); // No lockfiles
|
||||
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
|
||||
expect(info.packageManager).toBe(PackageManager.NPM);
|
||||
expect(info.isGlobal).toBe(false);
|
||||
@@ -328,14 +328,14 @@ describe('getInstallationInfo', () => {
|
||||
});
|
||||
|
||||
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
|
||||
const info = getInstallationInfo(projectRoot, true);
|
||||
const info = getInstallationInfo(workspaceRoot, true);
|
||||
expect(info.packageManager).toBe(PackageManager.NPM);
|
||||
expect(info.isGlobal).toBe(true);
|
||||
expect(info.updateCommand).toBe('npm install -g @google/gemini-cli@latest');
|
||||
expect(info.updateMessage).toContain('Attempting to automatically update');
|
||||
|
||||
// isAutoUpdateEnabled = false -> "Please run..."
|
||||
const infoDisabled = getInstallationInfo(projectRoot, false);
|
||||
const infoDisabled = getInstallationInfo(workspaceRoot, false);
|
||||
expect(infoDisabled.updateMessage).toContain('Please run npm install');
|
||||
});
|
||||
|
||||
@@ -368,7 +368,7 @@ describe('getInstallationInfo', () => {
|
||||
return String(p);
|
||||
});
|
||||
|
||||
const info = getInstallationInfo(projectRoot, false);
|
||||
const info = getInstallationInfo(workspaceRoot, false);
|
||||
|
||||
expect(info.packageManager).not.toBe(PackageManager.HOMEBREW);
|
||||
expect(info.packageManager).toBe(PackageManager.NPM);
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface InstallationInfo {
|
||||
}
|
||||
|
||||
export function getInstallationInfo(
|
||||
projectRoot: string,
|
||||
workspaceRoot: string,
|
||||
isAutoUpdateEnabled: boolean,
|
||||
): InstallationInfo {
|
||||
const cliPath = process.argv[1];
|
||||
@@ -43,7 +43,7 @@ export function getInstallationInfo(
|
||||
try {
|
||||
// Normalize path separators to forward slashes for consistent matching.
|
||||
const realPath = fs.realpathSync(cliPath).replace(/\\/g, '/');
|
||||
const normalizedProjectRoot = projectRoot?.replace(/\\/g, '/');
|
||||
const normalizedProjectRoot = workspaceRoot?.replace(/\\/g, '/');
|
||||
const isGit = isGitRepository(process.cwd());
|
||||
|
||||
// Check for local git clone first
|
||||
@@ -160,11 +160,11 @@ export function getInstallationInfo(
|
||||
realPath.startsWith(`${normalizedProjectRoot}/node_modules`)
|
||||
) {
|
||||
let pm = PackageManager.NPM;
|
||||
if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
|
||||
if (fs.existsSync(path.join(workspaceRoot, 'yarn.lock'))) {
|
||||
pm = PackageManager.YARN;
|
||||
} else if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
|
||||
} else if (fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'))) {
|
||||
pm = PackageManager.PNPM;
|
||||
} else if (fs.existsSync(path.join(projectRoot, 'bun.lockb'))) {
|
||||
} else if (fs.existsSync(path.join(workspaceRoot, 'bun.lockb'))) {
|
||||
pm = PackageManager.BUN;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
RELAUNCH_EXIT_CODE,
|
||||
relaunchApp,
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('sandboxUtils', () => {
|
||||
});
|
||||
|
||||
it('should handle registry path', () => {
|
||||
expect(parseImageName('gcr.io/my-project/my-image:v1')).toBe(
|
||||
expect(parseImageName('gcr.io/my-workspace/my-image:v1')).toBe(
|
||||
'my-image-v1',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
function createTestConfig(): Config {
|
||||
return {
|
||||
storage: {
|
||||
getProjectTempDir: () => '/tmp/nonexistent-test-dir',
|
||||
getWorkspaceTempDir: () => '/tmp/nonexistent-test-dir',
|
||||
},
|
||||
getSessionId: () => 'test-session-id',
|
||||
getDebugMode: () => false,
|
||||
@@ -93,7 +93,7 @@ describe('Session Cleanup Integration', () => {
|
||||
);
|
||||
|
||||
const config = createTestConfig();
|
||||
config.storage.getProjectTempDir = vi.fn().mockReturnValue(tempDir);
|
||||
config.storage.getWorkspaceTempDir = vi.fn().mockReturnValue(tempDir);
|
||||
|
||||
const settings: Settings = {};
|
||||
|
||||
@@ -209,7 +209,7 @@ describe('Session Cleanup Integration', () => {
|
||||
// Configure test with real temp directory
|
||||
const config: Config = {
|
||||
storage: {
|
||||
getProjectTempDir: () => tempDir,
|
||||
getWorkspaceTempDir: () => tempDir,
|
||||
},
|
||||
getSessionId: () => 'current123',
|
||||
getDebugMode: () => false,
|
||||
|
||||
@@ -28,8 +28,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
Storage: class MockStorage {
|
||||
getProjectTempDir() {
|
||||
return '/tmp/test-project';
|
||||
getWorkspaceTempDir() {
|
||||
return '/tmp/test-workspace';
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -42,7 +42,7 @@ const mockGetAllSessionFiles = vi.mocked(getAllSessionFiles);
|
||||
function createMockConfig(overrides: Partial<Config> = {}): Config {
|
||||
return {
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
|
||||
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
|
||||
},
|
||||
getSessionId: vi.fn().mockReturnValue('current123'),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
@@ -245,7 +245,7 @@ describe('Session Cleanup', () => {
|
||||
// Verify that unlink was never called with the current session file
|
||||
const unlinkCalls = mockFs.unlink.mock.calls;
|
||||
const currentSessionPath = path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}2025-01-20T10-30-00-current12.json`,
|
||||
);
|
||||
@@ -525,21 +525,21 @@ describe('Session Cleanup', () => {
|
||||
const unlinkCalls = mockFs.unlink.mock.calls.map((call) => call[0]);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}8d.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}15d.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).not.toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}5d.json`,
|
||||
),
|
||||
@@ -724,21 +724,21 @@ describe('Session Cleanup', () => {
|
||||
const unlinkCalls = mockFs.unlink.mock.calls.map((call) => call[0]);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}3d.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}4d.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}5d.json`,
|
||||
),
|
||||
@@ -747,21 +747,21 @@ describe('Session Cleanup', () => {
|
||||
// Verify which files were NOT deleted
|
||||
expect(unlinkCalls).not.toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}current.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).not.toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}1d.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).not.toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}2d.json`,
|
||||
),
|
||||
@@ -883,21 +883,21 @@ describe('Session Cleanup', () => {
|
||||
const unlinkCalls = mockFs.unlink.mock.calls.map((call) => call[0]);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}5d.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}7d.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}12d.json`,
|
||||
),
|
||||
@@ -906,14 +906,14 @@ describe('Session Cleanup', () => {
|
||||
// Verify which files were NOT deleted
|
||||
expect(unlinkCalls).not.toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}current.json`,
|
||||
),
|
||||
);
|
||||
expect(unlinkCalls).not.toContain(
|
||||
path.join(
|
||||
'/tmp/test-project',
|
||||
'/tmp/test-workspace',
|
||||
'chats',
|
||||
`${SESSION_FILE_PREFIX}3d.json`,
|
||||
),
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function cleanupExpiredSessions(
|
||||
}
|
||||
|
||||
const retentionConfig = settings.general.sessionRetention;
|
||||
const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
|
||||
const chatsDir = path.join(config.storage.getWorkspaceTempDir(), 'chats');
|
||||
|
||||
// Validate retention configuration
|
||||
const validationErrorMessage = validateRetentionConfig(
|
||||
@@ -95,7 +95,10 @@ export async function cleanupExpiredSessions(
|
||||
// ALSO cleanup Activity logs in the project logs directory
|
||||
const sessionId = sessionToDelete.sessionInfo?.id;
|
||||
if (sessionId) {
|
||||
const logsDir = path.join(config.storage.getProjectTempDir(), 'logs');
|
||||
const logsDir = path.join(
|
||||
config.storage.getWorkspaceTempDir(),
|
||||
'logs',
|
||||
);
|
||||
const logPath = path.join(logsDir, `session-${sessionId}.jsonl`);
|
||||
try {
|
||||
await fs.unlink(logPath);
|
||||
@@ -106,7 +109,7 @@ export async function cleanupExpiredSessions(
|
||||
// ALSO cleanup tool outputs for this session
|
||||
const safeSessionId = sanitizeFilenamePart(sessionId);
|
||||
const toolOutputDir = path.join(
|
||||
config.storage.getProjectTempDir(),
|
||||
config.storage.getWorkspaceTempDir(),
|
||||
TOOL_OUTPUTS_DIR,
|
||||
`session-${safeSessionId}`,
|
||||
);
|
||||
@@ -382,7 +385,7 @@ export async function cleanupToolOutputFiles(
|
||||
if (!tempDir) {
|
||||
const storage = new Storage(process.cwd());
|
||||
await storage.initialize();
|
||||
tempDir = storage.getProjectTempDir();
|
||||
tempDir = storage.getWorkspaceTempDir();
|
||||
}
|
||||
const toolOutputDir = path.join(tempDir, TOOL_OUTPUTS_DIR);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('SessionSelector', () => {
|
||||
// Mock config
|
||||
config = {
|
||||
storage: {
|
||||
getProjectTempDir: () => tmpDir,
|
||||
getWorkspaceTempDir: () => tmpDir,
|
||||
},
|
||||
getSessionId: () => 'current-session-id',
|
||||
} as Partial<Config> as Config;
|
||||
@@ -55,7 +55,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const session1 = {
|
||||
sessionId: sessionId1,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -70,7 +70,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const session2 = {
|
||||
sessionId: sessionId2,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T11:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T11:30:00.000Z',
|
||||
messages: [
|
||||
@@ -121,7 +121,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const session1 = {
|
||||
sessionId: sessionId1,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -136,7 +136,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const session2 = {
|
||||
sessionId: sessionId2,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T11:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T11:30:00.000Z',
|
||||
messages: [
|
||||
@@ -185,7 +185,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const session1 = {
|
||||
sessionId: sessionId1,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -200,7 +200,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const session2 = {
|
||||
sessionId: sessionId2,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T11:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T11:30:00.000Z',
|
||||
messages: [
|
||||
@@ -245,7 +245,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const sessionOriginal = {
|
||||
sessionId,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -260,7 +260,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const sessionDuplicate = {
|
||||
sessionId,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T11:00:00.000Z', // Newer
|
||||
messages: [
|
||||
@@ -309,7 +309,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const session1 = {
|
||||
sessionId: sessionId1,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -348,7 +348,7 @@ describe('SessionSelector', () => {
|
||||
|
||||
const emptyConfig = {
|
||||
storage: {
|
||||
getProjectTempDir: () => tmpDir,
|
||||
getWorkspaceTempDir: () => tmpDir,
|
||||
},
|
||||
getSessionId: () => 'current-session-id',
|
||||
} as Partial<Config> as Config;
|
||||
@@ -375,7 +375,7 @@ describe('SessionSelector', () => {
|
||||
// Session with user message - should be listed
|
||||
const sessionWithUser = {
|
||||
sessionId: sessionIdWithUser,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -391,7 +391,7 @@ describe('SessionSelector', () => {
|
||||
// Session with only system messages - should NOT be listed
|
||||
const sessionSystemOnly = {
|
||||
sessionId: sessionIdSystemOnly,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T11:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T11:30:00.000Z',
|
||||
messages: [
|
||||
@@ -444,7 +444,7 @@ describe('SessionSelector', () => {
|
||||
// Session with only gemini message - should be listed
|
||||
const sessionGeminiOnly = {
|
||||
sessionId: sessionIdGeminiOnly,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -484,7 +484,7 @@ describe('SessionSelector', () => {
|
||||
// Main session - should be listed
|
||||
const mainSession = {
|
||||
sessionId: mainSessionId,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T10:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T10:30:00.000Z',
|
||||
messages: [
|
||||
@@ -501,7 +501,7 @@ describe('SessionSelector', () => {
|
||||
// Subagent session - should NOT be listed
|
||||
const subagentSession = {
|
||||
sessionId: subagentSessionId,
|
||||
projectHash: 'test-hash',
|
||||
workspaceHash: 'test-hash',
|
||||
startTime: '2024-01-01T11:00:00.000Z',
|
||||
lastUpdated: '2024-01-01T11:30:00.000Z',
|
||||
messages: [
|
||||
|
||||
@@ -50,7 +50,7 @@ export class SessionError extends Error {
|
||||
static noSessionsFound(): SessionError {
|
||||
return new SessionError(
|
||||
'NO_SESSIONS_FOUND',
|
||||
'No previous sessions found for this project.',
|
||||
'No previous sessions found for this workspace.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ export class SessionSelector {
|
||||
*/
|
||||
async listSessions(): Promise<SessionInfo[]> {
|
||||
const chatsDir = path.join(
|
||||
this.config.storage.getProjectTempDir(),
|
||||
this.config.storage.getWorkspaceTempDir(),
|
||||
'chats',
|
||||
);
|
||||
return getSessionFiles(chatsDir, this.config.getSessionId());
|
||||
@@ -498,7 +498,7 @@ export class SessionSelector {
|
||||
sessionInfo: SessionInfo,
|
||||
): Promise<SessionSelectionResult> {
|
||||
const chatsDir = path.join(
|
||||
this.config.storage.getProjectTempDir(),
|
||||
this.config.storage.getWorkspaceTempDir(),
|
||||
'chats',
|
||||
);
|
||||
const sessionPath = path.join(chatsDir, sessionInfo.fileName);
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('listSessions', () => {
|
||||
// Create mock config
|
||||
mockConfig = {
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
|
||||
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
|
||||
},
|
||||
getSessionId: vi.fn().mockReturnValue('current-session-id'),
|
||||
} as unknown as Config;
|
||||
@@ -73,7 +73,7 @@ describe('listSessions', () => {
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'No previous sessions found for this project.',
|
||||
'No previous sessions found for this workspace.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('listSessions', () => {
|
||||
|
||||
// Check that the header was displayed
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'\nAvailable sessions for this project (3):\n',
|
||||
'\nAvailable sessions for this workspace (3):\n',
|
||||
);
|
||||
|
||||
// Check that each session was logged
|
||||
@@ -286,7 +286,7 @@ describe('listSessions', () => {
|
||||
|
||||
// Assert
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
'\nAvailable sessions for this project (1):\n',
|
||||
'\nAvailable sessions for this workspace (1):\n',
|
||||
);
|
||||
expect(mocks.writeToStdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1. Only session'),
|
||||
@@ -340,7 +340,7 @@ describe('deleteSession', () => {
|
||||
// Create mock config
|
||||
mockConfig = {
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
|
||||
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
|
||||
},
|
||||
getSessionId: vi.fn().mockReturnValue('current-session-id'),
|
||||
} as unknown as Config;
|
||||
@@ -380,7 +380,7 @@ describe('deleteSession', () => {
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(mocks.writeToStderr).toHaveBeenCalledWith(
|
||||
'No sessions found for this project.',
|
||||
'No sessions found for this workspace.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -25,12 +25,12 @@ export async function listSessions(config: Config): Promise<void> {
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
writeToStdout('No previous sessions found for this project.');
|
||||
writeToStdout('No previous sessions found for this workspace.');
|
||||
return;
|
||||
}
|
||||
|
||||
writeToStdout(
|
||||
`\nAvailable sessions for this project (${sessions.length}):\n`,
|
||||
`\nAvailable sessions for this workspace (${sessions.length}):\n`,
|
||||
);
|
||||
|
||||
sessions
|
||||
@@ -59,7 +59,7 @@ export async function deleteSession(
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
writeToStderr('No sessions found for this project.');
|
||||
writeToStderr('No sessions found for this workspace.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { installSkill, linkSkill } from './skillUtils.js';
|
||||
|
||||
describe('skillUtils', () => {
|
||||
let tempDir: string;
|
||||
const projectRoot = path.resolve(__dirname, '../../../../../');
|
||||
const workspaceRoot = path.resolve(__dirname, '../../../../../');
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-utils-test-'));
|
||||
@@ -130,7 +130,7 @@ describe('skillUtils', () => {
|
||||
});
|
||||
|
||||
it('should successfully install from a .skill file', async () => {
|
||||
const skillPath = path.join(projectRoot, 'weather-skill.skill');
|
||||
const skillPath = path.join(workspaceRoot, 'weather-skill.skill');
|
||||
|
||||
// Ensure the file exists
|
||||
const exists = await fs.stat(skillPath).catch(() => null);
|
||||
|
||||
@@ -82,10 +82,10 @@ describe('getUserStartupWarnings', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return a warning when running in a project directory', async () => {
|
||||
const projectDir = path.join(testRootDir, 'project');
|
||||
await fs.mkdir(projectDir);
|
||||
const warnings = await getUserStartupWarnings({}, projectDir);
|
||||
it('should not return a warning when running in a workspace directory', async () => {
|
||||
const workspaceDir = path.join(testRootDir, 'workspace');
|
||||
await fs.mkdir(workspaceDir);
|
||||
const warnings = await getUserStartupWarnings({}, workspaceDir);
|
||||
expect(warnings.find((w) => w.id === 'home-directory')).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -123,9 +123,9 @@ describe('getUserStartupWarnings', () => {
|
||||
});
|
||||
|
||||
it('should not return a warning when running in a non-root directory', async () => {
|
||||
const projectDir = path.join(testRootDir, 'project');
|
||||
await fs.mkdir(projectDir);
|
||||
const warnings = await getUserStartupWarnings({}, projectDir);
|
||||
const workspaceDir = path.join(testRootDir, 'workspace');
|
||||
await fs.mkdir(workspaceDir);
|
||||
const warnings = await getUserStartupWarnings({}, workspaceDir);
|
||||
expect(warnings.find((w) => w.id === 'root-directory')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -151,10 +151,10 @@ describe('getUserStartupWarnings', () => {
|
||||
priority: WarningPriority.High,
|
||||
};
|
||||
vi.mocked(getCompatibilityWarnings).mockReturnValue([compWarning]);
|
||||
const projectDir = path.join(testRootDir, 'project');
|
||||
await fs.mkdir(projectDir);
|
||||
const workspaceDir = path.join(testRootDir, 'workspace');
|
||||
await fs.mkdir(workspaceDir);
|
||||
|
||||
const warnings = await getUserStartupWarnings({}, projectDir);
|
||||
const warnings = await getUserStartupWarnings({}, workspaceDir);
|
||||
expect(warnings).toContainEqual(compWarning);
|
||||
});
|
||||
|
||||
@@ -165,12 +165,12 @@ describe('getUserStartupWarnings', () => {
|
||||
priority: WarningPriority.High,
|
||||
};
|
||||
vi.mocked(getCompatibilityWarnings).mockReturnValue([compWarning]);
|
||||
const projectDir = path.join(testRootDir, 'project');
|
||||
await fs.mkdir(projectDir);
|
||||
const workspaceDir = path.join(testRootDir, 'workspace');
|
||||
await fs.mkdir(workspaceDir);
|
||||
|
||||
const warnings = await getUserStartupWarnings(
|
||||
{ ui: { showCompatibilityWarnings: false } },
|
||||
projectDir,
|
||||
workspaceDir,
|
||||
);
|
||||
expect(warnings).not.toContainEqual(compWarning);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user