// ***************************************************************************** // Copyright (C) 2026 EclipseSource and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { expect } from 'chai'; import { OS } from '@theia/core/lib/common/os'; import { RemoteCliContext } from '@theia/core/lib/node/remote/remote-cli-contribution'; import { PreferenceCliContribution } from './preference-cli-contribution'; const REMOTE_CLI_CONTEXT: RemoteCliContext = { platform: { os: OS.Type.Linux, arch: 'x64' }, directory: '/' }; describe('PreferenceCliContribution', () => { let contribution: PreferenceCliContribution; beforeEach(() => { contribution = new PreferenceCliContribution(); }); describe('--set-preference', () => { it('parses a single key=value entry as JSON', async () => { contribution.setArguments({ setPreference: 'editor.fontSize=14' }); expect(await contribution.getPreferences()).to.deep.equal([['editor.fontSize', 14]]); expect(await contribution.getSessionPreferences()).to.deep.equal([]); }); it('parses string values quoted as JSON strings', async () => { contribution.setArguments({ setPreference: 'workbench.colorTheme="Dark+"' }); expect(await contribution.getPreferences()).to.deep.equal([['workbench.colorTheme', 'Dark+']]); }); it('parses object values', async () => { contribution.setArguments({ setPreference: 'ai-features.chat.toolConfirmation={"shellExecute":"always_allow"}' }); expect(await contribution.getPreferences()).to.deep.equal([ ['ai-features.chat.toolConfirmation', { shellExecute: 'always_allow' }] ]); }); it('accepts an array of entries', async () => { contribution.setArguments({ setPreference: ['editor.fontSize=12', 'editor.tabSize=2'] }); expect(await contribution.getPreferences()).to.deep.equal([ ['editor.fontSize', 12], ['editor.tabSize', 2] ]); }); it('decodes base64-prefixed JSON values', async () => { const json = JSON.stringify({ nested: true, n: 1 }); const b64 = Buffer.from(json, 'utf-8').toString('base64'); contribution.setArguments({ setPreference: `my.pref=base64:${b64}` }); expect(await contribution.getPreferences()).to.deep.equal([ ['my.pref', { nested: true, n: 1 }] ]); }); it('preserves "=" characters inside the value', async () => { contribution.setArguments({ setPreference: 'my.pref="a=b=c"' }); expect(await contribution.getPreferences()).to.deep.equal([['my.pref', 'a=b=c']]); }); it('skips entries that do not contain "="', async () => { const originalWarn = console.warn; console.warn = () => { /* silence expected warning */ }; try { contribution.setArguments({ setPreference: ['justAKey', 'editor.fontSize=14'] }); } finally { console.warn = originalWarn; } expect(await contribution.getPreferences()).to.deep.equal([['editor.fontSize', 14]]); }); it('skips entries whose value is not valid JSON', async () => { const originalWarn = console.warn; console.warn = () => { /* silence expected warning */ }; try { contribution.setArguments({ setPreference: ['my.pref=notJson', 'editor.fontSize=14'] }); } finally { console.warn = originalWarn; } expect(await contribution.getPreferences()).to.deep.equal([['editor.fontSize', 14]]); }); }); describe('--session-preference', () => { it('routes entries into the session preferences bucket', async () => { contribution.setArguments({ sessionPreference: 'ai-features.chat.defaultToolConfirmation="always_allow"' }); expect(await contribution.getSessionPreferences()).to.deep.equal([ ['ai-features.chat.defaultToolConfirmation', 'always_allow'] ]); expect(await contribution.getPreferences()).to.deep.equal([]); }); it('supports multiple session entries and base64', async () => { const b64 = Buffer.from('{"shellExecute":"always_allow"}', 'utf-8').toString('base64'); contribution.setArguments({ sessionPreference: [ 'ai-features.chat.defaultToolConfirmation="always_allow"', `ai-features.chat.toolConfirmation=base64:${b64}` ] }); expect(await contribution.getSessionPreferences()).to.deep.equal([ ['ai-features.chat.defaultToolConfirmation', 'always_allow'], ['ai-features.chat.toolConfirmation', { shellExecute: 'always_allow' }] ]); }); it('keeps --set-preference and --session-preference independent', async () => { contribution.setArguments({ setPreference: 'editor.fontSize=14', sessionPreference: 'editor.fontSize=20' }); expect(await contribution.getPreferences()).to.deep.equal([['editor.fontSize', 14]]); expect(await contribution.getSessionPreferences()).to.deep.equal([['editor.fontSize', 20]]); }); }); describe('enhanceArgs (remote forwarding)', () => { it('returns an empty list when no session prefs are set', () => { expect(contribution.enhanceArgs(REMOTE_CLI_CONTEXT)).to.deep.equal([]); }); it('emits one base64-encoded --session-preference flag per entry', () => { contribution.setArguments({ sessionPreference: [ 'ai-features.chat.defaultToolConfirmation="always_allow"', 'ai-features.chat.toolConfirmation={"shellExecute":"always_allow"}' ] }); const args = contribution.enhanceArgs(REMOTE_CLI_CONTEXT); expect(args).to.have.lengthOf(2); for (const arg of args) { expect(arg.startsWith('--session-preference=')).to.be.true; const eq = arg.indexOf('=', '--session-preference='.length); const key = arg.substring('--session-preference='.length, eq); const rawValue = arg.substring(eq + 1); expect(rawValue.startsWith('base64:')).to.be.true; const decoded = JSON.parse(Buffer.from(rawValue.substring('base64:'.length), 'base64').toString('utf-8')); if (key === 'ai-features.chat.defaultToolConfirmation') { expect(decoded).to.equal('always_allow'); } else { expect(decoded).to.deep.equal({ shellExecute: 'always_allow' }); } } }); it('does not forward --set-preference values', () => { contribution.setArguments({ setPreference: 'editor.fontSize=14', sessionPreference: 'foo="bar"' }); const args = contribution.enhanceArgs(REMOTE_CLI_CONTEXT); expect(args).to.have.lengthOf(1); expect(args[0]).to.match(/^--session-preference=foo=base64:/); }); }); });