Path: blob/main/extensions/copilot/src/extension/prompts/node/agent/test/parseAttachments.spec.ts
13406 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 type { Attachment } from '@github/copilot/sdk';6import { afterEach, beforeEach, expect, suite, test, vi } from 'vitest';7import { IFileSystemService } from '../../../../../platform/filesystem/common/fileSystemService';8import { FileType } from '../../../../../platform/filesystem/common/fileTypes';9import { MockFileSystemService } from '../../../../../platform/filesystem/node/test/mockFileSystemService';10import { IIgnoreService } from '../../../../../platform/ignore/common/ignoreService';11import { ILogService } from '../../../../../platform/log/common/logService';12import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';13import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';14import { DiagnosticSeverity } from '../../../../../util/common/test/shims/enums';15import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';16import { mock } from '../../../../../util/common/test/simpleMock';17import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';18import { DisposableStore } from '../../../../../util/vs/base/common/lifecycle';19import { Schemas } from '../../../../../util/vs/base/common/network';20import { URI } from '../../../../../util/vs/base/common/uri';21import { Location } from '../../../../../util/vs/workbench/api/common/extHostTypes/location';22import { Range } from '../../../../../util/vs/workbench/api/common/extHostTypes/range';23import { ChatReferenceDiagnostic } from '../../../../../vscodeTypes';24import { emptyWorkspaceInfo, IWorkspaceInfo } from '../../../../chatSessions/common/workspaceInfo';25import { extractChatPromptReferences } from '../../../../chatSessions/copilotcli/common/copilotCLIPrompt';26import { CopilotCLIImageSupport } from '../../../../chatSessions/copilotcli/node/copilotCLIImageSupport';27import { CopilotCLIPromptResolver } from '../../../../chatSessions/copilotcli/node/copilotcliPromptResolver';28import { MockSkillLocations } from '../../../../chatSessions/copilotcli/node/test/testHelpers';29import { createExtensionUnitTestingServices } from '../../../../test/node/services';30import { TestChatRequest } from '../../../../test/node/testHelpers';31import { MockExtensionContext } from '../../../../../platform/test/node/extensionContext';32import { IVSCodeExtensionContext } from '../../../../../platform/extContext/common/extensionContext';333435suite('CopilotCLI Generate & parse prompts', () => {36(['emptyWorkspace', 'workspace', 'worktree'] as const).forEach(workspaceType => {37suite(workspaceType, () => {38const disposables = new DisposableStore();39let fileSystem: MockFileSystemService;40let workspaceService: TestWorkspaceService;41let resolver: CopilotCLIPromptResolver;42const workspaceInfo = createWorkspaceInfo(workspaceType);43beforeEach(() => {44const services = createExtensionUnitTestingServices(disposables);45const accessor = disposables.add(services.createTestingAccessor());46fileSystem = accessor.get(IFileSystemService) as MockFileSystemService;47workspaceService = accessor.get(IWorkspaceService) as TestWorkspaceService;48const logService = accessor.get(ILogService);49const imageSupport = new class extends mock<CopilotCLIImageSupport>() {50override storeImage(imageData: Uint8Array, mimeType: string): Promise<URI> {51throw new Error('Method not implemented.');52}53};54if (workspaceType === 'workspace' || workspaceType === 'worktree') {55workspaceService.getWorkspaceFolders().push(URI.file('/workspace'));56}57resolver = new CopilotCLIPromptResolver(imageSupport, logService, fileSystem, workspaceService, services.seal(), accessor.get(IIgnoreService), new MockSkillLocations(), new MockExtensionContext() as unknown as IVSCodeExtensionContext);58});59afterEach(() => {60disposables.clear();61vi.resetAllMocks();62});63test('just the prompt without anything else', async () => {64const req = new TestChatRequest('hello world');65const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);6667const result = extractChatPromptReferences(resolved.prompt);68expect(resolved.prompt).toMatchSnapshot();69expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();70expect(result).toMatchSnapshot();71});7273test('returns original prompt unchanged for slash command', async () => {74const req = new TestChatRequest('/help something');75const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);7677const result = extractChatPromptReferences(resolved.prompt);78expect(resolved.prompt).toMatchSnapshot();79expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();80expect(result).toMatchSnapshot();81});8283test('returns overridden prompt instead of using the request prompt', async () => {84const req = new TestChatRequest('/help something');85const resolved = await resolver.resolvePrompt(req, 'What is 1+2', [], workspaceInfo, [], CancellationToken.None);8687const result = extractChatPromptReferences(resolved.prompt);88expect(resolved.prompt).toMatchSnapshot();89expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();90expect(result).toMatchSnapshot();91});9293test('files are attached as just references without content', async () => {94const tsUri = URI.file('/workspace/file.ts');95createMockFile(tsUri,96`function add(a: number, b: number) {97return a + b;98}99100function subtract(a: number, b: number) {101return a - b;102}103`);104const pyUri = URI.file('/workspace/sample.py');105createMockFile(pyUri,106`deff add(a, b):107return a + b;108109def subtract(a, b):110return a - b111`);112113const req = new TestChatRequest('explain contents of #file:file.ts and other files', [114{115id: tsUri.toString(),116name: 'file:file.ts',117range: [20, 32],118value: tsUri119},120{121id: pyUri.toString(),122name: 'sample.py',123value: pyUri124}125]);126const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);127128const result = extractChatPromptReferences(resolved.prompt);129expect(resolved.prompt).toMatchSnapshot();130expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();131expect(result).toMatchSnapshot();132});133test('Folders are attached with just references', async () => {134const folderUri = URI.file('/workspace/folder');135136fileSystem.mockDirectory(folderUri, [137['file1.txt', FileType.File],138['file2.txt', FileType.File],139]);140if (workspaceType === 'worktree') {141fileSystem.mockDirectory(URI.file('/worktree/folder'), [142['file1.txt', FileType.File],143['file2.txt', FileType.File],144]);145}146const req = new TestChatRequest('list files in #file:folder', [147{148id: folderUri.toString(),149name: 'file:folder',150value: folderUri151}152]);153const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);154155const result = extractChatPromptReferences(resolved.prompt);156expect(resolved.prompt).toMatchSnapshot();157expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();158expect(result).toMatchSnapshot();159});160161test('parses single error diagnostic', async () => {162createMockFile(URI.file('/workspace/file.py'), `pass`);163const req = new TestChatRequest('Fix this error', [164{165id: new Location(URI.file('/workspace/file.py'), new Range(12, 0, 12, 20)).toString(),166name: 'Unterminated string',167value: new ChatReferenceDiagnostic([168[169URI.file('/workspace/file.py'),170[{171message: 'Unterminated string',172severity: DiagnosticSeverity.Error,173range: new Range(12, 0, 12, 20),174code: 'E001'175}]176]])177}178]);179const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);180181const result = extractChatPromptReferences(resolved.prompt);182expect(resolved.prompt).toMatchSnapshot();183expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();184expect(result).toMatchSnapshot();185});186187test('groups diagnostics based on same range', async () => {188createMockFile(URI.file('/workspace/file.py'), `pass`);189const req = new TestChatRequest('Fix these errors', [190{191id: new Location(URI.file('/workspace/file.py'), new Range(12, 0, 12, 20)).toString(),192name: 'Unterminated string',193value: new ChatReferenceDiagnostic([194[195URI.file('/workspace/file.py'),196[197{198message: 'Msg1',199severity: DiagnosticSeverity.Warning,200range: new Range(1, 0, 1, 20),201code: 'E001'202},203{204message: 'MsgB',205severity: DiagnosticSeverity.Error,206range: new Range(1, 0, 4, 20),207code: 'E002'208},209{210message: 'MsgC',211severity: DiagnosticSeverity.Information,212range: new Range(6, 1, 6, 20),213code: 'E003'214},215{216message: 'MsgD',217severity: DiagnosticSeverity.Hint,218range: new Range(6, 10, 6, 15),219code: 'E004',220},221]222]223])224}225]);226227const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);228229const result = extractChatPromptReferences(resolved.prompt);230expect(resolved.prompt).toMatchSnapshot();231expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();232expect(result).toMatchSnapshot();233});234test('aggregates multiple errors across same and different files', async () => {235createMockFile(URI.file('/workspace/file.py'), `pass`);236createMockFile(URI.file('/workspace/sample.py'), `pass`);237238const req = new TestChatRequest('Fix these errors', [239{240id: new Location(URI.file('/workspace/file.py'), new Range(12, 0, 12, 20)).toString(),241name: 'Unterminated string',242value: new ChatReferenceDiagnostic([243[244URI.file('/workspace/file.py'),245[246{247message: 'Msg1',248severity: DiagnosticSeverity.Warning,249range: new Range(1, 0, 1, 20),250code: 'E001'251},252{253message: 'MsgB',254severity: DiagnosticSeverity.Error,255range: new Range(4, 0, 4, 20),256code: 'E002'257},258{259message: 'MsgC',260severity: DiagnosticSeverity.Information,261range: new Range(6, 1, 6, 20),262code: 'E003'263},264{265message: 'MsgD',266severity: DiagnosticSeverity.Hint,267range: new Range(1, 1, 1, 10),268code: 'E004',269},270]271],272[273URI.file('/workspace/sample.py'),274[275{276message: 'Msg2',277severity: DiagnosticSeverity.Warning,278range: new Range(20, 0, 21, 10),279code: 'W001'280},281]282]])283}284]);285286const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);287288const result = extractChatPromptReferences(resolved.prompt);289expect(resolved.prompt).toMatchSnapshot();290expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();291expect(result).toMatchSnapshot();292});293test('parses locations including files with spaces', async () => {294const tsUri = URI.file('/workspace/file.ts');295createMockFile(tsUri,296`function add(a: number, b: number) {297return a + b;298}299300function subtract(a: number, b: number) {301return a - b;302}303`);304const tsWithSpacesUri = URI.file('/workspace/hello world/sample.ts');305createMockFile(tsWithSpacesUri,306`function mod(a: number) {307return a;308}`);309const pyUri = URI.file('/workspace/sample.py');310createMockFile(pyUri,311`deff add(a, b):312return a + b;313314def subtract(a, b):315return a - b316`);317const req = new TestChatRequest('base', [318{319id: tsUri.toString(),320name: 'file:file.ts',321value: new Location(tsUri, new Range(4, 0, 4, 15))322},323{324id: tsWithSpacesUri.toString(),325name: 'file:sample.ts',326value: new Location(tsWithSpacesUri, new Range(4, 0, 4, 15))327},328{329id: pyUri.toString(),330name: 'file:sample.py',331value: new Location(pyUri, new Range(3, 0, 3, 15))332}333]);334const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);335336const result = extractChatPromptReferences(resolved.prompt);337expect(resolved.prompt).toMatchSnapshot();338expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();339expect(result).toMatchSnapshot();340});341342test('uses attachment id attribute for name/id', async () => {343const tsUri = URI.file('/workspace/add.py');344createMockFile(tsUri,345`# Basic arithmetic ops346def add(a, b):347return a + b348}349350def subtract(a, b):351return a - b352`);353const req = new TestChatRequest('explain #sym:add', [354{355id: 'sym:add',356name: 'sym:add',357value: new Location(URI.file('/workspace/add.py'), new Range(1, 0, 3, 15)),358range: [1, 3]359}360]);361362const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);363364const result = extractChatPromptReferences(resolved.prompt);365expect(resolved.prompt).toMatchSnapshot();366expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();367expect(result).toMatchSnapshot();368});369370test('includes contents of untitled file', async () => {371const untitledTsFile = {372id: 'file:untitled-1',373name: 'file:untitled-1',374value: URI.from({ scheme: Schemas.untitled, path: 'untitled-1' })375};376createMockFile(untitledTsFile.value, `function example() {377console.log("This is an example");378}`);379const req = new TestChatRequest('Process these files', [380untitledTsFile381]);382383const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);384385const result = extractChatPromptReferences(resolved.prompt);386expect(resolved.prompt).toMatchSnapshot();387expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();388expect(result).toMatchSnapshot();389});390391test('includes contents of untitled prompt files', async () => {392const untitledPromptFile = {393id: 'vscode.prompt.file__untitled:untitled-1',394name: 'prompt:Untitled-2',395value: URI.from({ scheme: Schemas.untitled, path: 'untitled-1' })396};397const regularFileRef = {398id: 'regular-file',399name: 'regular.ts',400value: URI.file('/workspace/regular.ts')401};402createMockFile(untitledPromptFile.value, `This is a prompt file`);403createMockFile(regularFileRef.value, `This is a regular file`);404405const req = new TestChatRequest('Process these files', [406untitledPromptFile,407regularFileRef408]);409410const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);411412const result = extractChatPromptReferences(resolved.prompt);413expect(resolved.prompt).toMatchSnapshot();414expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();415expect(result).toMatchSnapshot();416});417418test('includes contents of regular prompt files', async () => {419const promptFile = {420id: 'vscode.prompt.file__file:doit.prompt.md',421name: 'prompt:doit.prompt.md',422value: URI.file('doit.prompt.md')423};424createMockFile(promptFile.value, `This is a prompt file`);425426const req = new TestChatRequest('Process these files', [427promptFile428]);429430const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);431432const result = extractChatPromptReferences(resolved.prompt);433expect(resolved.prompt).toMatchSnapshot();434expect(fixFilePathsForTestComparison(resolved.attachments)).toMatchSnapshot();435expect(result).toMatchSnapshot();436});437438test('excludes instruction files from references and attachments', async () => {439const instructionFile = {440id: 'vscode.instructions.file__file:/workspace/my.instructions.md',441name: 'my.instructions.md',442value: URI.file('/workspace/my.instructions.md')443};444const regularFileRef = {445id: 'regular-file',446name: 'regular.ts',447value: URI.file('/workspace/regular.ts')448};449createMockFile(instructionFile.value, `# Instructions\nDo things this way.`);450createMockFile(regularFileRef.value, `const x = 1;`);451452const req = new TestChatRequest('Process these files', [453instructionFile,454regularFileRef455]);456457const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);458459// Instruction file should be excluded from references and attachments460const instructionRef = resolved.references.find(r => URI.isUri(r.value) && (r.value as URI).fsPath.includes('my.instructions.md'));461expect(instructionRef).toBeUndefined();462// Regular file reference should still be included463const regularRef = resolved.references.find(r => URI.isUri(r.value) && (r.value as URI).fsPath.includes('regular.ts'));464expect(regularRef).toBeDefined();465// Attachment for instruction file should not be present466const instructionAttachment = resolved.attachments.find(a => a.type === 'file' && a.path.includes('my.instructions.md'));467expect(instructionAttachment).toBeUndefined();468});469470test('excludes customizations index from references and attachments', async () => {471const customizationsIndex = {472id: 'vscode.customizations.index',473name: 'customizations',474value: URI.file('/workspace/.github/copilot-instructions.md')475};476const regularFileRef = {477id: 'regular-file',478name: 'regular.ts',479value: URI.file('/workspace/regular.ts')480};481createMockFile(customizationsIndex.value, `# Customizations\nSome instructions.`);482createMockFile(regularFileRef.value, `const x = 1;`);483484const req = new TestChatRequest('Process these files', [485customizationsIndex,486regularFileRef487]);488489const resolved = await resolver.resolvePrompt(req, undefined, [], workspaceInfo, [], CancellationToken.None);490491// Customizations index should be excluded from references and attachments492const customizationsRef = resolved.references.find(r => URI.isUri(r.value) && (r.value as URI).fsPath.includes('copilot-instructions.md'));493expect(customizationsRef).toBeUndefined();494// Regular file reference should still be included495const regularRef = resolved.references.find(r => URI.isUri(r.value) && (r.value as URI).fsPath.includes('regular.ts'));496expect(regularRef).toBeDefined();497// Attachment for customizations index should not be present498const customizationsAttachment = resolved.attachments.find(a => a.type === 'file' && a.path.includes('copilot-instructions.md'));499expect(customizationsAttachment).toBeUndefined();500});501502test('extract GitHub PR/Issues', async () => {503const result = extractChatPromptReferences(getPromptTextWithGithubIssuePR());504expect(result).toMatchSnapshot();505});506test('extract Git Commit', async () => {507const result = extractChatPromptReferences(getPromptTextWithGitCommit());508expect(result).toMatchSnapshot();509});510function createMockFile(uri: URI, text: string) {511const doc = createTextDocumentData(uri, text, 'plaintext', '\n').document;512workspaceService.textDocuments.push(doc);513if (workspaceService.getWorkspaceFolders().length === 0) {514workspaceService.getWorkspaceFolders().push(URI.file('/workspace'));515}516if (uri.scheme !== Schemas.untitled) {517fileSystem.mockFile(uri, text);518if (workspaceType === 'worktree') {519if (uri.fsPath.startsWith('/workspace')) {520fileSystem.mockFile(URI.file(uri.fsPath.replace('/workspace', '/worktree')), text);521} else if (uri.fsPath.startsWith('\\workspace')) {522fileSystem.mockFile(URI.file(uri.fsPath.replace('\\workspace', '\\worktree')), text);523}524}525}526}527});528});529});530531suite('multi-workspace with additionalWorkspaces', () => {532const disposables = new DisposableStore();533let fileSystem: MockFileSystemService;534let workspaceService: TestWorkspaceService;535let resolver: CopilotCLIPromptResolver;536537beforeEach(() => {538const services = createExtensionUnitTestingServices(disposables);539const accessor = disposables.add(services.createTestingAccessor());540fileSystem = accessor.get(IFileSystemService) as MockFileSystemService;541workspaceService = accessor.get(IWorkspaceService) as TestWorkspaceService;542const logService = accessor.get(ILogService);543const imageSupport = new class extends mock<CopilotCLIImageSupport>() {544override storeImage(_imageData: Uint8Array, _mimeType: string): Promise<URI> {545throw new Error('Method not implemented.');546}547};548workspaceService.getWorkspaceFolders().push(URI.file('/workspace'));549workspaceService.getWorkspaceFolders().push(URI.file('/workspace2'));550resolver = new CopilotCLIPromptResolver(imageSupport, logService, fileSystem, workspaceService, services.seal(), accessor.get(IIgnoreService), new MockSkillLocations(), new MockExtensionContext() as unknown as IVSCodeExtensionContext);551});552553afterEach(() => {554disposables.clear();555vi.resetAllMocks();556});557558test('translates file reference in additionalWorkspaces to its worktree path', async () => {559const fileUri = URI.file('/workspace2/src/main.ts');560const worktreeFileUri = URI.file('/worktree2/src/main.ts');561562fileSystem.mockFile(fileUri, 'const x = 1;');563fileSystem.mockFile(worktreeFileUri, 'const x = 1;');564565const primaryWorkspaceInfo: IWorkspaceInfo = {566folder: URI.file('/workspace'),567repository: undefined,568worktree: undefined,569worktreeProperties: undefined,570};571const additionalWorkspace: IWorkspaceInfo = {572folder: URI.file('/workspace2'),573repository: URI.file('/workspace2'),574worktree: URI.file('/worktree2'),575worktreeProperties: {576version: 2,577baseCommit: 'HEAD',578branchName: 'worktree2-branch',579repositoryPath: '/workspace2',580worktreePath: '/worktree2',581baseBranchName: 'main',582},583};584585const req = new TestChatRequest('explain file', [586{587id: fileUri.toString(),588name: 'file:main.ts',589value: fileUri,590},591]);592593const resolved = await resolver.resolvePrompt(req, undefined, [], primaryWorkspaceInfo, [additionalWorkspace], CancellationToken.None);594595// File reference should be translated to the worktree path of additionalWorkspace596const fileRef = resolved.references.find(r => URI.isUri(r.value));597expect((fileRef?.value as {}).toString()).toBe(worktreeFileUri.toString());598});599600test('falls back to original URI when worktree file does not exist for additionalWorkspaces', async () => {601const fileUri = URI.file('/workspace2/src/main.ts');602603fileSystem.mockFile(fileUri, 'const x = 1;');604// Note: worktree file is NOT mocked, so stat will fail605606const primaryWorkspaceInfo: IWorkspaceInfo = {607folder: URI.file('/workspace'),608repository: undefined,609worktree: undefined,610worktreeProperties: undefined,611};612const additionalWorkspace: IWorkspaceInfo = {613folder: URI.file('/workspace2'),614repository: URI.file('/workspace2'),615worktree: URI.file('/worktree2'),616worktreeProperties: {617version: 2,618baseCommit: 'HEAD',619branchName: 'worktree2-branch',620repositoryPath: '/workspace2',621worktreePath: '/worktree2',622baseBranchName: 'main',623},624};625626const req = new TestChatRequest('explain file', [627{628id: fileUri.toString(),629name: 'file:main.ts',630value: fileUri,631},632]);633634const resolved = await resolver.resolvePrompt(req, undefined, [], primaryWorkspaceInfo, [additionalWorkspace], CancellationToken.None);635636// File reference should remain at original URI since worktree file doesn't exist637const fileRef = resolved.references.find(r => URI.isUri(r.value));638expect((fileRef?.value as {}).toString()).toBe(fileUri.toString());639});640641test('uses findMatchingWorktree fallback when file is under repository but not in workspace service', async () => {642// Remove /workspace2 from workspace service so getWorkspaceFolder returns undefined643const folders = workspaceService.getWorkspaceFolders();644const idx = folders.findIndex(f => f.toString() === URI.file('/workspace2').toString());645if (idx >= 0) {646folders.splice(idx, 1);647}648649const fileUri = URI.file('/workspace2/src/main.ts');650const worktreeFileUri = URI.file('/worktree2/src/main.ts');651652fileSystem.mockFile(fileUri, 'const x = 1;');653fileSystem.mockFile(worktreeFileUri, 'const x = 1;');654655const primaryWorkspaceInfo: IWorkspaceInfo = {656folder: URI.file('/workspace'),657repository: undefined,658worktree: undefined,659worktreeProperties: undefined,660};661// additionalWorkspace has isolation enabled and its repository covers the file662const additionalWorkspace: IWorkspaceInfo = {663folder: URI.file('/workspace2'),664repository: URI.file('/workspace2'),665worktree: URI.file('/worktree2'),666worktreeProperties: {667version: 2,668baseCommit: 'HEAD',669branchName: 'worktree2-branch',670repositoryPath: '/workspace2',671worktreePath: '/worktree2',672baseBranchName: 'main',673},674};675676const req = new TestChatRequest('explain file', [677{678id: fileUri.toString(),679name: 'file:main.ts',680value: fileUri,681},682]);683684const resolved = await resolver.resolvePrompt(req, undefined, [], primaryWorkspaceInfo, [additionalWorkspace], CancellationToken.None);685686// findMatchingWorktree should map /workspace2/src/main.ts -> /worktree2/src/main.ts687const fileRef = resolved.references.find(r => URI.isUri(r.value));688expect((fileRef?.value as {}).toString()).toBe(worktreeFileUri.toString());689});690691test('falls back to original URI when findMatchingWorktree candidate does not exist', async () => {692// Remove /workspace2 from workspace service so getWorkspaceFolder returns undefined → triggers findMatchingWorktree693const folders = workspaceService.getWorkspaceFolders();694const idx = folders.findIndex(f => f.toString() === URI.file('/workspace2').toString());695if (idx >= 0) {696folders.splice(idx, 1);697}698699const fileUri = URI.file('/workspace2/src/main.ts');700fileSystem.mockFile(fileUri, 'const x = 1;');701// worktree file is NOT mocked → stat throws ENOENT → should fall back to original URI702703const primaryWorkspaceInfo: IWorkspaceInfo = {704folder: URI.file('/workspace'),705repository: undefined,706worktree: undefined,707worktreeProperties: undefined,708};709const additionalWorkspace: IWorkspaceInfo = {710folder: URI.file('/workspace2'),711repository: URI.file('/workspace2'),712worktree: URI.file('/worktree2'),713worktreeProperties: {714version: 2,715baseCommit: 'HEAD',716branchName: 'worktree2-branch',717repositoryPath: '/workspace2',718worktreePath: '/worktree2',719baseBranchName: 'main',720},721};722723const req = new TestChatRequest('explain file', [724{725id: fileUri.toString(),726name: 'file:main.ts',727value: fileUri,728},729]);730731const resolved = await resolver.resolvePrompt(req, undefined, [], primaryWorkspaceInfo, [additionalWorkspace], CancellationToken.None);732733// findMatchingWorktree candidate stat fails → original URI returned unchanged734const fileRef = resolved.references.find(r => URI.isUri(r.value));735expect((fileRef?.value as {}).toString()).toBe(fileUri.toString());736});737738test('does not translate URIs when isolation is not enabled in any workspace', async () => {739const fileUri = URI.file('/workspace2/src/main.ts');740fileSystem.mockFile(fileUri, 'const x = 1;');741742const primaryWorkspaceInfo: IWorkspaceInfo = {743folder: URI.file('/workspace'),744repository: undefined,745worktree: undefined,746worktreeProperties: undefined,747};748// additionalWorkspace has NO isolation749const additionalWorkspace: IWorkspaceInfo = {750folder: URI.file('/workspace2'),751repository: URI.file('/workspace2'),752worktree: undefined,753worktreeProperties: undefined,754};755756const req = new TestChatRequest('explain file', [757{758id: fileUri.toString(),759name: 'file:main.ts',760value: fileUri,761},762]);763764const resolved = await resolver.resolvePrompt(req, undefined, [], primaryWorkspaceInfo, [additionalWorkspace], CancellationToken.None);765766// No translation should occur767const fileRef = resolved.references.find(r => URI.isUri(r.value));768expect((fileRef?.value as {}).toString()).toBe(fileUri.toString());769});770});771772function createWorkspaceInfo(workspaceType: 'emptyWorkspace' | 'workspace' | 'worktree'): IWorkspaceInfo {773if (workspaceType === 'workspace') {774return {775...emptyWorkspaceInfo(),776folder: URI.file('/workspace'),777};778}779780if (workspaceType === 'worktree') {781return {782...emptyWorkspaceInfo(),783folder: URI.file('/workspace'),784repository: URI.file('/workspace'),785worktree: URI.file('/worktree'),786worktreeProperties: {787version: 2,788baseCommit: 'HEAD',789branchName: 'worktree-branch',790repositoryPath: '/workspace',791worktreePath: '/worktree',792baseBranchName: 'main',793},794};795}796797return emptyWorkspaceInfo();798}799800/**801* As we want test to run on all platforms, we need to fix file paths in attachments802* to use forward slashes for comparison.803*/804function fixFilePathsForTestComparison(attachments: Attachment[]): Attachment[] {805attachments.forEach(attachment => {806if (attachment.type === 'file') {807attachment.path = attachment.path.replace(/\\/g, '/');808} else if (attachment.type === 'directory') {809attachment.path = attachment.path.replace(/\\/g, '/');810} else if (attachment.type === 'selection') {811attachment.filePath = attachment.filePath.replace(/\\/g, '/');812}813});814return attachments;815}816817818function getPromptTextWithGithubIssuePR() {819return `820'Explain this821<reminder>822IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task823</reminder>824<attachments>825<attachment id="#17143 Kernel interrupt_mode \\"message\\" sends interrupt_request on shell channel instead of control channel">826{"issueNumber":17143,"owner":"microsoft","repo":"vscode-jupyter","title":"Kernel interrupt_mode \\"message\\" sends interrupt_request on shell channel instead of control channel","body":"### Applies To\\n\\n- [x] Notebooks (.ipynb files)\\n- [ ] Interactive Window and\\\\/or Cell Scripts (.py files with \\\\#%% markers)\\n\\n### What happened?\\n\\n **Description**\\n\\n When a kernel has interrupt_mode set to \'message\', the extension incorrectly sends the interrupt_request message on the shell channel instead of the control channel as specified by the Jupyter protocol.\\n\\n **Expected Behavior**\\n\\n According to the Jupyter [kernel spec documentation](https://github.com/microsoft/vscode-jupyter/blob/4efd36fb61d83bf0c99008648ca633ad688d4ab9/src/kernels/raw/session/rawKernelConnection.node.ts#L361-L370):\\n\\n> In case a kernel can not catch operating system interrupt signals (e.g. the used runtime handles signals and does not allow a user program to define a callback), a kernel can choose to be notified using a message instead. For this to work, the kernels kernelspec must set \`interrupt_mode\` to \`message\`. An interruption will then result in the following message on the \`control\` channel:\\n\\n The interrupt request should:\\n 1. Be created with channel: \'control\'\\n 2. Be sent via sendControlMessage()\\n\\n **Actual Behavior**\\n\\n The current implementation ([rawKernelConnection.node.ts](https://github.com/microsoft/vscode-jupyter/blob/4efd36fb61d83bf0c99008648ca633ad688d4ab9/src/kernels/raw/session/rawKernelConnection.node.ts#L361-L370)):\\n 1. Creates message with channel: \'shell\'\\n 2. Sends via sendShellMessage()\\n\\n This causes two problems:\\n 1. The interrupt is queued behind other shell messages - The shell channel processes messages sequentially, so if there are pending execution requests or other shell messages, the interrupt will wait in the queue instead of being processed immediately. This defeats the purpose of interrupting, as the kernel continues executing while the interrupt waits.\\n 2. Interrupt may fail entirely - Kernels expect interrupt_request on the control channel and may ignore or reject it on the shell channel.\\n\\n### VS Code Version\\n\\n1.105.1\\n\\n### Jupyter Extension Version\\n\\n2025.9.1\\n\\n### Jupyter logs\\n\\n\`\`\`shell\\n\\n\`\`\`\\n\\n### Coding Language and Runtime Version\\n\\n_No response_\\n\\n### Language Extension Version (if applicable)\\n\\n_No response_\\n\\n### Anaconda Version (if applicable)\\n\\n_No response_\\n\\n### Running Jupyter locally or remotely?\\n\\nNone","comments":[]}827</attachment>828<attachment id="#17131 Bump ipywidgets from 7.7.2 to 8.1.8">829{"prNumber":17131,"owner":"microsoft","repo":"vscode-jupyter","title":"Bump ipywidgets from 7.7.2 to 8.1.8","body":"Bumps [ipywidgets](https://github.com/jupyter-widgets/ipywidgets) from 7.7.2 to 8.1.8.\\n<details>\\n<summary>Release notes</summary>\\n<p><em>Sourced from <a href=\\"https://github.com/jupyter-widgets/ipywidgets/releases\\">ipywidgets\'s releases</a>.</em></p>\\n<blockquote>\\n<h2>8.1.8</h2>\\n<h2>What\'s Changed</h2>\\n<ul>\\n<li>Add JupyterCon banner and jupyter colors by <a href=\\"https://github.com/choldgraf\\"><code>@choldgraf</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3998\\">jupyter-widgets/ipywidgets#3998</a></li>\\n<li>Fix badge formatting in README.md by <a href=\\"https://github.com/Carreau\\"><code>@Carreau</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/4000\\">jupyter-widgets/ipywidgets#4000</a></li>\\n<li>Add Plausible web stats by <a href=\\"https://github.com/jasongrout\\"><code>@jasongrout</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/4003\\">jupyter-widgets/ipywidgets#4003</a></li>\\n<li>Update jupyterlab_widgets metadata to indicate it works with JupyterLab 4 by <a href=\\"https://github.com/jasongrout\\"><code>@jasongrout</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/4004\\">jupyter-widgets/ipywidgets#4004</a></li>\\n</ul>\\n<h2>New Contributors</h2>\\n<ul>\\n<li><a href=\\"https://github.com/choldgraf\\"><code>@choldgraf</code></a> made their first contribution in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3998\\">jupyter-widgets/ipywidgets#3998</a></li>\\n</ul>\\n<p><strong>Full Changelog</strong>: <a href=\\"https://github.com/jupyter-widgets/ipywidgets/compare/8.1.7...8.1.8\\">https://github.com/jupyter-widgets/ipywidgets/compare/8.1.7...8.1.8</a></p>\\n<h2>8.1.7</h2>\\n<h2>What\'s Changed</h2>\\n<ul>\\n<li>Fix CI + remove Python 3.8 by <a href=\\"https://github.com/martinRenou\\"><code>@martinRenou</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3989\\">jupyter-widgets/ipywidgets#3989</a></li>\\n<li>Dynamic widgets registry by <a href=\\"https://github.com/martinRenou\\"><code>@martinRenou</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3988\\">jupyter-widgets/ipywidgets#3988</a></li>\\n</ul>\\n<p><strong>Full Changelog</strong>: <a href=\\"https://github.com/jupyter-widgets/ipywidgets/compare/8.1.6...8.1.7\\">https://github.com/jupyter-widgets/ipywidgets/compare/8.1.6...8.1.7</a></p>\\n<h2>8.1.6</h2>\\n<h2>What\'s Changed</h2>\\n<ul>\\n<li>Fix lumino and lab packages pinning by <a href=\\"https://github.com/martinRenou\\"><code>@martinRenou</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3958\\">jupyter-widgets/ipywidgets#3958</a></li>\\n<li>Typo fix by <a href=\\"https://github.com/david4096\\"><code>@david4096</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3960\\">jupyter-widgets/ipywidgets#3960</a></li>\\n<li>Update lables even without MatJax/TypeSetter by <a href=\\"https://github.com/DonJayamanne\\"><code>@DonJayamanne</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3962\\">jupyter-widgets/ipywidgets#3962</a></li>\\n<li>Update github actions and fix readthedocs by <a href=\\"https://github.com/brichet\\"><code>@brichet</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3983\\">jupyter-widgets/ipywidgets#3983</a></li>\\n<li>Fix the new line when pressing enter in textarea widget by <a href=\\"https://github.com/brichet\\"><code>@brichet</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3982\\">jupyter-widgets/ipywidgets#3982</a></li>\\n<li>Backward compatibility on processPhosphorMessage by <a href=\\"https://github.com/martinRenou\\"><code>@martinRenou</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3945\\">jupyter-widgets/ipywidgets#3945</a></li>\\n<li>Include sourcemaps in npm tarballs by <a href=\\"https://github.com/manzt\\"><code>@manzt</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3978\\">jupyter-widgets/ipywidgets#3978</a></li>\\n<li>Fix deprecation warning when importing the backend_inline by <a href=\\"https://github.com/emolinlu\\"><code>@emolinlu</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3984\\">jupyter-widgets/ipywidgets#3984</a></li>\\n</ul>\\n<h2>New Contributors</h2>\\n<ul>\\n<li><a href=\\"https://github.com/david4096\\"><code>@david4096</code></a> made their first contribution in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3960\\">jupyter-widgets/ipywidgets#3960</a></li>\\n<li><a href=\\"https://github.com/brichet\\"><code>@brichet</code></a> made their first contribution in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3983\\">jupyter-widgets/ipywidgets#3983</a></li>\\n<li><a href=\\"https://github.com/emolinlu\\"><code>@emolinlu</code></a> made their first contribution in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3984\\">jupyter-widgets/ipywidgets#3984</a></li>\\n</ul>\\n<p><strong>Full Changelog</strong>: <a href=\\"https://github.com/jupyter-widgets/ipywidgets/compare/8.1.5...8.1.6\\">https://github.com/jupyter-widgets/ipywidgets/compare/8.1.5...8.1.6</a></p>\\n<h2>8.1.5</h2>\\n<h2>What\'s Changed</h2>\\n<ul>\\n<li>More Phosphor backward compatibility by <a href=\\"https://github.com/martinRenou\\"><code>@martinRenou</code></a> in <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/pull/3942\\">jupyter-widgets/ipywidgets#3942</a></li>\\n</ul>\\n<p><strong>Full Changelog</strong>: <a href=\\"https://github.com/jupyter-widgets/ipywidgets/compare/8.1.4...8.1.5\\">https://github.com/jupyter-widgets/ipywidgets/compare/8.1.4...8.1.5</a></p>\\n<h2>8.1.4</h2>\\n<h2>What\'s Changed</h2>\\n<h3>New features</h3>\\n<!-- raw HTML omitted -->\\n</blockquote>\\n<p>... (truncated)</p>\\n</details>\\n<details>\\n<summary>Commits</summary>\\n<ul>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/3171b1c746643a3893987190dc505661c5562877\\"><code>3171b1c</code></a> Update Output Widget.ipynb (<a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/issues/3881\\">#3881</a>)</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/cd817839ab8b6ef80c8e2b7a94c8f1df1de29734\\"><code>cd81783</code></a> update image processing example notebok imports and function call (<a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/issues/3896\\">#3896</a>)</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/cecd2b0d0314a92b71dce364e3db7a06af8cf64a\\"><code>cecd2b0</code></a> specify Jupyterlab (version 3.x or above) (<a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/issues/3880\\">#3880</a>)</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/0aa1efb563edeb3564f5738dfbee630fd6e4ed6f\\"><code>0aa1efb</code></a> Allow <code>interact</code> to use basic type hint annotations (<a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/issues/3908\\">#3908</a>)</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/2e15cfc030b8f6c319114be23b4f95efb537fd4d\\"><code>2e15cfc</code></a> Update Widget List.ipynb</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/06ed868181a3192067ffcff0ed94815f72a1f7bf\\"><code>06ed868</code></a> Merge pull request <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/issues/3793\\">#3793</a> from ferdnyc/mappings-work-again</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/31259ca8ba33c44a29ba8ffede9de0eece61fb44\\"><code>31259ca</code></a> Merge pull request <a href=\\"https://redirect.github.com/jupyter-widgets/ipywidgets/issues/3801\\">#3801</a> from warrickball/patch-2</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/dd250bfacd875561ad05f692d39c41f350a56b42\\"><code>dd250bf</code></a> Handle Notebook 7 in dev install script</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/a1282ec692b35d91e0b3062016962634c7a8012e\\"><code>a1282ec</code></a> Fix link to "Output widget examples"</li>\\n<li><a href=\\"https://github.com/jupyter-widgets/ipywidgets/commit/b6b3051e0b89c1086ea79327d3e957af7da957fd\\"><code>b6b3051</code></a> Revert "Add note on removal of mapping types in documentation"</li>\\n<li>Additional commits viewable in <a href=\\"https://github.com/jupyter-widgets/ipywidgets/compare/7.7.2...8.1.8\\">compare view</a></li>\\n</ul>\\n</details>\\n<br />\\n\\n\\n[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)\\n\\nDependabot will resolve any conflicts with this PR as long as you don\'t alter it yourself. You can also trigger a rebase manually by commenting \`@dependabot rebase\`.\\n\\n[//]: # (dependabot-automerge-start)\\n[//]: # (dependabot-automerge-end)\\n\\n---\\n\\n<details>\\n<summary>Dependabot commands and options</summary>\\n<br />\\n\\nYou can trigger Dependabot actions by commenting on this PR:\\n- \`@dependabot rebase\` will rebase this PR\\n- \`@dependabot recreate\` will recreate this PR, overwriting any edits that have been made to it\\n- \`@dependabot merge\` will merge this PR after your CI passes on it\\n- \`@dependabot squash and merge\` will squash and merge this PR after your CI passes on it\\n- \`@dependabot cancel merge\` will cancel a previously requested merge and block automerging\\n- \`@dependabot reopen\` will reopen this PR if it is closed\\n- \`@dependabot close\` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually\\n- \`@dependabot show <dependency name> ignore conditions\` will show all of the ignore conditions of the specified dependency\\n- \`@dependabot ignore this major version\` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)\\n- \`@dependabot ignore this minor version\` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)\\n- \`@dependabot ignore this dependency\` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)\\n\\n\\n</details>","comments":[],"threads":[],"changes":["@@ -5,7 +5,7 @@ pandas\\n jupyter\\n # List of requirements for conda environments that cannot be installed using conda\\n # Pinned per ipywidget 8 support: https://github.com/microsoft/vscode-jupyter/issues/11598\\n-ipywidgets==7.7.2\\n+ipywidgets==8.1.8\\n anywidget\\n matplotlib\\n ipympl"]}830</attachment>831<attachment id="microsoft/vscode-jupyter">832Information about the current repository. You can use this information when you need to calculate diffs or compare changes with the default branch:833Repository name: vscode-jupyter834Owner: microsoft835Current branch: don/well-landfowl836Default branch: main837Active pull request (may not be the same as open pull request): Skip failing tests https://github.com/microsoft/vscode-jupyter/pull/17155838</attachment>839840</attachments>841<userRequest>842Explain this (See <attachments> above for file contents. You may not need to search or read the file again.)843</userRequest>`;844}845846function getPromptTextWithGitCommit() {847return `848Explain this849<reminder>850IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task851</reminder>852<attachments>853<attachment id="microsoft/vscode-jupyter">854Information about the current repository. You can use this information when you need to calculate diffs or compare changes with the default branch:855Repository name: vscode-jupyter856Owner: microsoft857Current branch: don/well-landfowl858Default branch: main859Active pull request (may not be the same as open pull request): Skip failing tests https://github.com/microsoft/vscode-jupyter/pull/17155860</attachment>861<attachment id="$(repo) select-impala $(git-commit) 4efd36f" filePath="scm-history-item:/Users/donjayamanne/Development/vsc/vscode-jupyter.worktrees/select-impala?%7B%22repositoryId%22%3A%22scm10%22%2C%22historyItemId%22%3A%224efd36fb61d83bf0c99008648ca633ad688d4ab9%22%2C%22historyItemParentId%22%3A%22b67ca34030530fdb75e326293e2c023d59f24fc8%22%2C%22historyItemDisplayId%22%3A%224efd36f%22%7D">862commit 4efd36fb61d83bf0c99008648ca633ad688d4ab9863Author: Don Jayamanne <[email protected]>864Date: Fri Oct 10 18:58:40 2025 +1100865866Fix identification of self signed certs (#17049)867868diff --git a/src/platform/errors/jupyterSelfCertsError.ts b/src/platform/errors/jupyterSelfCertsError.ts869index f30ade6977b4..b564ebfafc09 100644870--- a/src/platform/errors/jupyterSelfCertsError.ts871+++ b/src/platform/errors/jupyterSelfCertsError.ts872@@ -18,14 +18,19 @@ export class JupyterSelfCertsError extends BaseError {873}874public static isSelfCertsError(err: unknown) {875const message = (err as undefined | { message: string })?.message ?? \'\';876+ const name = (err as undefined | { name: string })?.name ?? \'\';877+ const messageToCheck = "";878return (879- message.indexOf(\'reason: self signed certificate\') >= 0 ||880+ messageToCheck.indexOf(\'reason: self signed certificate\') >= 0 ||881// https://github.com/microsoft/vscode-jupyter-hub/issues/36#issuecomment-1854097594882- message.indexOf(\'reason: unable to verify the first certificate\') >= 0 ||883+ messageToCheck.indexOf(\'reason: unable to verify the first certificate\') >= 0 ||884// https://github.com/microsoft/vscode-jupyter-hub/issues/36#issuecomment-1761234981885- message.indexOf(\'reason: unable to get issuer certificate\') >= 0 ||886+ messageToCheck.indexOf(\'reason: unable to get issuer certificate\') >= 0 ||887// https://github.com/microsoft/vscode-jupyter/issues/7558#issuecomment-993054968888- message.indexOf("is not in the cert\'s list") >= 0889+ messageToCheck.indexOf("is not in the cert\'s list") >= 0 ||890+ // https://github.com/microsoft/vscode-jupyter/issues/16522891+ messageToCheck.indexOf(\'unable to verify the first certificate\') >= 0 ||892+ messageToCheck.indexOf(\'UNABLE_TO_GET_ISSUER_CERT\') >= 0893);894}895}896</attachment>897898</attachments>899<userRequest>900Explain this (See <attachments> above for file contents. You may not need to search or read the file again.)901</userRequest>`;902}903904905