Verified Commit 1834085d authored by Mohammed Osumah's avatar Mohammed Osumah 2️⃣ Committed by GitLab
Browse files

feat: surface sandbox state in diagnostics and duo quick pick menu

parent 9e2f941a
Loading
Loading
Loading
Loading
+15 −1
Original line number Diff line number Diff line
@@ -153,7 +153,14 @@ const createMockState = (engaged: boolean): AllFeaturesState => {
        createCheck('agentic-chat-no-support', 'Not supported'),
      ],
    },
    [SANDBOX]: { featureId: SANDBOX, engagedChecks: [], allChecks: [] },
    [SANDBOX]: {
      featureId: SANDBOX,
      engagedChecks: engaged ? [createCheck('sandbox-disabled-by-user' as StateCheckId)] : [],
      allChecks: [
        createCheck('sandbox-disabled-by-user' as StateCheckId),
        createCheck('sandbox-missing-dependencies' as StateCheckId),
      ],
    },
  };
};

@@ -221,6 +228,13 @@ describe('FeatureStateDiagnosticsRenderer', () => {
      expect(result[3].content).toBe(expectedAgenticChat);
      expect(result[4].title).toBe(`GitLab Flows (${expectedStatus})`);
      expect(result[4].content).toBe(expectedFlows);

      const expectedSandboxing =
        `- ${checkStatusPre} Process sandboxing is enabled in settings ${checkStatusAppend}\n` +
        `- ${checkStatusPre} System dependencies required for process sandboxing are installed ${checkStatusAppend}`;

      expect(result[5].title).toBe(`GitLab Duo Agent Sandboxing (${expectedStatus})`);
      expect(result[5].content).toBe(expectedSandboxing);
    },
  );

+2 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import {
  CHAT_TERMINAL_CONTEXT,
  AGENTIC_CHAT,
  FLOWS,
  SANDBOX,
} from '@gitlab-org/gitlab-lsp';
import { DiagnosticsRenderer, DiagnosticsSection } from '../diagnostics_service';
import {
@@ -70,6 +71,7 @@ export class FeatureStateDiagnosticsRenderer implements DiagnosticsRenderer<[All
        agentPlatformEnabledCheck,
      ),
      createFeatureStateDiagnosticsSection('GitLab Flows', state[FLOWS], agentPlatformEnabledCheck),
      createFeatureStateDiagnosticsSection('GitLab Duo Agent Sandboxing', state[SANDBOX]),
    ];
  }
}
+78 −0
Original line number Diff line number Diff line
@@ -10,6 +10,8 @@ import { createFakePartial } from '../../test_utils/create_fake_partial';
import { createFakeWorkspaceConfiguration } from '../../test_utils/vscode_fakes';
import * as utils from '../utils';
import { USER_COMMANDS } from '../../command_names';
import { ENABLE_SANDBOX, DISABLE_SANDBOX } from '../constants';
import { COMMAND_TOGGLE_SANDBOX } from '../../sandbox/commands/toggle_sandbox';
import { showDuoQuickPickMenu } from './show_quick_pick_menu';

