mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
fix(cli): reset plan session state on /clear (#25515)
This commit is contained in:
committed by
GitHub
parent
2b6dab6136
commit
9600da2c8f
@@ -1774,6 +1774,95 @@ describe('Server Config (config.ts)', () => {
|
||||
expect(config1.topicState.getTopic()).toBe('Topic 1');
|
||||
expect(config2.topicState.getTopic()).toBe('Topic 2');
|
||||
});
|
||||
|
||||
it('updates storage session-scoped directories when the sessionId changes', async () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
sessionId: 'session-one',
|
||||
plan: true,
|
||||
});
|
||||
|
||||
await config.initialize();
|
||||
const tempDir = config.storage.getProjectTempDir();
|
||||
const oldPlansDir = path.join(tempDir, 'session-one', 'plans');
|
||||
const oldTrackerService = config.getTrackerService();
|
||||
|
||||
config.setSessionId('session-two');
|
||||
|
||||
expect(config.getSessionId()).toBe('session-two');
|
||||
expect(config.storage.getProjectTempPlansDir()).toBe(
|
||||
path.join(tempDir, 'session-two', 'plans'),
|
||||
);
|
||||
expect(config.storage.getProjectTempTrackerDir()).toBe(
|
||||
path.join(tempDir, 'session-two', 'tracker'),
|
||||
);
|
||||
expect(config.getTrackerService()).not.toBe(oldTrackerService);
|
||||
expect(config.getTrackerService().trackerDir).toBe(
|
||||
path.join(tempDir, 'session-two', 'tracker'),
|
||||
);
|
||||
expect(config.getWorkspaceContext().getDirectories()).not.toContain(
|
||||
oldPlansDir,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw when changing sessions before the previous plans dir exists', async () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
sessionId: 'session-one',
|
||||
plan: true,
|
||||
});
|
||||
|
||||
await config.initialize();
|
||||
const missingPlansDir = config.storage.getProjectTempPlansDir();
|
||||
const realpathMock = vi.mocked(fs.realpathSync);
|
||||
const originalImplementation = realpathMock.getMockImplementation();
|
||||
|
||||
try {
|
||||
realpathMock.mockImplementation((input) => {
|
||||
const normalizedInput =
|
||||
typeof input === 'string' || Buffer.isBuffer(input)
|
||||
? input
|
||||
: input.toString();
|
||||
|
||||
if (normalizedInput === missingPlansDir) {
|
||||
const error = new Error(
|
||||
`ENOENT: no such file or directory, ${normalizedInput}`,
|
||||
);
|
||||
Object.assign(error, { code: 'ENOENT' });
|
||||
throw error;
|
||||
}
|
||||
if (originalImplementation) {
|
||||
return originalImplementation(input);
|
||||
}
|
||||
return normalizedInput;
|
||||
});
|
||||
|
||||
expect(() => config.setSessionId('session-two')).not.toThrow();
|
||||
} finally {
|
||||
realpathMock.mockImplementation((input) => {
|
||||
if (originalImplementation) {
|
||||
return originalImplementation(input);
|
||||
}
|
||||
return typeof input === 'string' || Buffer.isBuffer(input)
|
||||
? input
|
||||
: input.toString();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('clears the approved plan when starting a new session', () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
sessionId: 'session-one',
|
||||
});
|
||||
|
||||
config.setApprovedPlanPath('/tmp/session-one/plans/approved.md');
|
||||
|
||||
expect(() => config.resetNewSessionState('session-two')).not.toThrow();
|
||||
|
||||
expect(config.getSessionId()).toBe('session-two');
|
||||
expect(config.getApprovedPlanPath()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GemmaModelRouterSettings', () => {
|
||||
|
||||
@@ -1762,7 +1762,22 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
|
||||
setSessionId(sessionId: string): void {
|
||||
const previousPlansDir = this.storage.isInitialized()
|
||||
? this.storage.getPlansDir()
|
||||
: undefined;
|
||||
|
||||
this._sessionId = sessionId;
|
||||
this.storage.setSessionId(sessionId);
|
||||
this.trackerService = undefined;
|
||||
|
||||
if (previousPlansDir) {
|
||||
this.refreshSessionScopedPlansDirectory(previousPlansDir);
|
||||
}
|
||||
}
|
||||
|
||||
resetNewSessionState(sessionId: string): void {
|
||||
this.setSessionId(sessionId);
|
||||
this.approvedPlanPath = undefined;
|
||||
}
|
||||
|
||||
setTerminalBackground(terminalBackground: string | undefined): void {
|
||||
@@ -2051,6 +2066,37 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return getWorkspaceContextOverride() ?? this.workspaceContext;
|
||||
}
|
||||
|
||||
private refreshSessionScopedPlansDirectory(previousPlansDir: string): void {
|
||||
const nextPlansDir = this.storage.getPlansDir();
|
||||
if (previousPlansDir === nextPlansDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathsToRemove = new Set([previousPlansDir]);
|
||||
try {
|
||||
pathsToRemove.add(resolveToRealPath(previousPlansDir));
|
||||
} catch {
|
||||
// The previous session's plans directory may never have been created.
|
||||
// In that case there is nothing to resolve or remove beyond the raw path.
|
||||
}
|
||||
|
||||
const currentDirectories = this.workspaceContext
|
||||
.getDirectories()
|
||||
.filter((dir) => !pathsToRemove.has(dir));
|
||||
|
||||
this.workspaceContext.setDirectories(currentDirectories);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(nextPlansDir)) {
|
||||
this.workspaceContext.addDirectory(nextPlansDir);
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid or unreadable plans directories here. This mirrors
|
||||
// initialization behavior, which only adds the plans directory when it
|
||||
// already exists and is readable.
|
||||
}
|
||||
}
|
||||
|
||||
getAgentRegistry(): AgentRegistry {
|
||||
return this.agentRegistry;
|
||||
}
|
||||
|
||||
@@ -211,6 +211,27 @@ describe('Storage – additional helpers', () => {
|
||||
expect(storageWithSession.getProjectTempTrackerDir()).toBe(expected);
|
||||
});
|
||||
|
||||
it('updates session-scoped directories when the sessionId changes', async () => {
|
||||
const storageWithSession = new Storage(projectRoot, 'session-one');
|
||||
ProjectRegistry.prototype.getShortId = vi
|
||||
.fn()
|
||||
.mockReturnValue(PROJECT_SLUG);
|
||||
await storageWithSession.initialize();
|
||||
const tempDir = storageWithSession.getProjectTempDir();
|
||||
|
||||
storageWithSession.setSessionId('session-two');
|
||||
|
||||
expect(storageWithSession.getProjectTempPlansDir()).toBe(
|
||||
path.join(tempDir, 'session-two', 'plans'),
|
||||
);
|
||||
expect(storageWithSession.getProjectTempTrackerDir()).toBe(
|
||||
path.join(tempDir, 'session-two', 'tracker'),
|
||||
);
|
||||
expect(storageWithSession.getProjectTempTasksDir()).toBe(
|
||||
path.join(tempDir, 'session-two', 'tasks'),
|
||||
);
|
||||
});
|
||||
|
||||
describe('Session and JSON Loading', () => {
|
||||
beforeEach(async () => {
|
||||
await storage.initialize();
|
||||
|
||||
@@ -28,7 +28,7 @@ export const AUTO_SAVED_POLICY_FILENAME = 'auto-saved.toml';
|
||||
|
||||
export class Storage {
|
||||
private readonly targetDir: string;
|
||||
private readonly sessionId: string | undefined;
|
||||
private sessionId: string | undefined;
|
||||
private projectIdentifier: string | undefined;
|
||||
private initPromise: Promise<void> | undefined;
|
||||
private customPlansDir: string | undefined;
|
||||
@@ -42,6 +42,14 @@ export class Storage {
|
||||
this.customPlansDir = dir;
|
||||
}
|
||||
|
||||
setSessionId(sessionId: string | undefined): void {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
isInitialized(): boolean {
|
||||
return !!this.projectIdentifier;
|
||||
}
|
||||
|
||||
static getGlobalGeminiDir(): string {
|
||||
const homeDir = homedir();
|
||||
if (!homeDir) {
|
||||
|
||||
Reference in New Issue
Block a user