fix(core): reset session-scoped state on resumption (#26342)

This commit is contained in:
Coco Sheng
2026-05-01 17:20:06 -04:00
committed by GitHub
parent a93d2a1d1c
commit 408afd3c5a
4 changed files with 104 additions and 1 deletions
+65
View File
@@ -23,6 +23,7 @@ import { createMockSandboxConfig } from '@google/gemini-cli-test-utils';
import { DEFAULT_MAX_ATTEMPTS } from '../utils/retry.js';
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
import { debugLogger } from '../utils/debugLogger.js';
import { coreEvents } from '../utils/events.js';
import { ApprovalMode } from '../policy/types.js';
import {
HookType,
@@ -1940,6 +1941,70 @@ describe('Server Config (config.ts)', () => {
expect(config.getSessionId()).toBe('session-two');
expect(config.getApprovedPlanPath()).toBeUndefined();
});
it('performs a comprehensive reset of all session-scoped state when sessionId changes', async () => {
const config = new Config({
...baseParams,
sessionId: 'session-one',
plan: true,
tracker: true,
});
await config.initialize();
// 1. "Dirty" the session state
const oldTrackerService = config.getTrackerService();
config.setApprovedPlanPath('/tmp/plan.md');
config.topicState.setTopic('Old Topic', 'Old Intent');
config.getSkillManager().activateSkill('old-skill');
config.getModelAvailabilityService().markTerminal('model-1', 'quota');
config.setLatestApiRequest({} as never);
// Interface to access private fields without 'any'
interface PrivateConfig {
modelQuotas: Map<string, unknown>;
lastEmittedQuotaRemaining: number | undefined;
lastEmittedQuotaLimit: number | undefined;
lastQuotaFetchTime: number;
hasAccessToPreviewModel: boolean | null;
}
const configInternal = config as unknown as PrivateConfig;
// Mock internal quota state
configInternal.modelQuotas.set('model-1', { remaining: 0, limit: 100 });
configInternal.lastEmittedQuotaRemaining = 0;
configInternal.lastEmittedQuotaLimit = 100;
configInternal.lastQuotaFetchTime = 12345;
configInternal.hasAccessToPreviewModel = true;
// Listen for quota event
const emitQuotaSpy = vi.spyOn(coreEvents, 'emitQuotaChanged');
// 2. Trigger session change
config.setSessionId('session-two');
// 3. Verify EVERYTHING is reset
expect(config.getSessionId()).toBe('session-two');
expect(config.getApprovedPlanPath()).toBeUndefined();
expect(config.topicState.getTopic()).toBeUndefined();
expect(config.topicState.getIntent()).toBeUndefined();
expect(config.getSkillManager().isSkillActive('old-skill')).toBe(false);
expect(config.getTrackerService()).not.toBe(oldTrackerService);
expect(
config.getModelAvailabilityService().snapshot('model-1').available,
).toBe(true);
expect(config.getLatestApiRequest()).toBeUndefined();
// Quota resets
expect(configInternal.modelQuotas.size).toBe(0);
expect(configInternal.lastEmittedQuotaRemaining).toBeUndefined();
expect(configInternal.lastEmittedQuotaLimit).toBeUndefined();
expect(configInternal.lastQuotaFetchTime).toBe(0);
expect(configInternal.hasAccessToPreviewModel).toBeNull();
// Event emission
expect(emitQuotaSpy).toHaveBeenCalledWith(undefined, undefined, undefined);
});
});
describe('GemmaModelRouterSettings', () => {
+18 -1
View File
@@ -1803,6 +1803,24 @@ export class Config implements McpContext, AgentLoopContext {
this._sessionId = sessionId;
this.storage.setSessionId(sessionId);
this.trackerService = undefined;
this.approvedPlanPath = undefined;
this.topicState.reset();
this.skillManager.reset();
this.latestApiRequest = undefined;
this.lastModeSwitchTime = performance.now();
this.compressionTruncationCounter = 0;
this.quotaErrorOccurred = false;
this.creditsNotificationShown = false;
this.modelAvailabilityService.reset();
this.modelQuotas.clear();
this.lastRetrievedQuota = undefined;
this.lastQuotaFetchTime = 0;
this.hasAccessToPreviewModel = null;
// Force an event emission to clear the UI display
coreEvents.emitQuotaChanged(undefined, undefined, undefined);
this.lastEmittedQuotaRemaining = undefined;
this.lastEmittedQuotaLimit = undefined;
if (previousPlansDir) {
this.refreshSessionScopedPlansDirectory(previousPlansDir);
@@ -1811,7 +1829,6 @@ export class Config implements McpContext, AgentLoopContext {
resetNewSessionState(sessionId: string): void {
this.setSessionId(sessionId);
this.approvedPlanPath = undefined;
}
setTerminalBackground(terminalBackground: string | undefined): void {
@@ -318,6 +318,20 @@ description: project-desc
expect(service.isAdminEnabled()).toBe(false);
});
it('should reset active skill names', () => {
const service = new SkillManager();
service.activateSkill('skill-1');
service.activateSkill('skill-2');
expect(service.isSkillActive('skill-1')).toBe(true);
expect(service.isSkillActive('skill-2')).toBe(true);
service.reset();
expect(service.isSkillActive('skill-1')).toBe(false);
expect(service.isSkillActive('skill-2')).toBe(false);
});
describe('Conflict Detection', () => {
it('should emit UI warning when a non-built-in skill is overridden', async () => {
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
+7
View File
@@ -26,6 +26,13 @@ export class SkillManager {
this.skills = [];
}
/**
* Resets session-scoped state (active skill names).
*/
reset(): void {
this.activeSkillNames.clear();
}
/**
* Sets administrative settings for skills.
*/