describe('show duo quick pick menu command', () => {
@@ -235,6 +237,82 @@ describe('show duo quick pick menu command', () => {
    });
  });

  describe('sandbox status item', () => {
    const sandboxItemMock = { label: '$(pass) GitLab Duo Agent Sandboxing: Enabled' };

    beforeEach(() => {
      jest.spyOn(utils, 'generateSandboxToggleItem').mockReturnValue(undefined);
    });

    it('does not show sandbox item when no languageServerFeatureStateProvider is given', () => {
      expect(findItemByLabel(sandboxItemMock.label)).toBeUndefined();
    });

    it('shows sandbox item in the status section when provider returns an item', async () => {
      jest.spyOn(utils, 'generateSandboxStatusItem').mockReturnValue(sandboxItemMock);

      await showDuoQuickPickMenu({
        stateManager: mockStateManager,
        languageServerFeatureStateProvider: {} as never,
      });

      expect(findItemByLabel(sandboxItemMock.label)).toBeDefined();
    });

    it('does not show sandbox item when provider returns undefined (LS not yet reporting)', async () => {
      jest.spyOn(utils, 'generateSandboxStatusItem').mockReturnValue(undefined);

      await showDuoQuickPickMenu({
        stateManager: mockStateManager,
        languageServerFeatureStateProvider: {} as never,
      });

      expect(findItemByLabel(sandboxItemMock.label)).toBeUndefined();
    });

    it('does not show sandbox item when account is missing', async () => {
      jest.spyOn(utils, 'generateSandboxStatusItem').mockReturnValue(sandboxItemMock);

      setupTest({ isMissingAccount: true, visibleState: VisibleCodeSuggestionsState.NO_ACCOUNT });
      await showDuoQuickPickMenu({
        stateManager: mockStateManager,
        languageServerFeatureStateProvider: {} as never,
      });

      expect(findItemByLabel(sandboxItemMock.label)).toBeUndefined();
    });

    it('toggles sandbox when the toggle item is selected', async () => {
      const toggleItem = { label: ENABLE_SANDBOX };
      jest.spyOn(utils, 'generateSandboxToggleItem').mockReturnValue(toggleItem);

      setupTest();
      await showDuoQuickPickMenu({
        stateManager: mockStateManager,
        languageServerFeatureStateProvider: {} as never,
      });

      const selectionCallback = jest.mocked(mockQuickPick.onDidChangeSelection).mock.calls[0][0];
      await selectionCallback([toggleItem]);

      expect(vscode.commands.executeCommand).toHaveBeenCalledWith(COMMAND_TOGGLE_SANDBOX);
      expect(mockQuickPick.hide).toHaveBeenCalled();
    });

    it('does not show sandbox toggle item when provider returns undefined', async () => {
      jest.spyOn(utils, 'generateSandboxToggleItem').mockReturnValue(undefined);

      setupTest();
      await showDuoQuickPickMenu({
        stateManager: mockStateManager,
        languageServerFeatureStateProvider: {} as never,
      });

      expect(findItemByLabel(ENABLE_SANDBOX)).toBeUndefined();
      expect(findItemByLabel(DISABLE_SANDBOX)).toBeUndefined();
    });
  });

  describe('code suggestions language toggle', () => {
    const toggleItemMock = { label: 'Enable Code Suggestions for javascript' };

+18 −0
Original line number Diff line number Diff line
@@ -10,6 +10,8 @@ import {
  generateQuickPickItem,
  generateDuoUnavailableStatusItem,
  generateDuoDiagnosticsStatusItem,
  generateSandboxStatusItem,
  generateSandboxToggleItem,
} from '../utils';
import {
  CODE_SUGGESTIONS_DISABLED_NOTIFICATION,
@@ -26,6 +28,8 @@ import {
import { CodeSuggestionsStateManager } from '../../code_suggestions/code_suggestions_state_manager';
import { COMMAND_TOGGLE_CODE_SUGGESTIONS } from '../../code_suggestions/commands/toggle';
import { COMMAND_TOGGLE_CODE_SUGGESTIONS_FOR_LANGUAGE } from '../../code_suggestions/commands/toggle_language';
import { LanguageServerFeatureStateProvider } from '../../language_server/language_server_feature_state_provider';
import { COMMAND_TOGGLE_SANDBOX } from '../../sandbox/commands/toggle_sandbox';

export const SHOW_QUICK_PICK_MENU = 'gl.showDuoQuickPickMenu';

@@ -50,6 +54,7 @@ const openDuoChat = () => {
const toggleCodeSuggestions = () => vscode.commands.executeCommand(COMMAND_TOGGLE_CODE_SUGGESTIONS);
const toggleCodeSuggestionsForLanguage = () =>
  vscode.commands.executeCommand(COMMAND_TOGGLE_CODE_SUGGESTIONS_FOR_LANGUAGE);
const toggleSandbox = () => vscode.commands.executeCommand(COMMAND_TOGGLE_SANDBOX);
const openDuoSettings = () =>
  vscode.commands.executeCommand(VS_COMMANDS.OPEN_SETTINGS, GITLAB_WORKFLOW_SETTINGS_ANCHOR);
const openDocumentation = () => vscode.env.openExternal(vscode.Uri.parse(DOCUMENTATION_URL));
@@ -58,8 +63,10 @@ const showExtensionLogs = () => vscode.commands.executeCommand(USER_COMMANDS.SHO

export const showDuoQuickPickMenu = async ({
  stateManager,
  languageServerFeatureStateProvider,
}: {
  stateManager: CodeSuggestionsStateManager;
  languageServerFeatureStateProvider?: LanguageServerFeatureStateProvider;
}) => {
  const statusItems: vscode.QuickPickItem[] = [];
  const toggleItems: vscode.QuickPickItem[] = [];
@@ -74,12 +81,20 @@ export const showDuoQuickPickMenu = async ({
  const codeSuggestionsLanguageToggleItem =
    generateCodeSuggestionsLangToggleItem(codeSuggestionsEnabled);
  const duoDiagnosticsStatusItem = generateDuoDiagnosticsStatusItem(stateManager.getVisibleState());
  const sandboxStatusItem = languageServerFeatureStateProvider
    ? generateSandboxStatusItem(languageServerFeatureStateProvider)
    : undefined;
  const sandboxToggleItem = languageServerFeatureStateProvider
    ? generateSandboxToggleItem(languageServerFeatureStateProvider)
    : undefined;
  if (!isMissingAccount) {
    statusItems.push(codeSuggestionsStatusItem, duoChatStatusItem);
    if (sandboxStatusItem) statusItems.push(sandboxStatusItem);
    toggleItems.push(
      ...[
        ...(codeSuggestionsLanguageToggleItem ? [codeSuggestionsLanguageToggleItem] : []),
        codeSuggestionsToggleItem,
        ...(sandboxToggleItem ? [sandboxToggleItem] : []),
      ],
    );
  } else {
@@ -127,6 +142,9 @@ export const showDuoQuickPickMenu = async ({
      case duoSettingsItem:
        await openDuoSettings();
        break;
      case sandboxToggleItem:
        await toggleSandbox();
        break;
      case documentationItem:
        await openDocumentation();
        break;
+7 −0
Original line number Diff line number Diff line
@@ -26,3 +26,10 @@ export const DUO_CHAT_DISABLED_NOTIFICATION = 'GitLab Duo Chat is currently disa
export const GITLAB_WORKFLOW_SETTINGS_ANCHOR = '@ext:gitlab.gitlab-workflow';

export const DUO_STATUS_ZERO_PROBLEMS_DETECTED = 'Status: No problems detected';

export const SANDBOX_STATUS_ENABLED = 'Agent Sandboxing: Enabled';
export const SANDBOX_STATUS_DISABLED = 'Agent Sandboxing: Disabled';
export const SANDBOX_STATUS_UNSUPPORTED = 'Agent Sandboxing: Unsupported';
export const SANDBOX_STATUS_MISSING_DEPS = 'Agent Sandboxing: Missing Dependencies';
export const ENABLE_SANDBOX = 'Enable Agent Sandboxing';
export const DISABLE_SANDBOX = 'Disable Agent Sandboxing';
Loading