Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCloudSessionsProvider.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { describe, expect, it, vi } from 'vitest';6import * as vscode from 'vscode';7import { IGitService } from '../../../../platform/git/common/gitService';8import { PullRequestSearchItem, SessionInfo } from '../../../../platform/github/common/githubAPI';9import { TestLogService } from '../../../../platform/testing/common/testLogService';10import { mock } from '../../../../util/common/test/simpleMock';11import { ChatResponseMarkdownPart, ChatResponseTurn2 } from '../../../../vscodeTypes';12import { ChatSessionContentBuilder } from '../copilotCloudSessionContentBuilder';13import { normalizeInitialSessionOptions, parseSessionLogChunksSafely } from '../copilotCloudSessionsProvider';1415vi.mock('vscode', async () => {16const actual = await import('../../../../vscodeTypes');17return {18...actual,19workspace: {20workspaceFolders: [],21},22};23});2425class RecordingLogService extends TestLogService {26override readonly trace = vi.fn();27override readonly warn = vi.fn();28override readonly error = vi.fn();29}3031class TestGitService extends mock<IGitService>() {32declare readonly _serviceBrand: undefined;33override activeRepository = { get: () => undefined } as IGitService['activeRepository'];34override initialize = vi.fn(async () => { });35override repositories = [];36}3738function createPullRequest(): PullRequestSearchItem {39return {40id: 'pr-1',41number: 1,42title: 'Test PR',43state: 'OPEN',44url: 'https://example.com/pr/1',45createdAt: '2026-03-27T00:00:00Z',46updatedAt: '2026-03-27T00:00:00Z',47author: { login: 'octocat' },48repository: {49owner: { login: 'microsoft' },50name: 'vscode',51},52additions: 1,53deletions: 0,54files: { totalCount: 1 },55fullDatabaseId: 1,56headRefOid: 'abc123',57headRefName: 'copilot/test-branch',58baseRefName: 'main',59body: 'Body',60};61}6263function createSession(state: SessionInfo['state'] = 'completed'): SessionInfo {64return {65id: 'session-1',66name: 'Cloud session',67user_id: 1,68agent_id: 1,69logs: '',70logs_blob_id: 'blob-1',71state,72owner_id: 1,73repo_id: 1,74resource_type: 'pull_request',75resource_id: 1,76last_updated_at: '2026-03-27T00:00:00Z',77created_at: '2026-03-27T00:00:00Z',78completed_at: '2026-03-27T00:00:00Z',79event_type: 'pull_request',80workflow_run_id: 1,81premium_requests: 0,82error: null,83resource_global_id: 'global-1',84};85}8687describe('copilotCloudSessionsProvider helpers', () => {88it('coerces object-shaped initialSessionOptions into option entries', () => {89const logService = new RecordingLogService();90const sessionResource = vscode.Uri.parse('copilot-cloud-agent:/1');9192const result = normalizeInitialSessionOptions({93models: { id: 'gpt-4.1', name: 'GPT-4.1' },94repositories: 'microsoft/vscode',95}, logService, sessionResource);9697expect(result).toEqual([98{ optionId: 'models', value: { id: 'gpt-4.1', name: 'GPT-4.1' } },99{ optionId: 'repositories', value: 'microsoft/vscode' },100]);101expect(logService.warn).toHaveBeenCalledWith(expect.stringContaining('Coerced object-shaped initialSessionOptions'));102});103104it('ignores unsupported initialSessionOptions payloads and logs a warning', () => {105const logService = new RecordingLogService();106107const result = normalizeInitialSessionOptions({108models: { foo: 'bar' },109}, logService);110111expect(result).toEqual([]);112expect(logService.warn).toHaveBeenCalledWith(expect.stringContaining('Ignoring unsupported initialSessionOptions'));113});114115it('logs parse failures when streamed log content is malformed', () => {116const logService = new RecordingLogService();117118const result = parseSessionLogChunksSafely('data: {not-json}', logService, () => {119throw new SyntaxError('Unexpected token');120});121122expect(result).toEqual([]);123expect(logService.error).toHaveBeenCalledWith(expect.any(SyntaxError), expect.stringContaining('Failed to parse streamed log content'));124});125});126127describe('ChatSessionContentBuilder', () => {128it('ignores malformed tool_calls payloads instead of throwing', async () => {129const builder = new ChatSessionContentBuilder('copilot-cloud-agent', new TestGitService());130const logs = [131'data: {"choices":[{"finish_reason":"stop","delta":{"role":"assistant","content":"Cloud reply","tool_calls":{"id":"not-an-array"}}}],"created":0,"id":"chunk-1","usage":{"completion_tokens":0,"prompt_tokens":0,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":0},"model":"test-model","object":"chat.completion.chunk"}',132].join('\n');133134const history = await builder.buildSessionHistory(135Promise.resolve('Continue in cloud'),136[createSession()],137createPullRequest(),138async () => logs,139Promise.resolve([]),140);141142expect(history).toHaveLength(2);143const responseTurn = history[1];144expect(responseTurn).toBeInstanceOf(ChatResponseTurn2);145if (!(responseTurn instanceof ChatResponseTurn2)) {146throw new Error('Expected a response turn.');147}148149expect(responseTurn.response).toHaveLength(1);150expect(responseTurn.response[0]).toBeInstanceOf(ChatResponseMarkdownPart);151if (!(responseTurn.response[0] instanceof ChatResponseMarkdownPart)) {152throw new Error('Expected markdown response content.');153}154155expect(responseTurn.response[0].value.value).toBe('Cloud reply');156});157});158159160