diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts index 2c0e07baff..48007a58ca 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts @@ -65,6 +65,33 @@ describe('ChatDatabase', () => { sinon.restore() }) + describe('pinned context', () => { + beforeEach(async () => { + await chatDb.databaseInitialize(0) + }) + + it('does not mutate the shared DEFAULT_PINNED_CONTEXT when pinning in a tab', () => { + const defaultLengthBefore = util.DEFAULT_PINNED_CONTEXT.length + const customContext = { id: 'custom-folder-ctx', command: 'myFolder', label: 'folder' } as any + + // Create the tab first (with tabContext but no pinnedContext yet) so that + // addPinnedContext initializes pinnedContext from the shared default — the + // code path that previously aliased and then mutated DEFAULT_PINNED_CONTEXT. + chatDb.setRules('tab-1', { folders: {}, rules: {} } as any) + chatDb.addPinnedContext('tab-1', customContext) + + // The shared module-level default must not be polluted by pinning in a tab, + // otherwise every new chat window would inherit the custom pinned item. + assert.strictEqual(util.DEFAULT_PINNED_CONTEXT.length, defaultLengthBefore) + assert.ok(!util.DEFAULT_PINNED_CONTEXT.some((c: any) => c.id === 'custom-folder-ctx')) + + // A brand-new tab must only receive the default pinned context, not the item + // that was pinned in a different tab. + const freshTabPinned = chatDb.getPinnedContext('tab-2-fresh') + assert.ok(!freshTabPinned.some((c: any) => c.id === 'custom-folder-ctx')) + }) + }) + describe('replaceWithSummary', () => { it('should create a new history with summary message', async () => { await chatDb.databaseInitialize(0) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts index b0dd06280f..de9b63f999 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts @@ -269,7 +269,9 @@ export class ChatDatabase { const historyId = this.getOrCreateHistoryId(tabId) if (historyId) { const tab = collection.findOne({ historyId }) - return tab?.tabContext?.pinnedContext || DEFAULT_PINNED_CONTEXT + // Return a copy of the default so callers cannot mutate the shared + // module-level DEFAULT_PINNED_CONTEXT array. + return tab?.tabContext?.pinnedContext || [...DEFAULT_PINNED_CONTEXT] } } return [] @@ -313,7 +315,10 @@ export class ChatDatabase { tab.tabContext = {} } if (!tab.tabContext.pinnedContext) { - tab.tabContext.pinnedContext = DEFAULT_PINNED_CONTEXT + // Use a copy: DEFAULT_PINNED_CONTEXT is a shared module-level array, + // and the unshift/push below would otherwise mutate it, polluting the + // default pinned context for every new tab/chat window. + tab.tabContext.pinnedContext = [...DEFAULT_PINNED_CONTEXT] } // Only add context item if its not already in this tab's pinned context if (!tab.tabContext.pinnedContext.find(c => c.id === context.id)) {