Path: blob/main/extensions/copilot/test/inline/multiFileEdit.stest.ts
13388 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*--------------------------------------------------------------------------------------------*/4import * as assert from 'assert';5import { CHAT_MODEL } from '../../src/platform/configuration/common/configurationService';6import { TestingServiceCollection } from '../../src/platform/test/node/services';7import { escapeRegExpCharacters } from '../../src/util/vs/base/common/strings';8import { URI } from '../../src/util/vs/base/common/uri';9import { Configuration, ssuite, stest } from '../base/stest';10import { assertContainsAllSnippets, assertCriteriaMetAsync, assertFileContent, assertJSON, assertNoElidedCodeComments, getFileContent, getWorkspaceDiagnostics } from '../simulation/outcomeValidators';11import { EditTestStrategyPanel, simulatePanelCodeMapper } from '../simulation/panelCodeMapperSimulator';12import { assertInlineEdit, assertInlineEditShape, assertNoErrorOutcome, assertQualifiedFile, assertWorkspaceEdit, fromFixture, toFile } from '../simulation/stestUtil';13import { EditTestStrategy, IScenario } from '../simulation/types';1415function executeEditTest(16strategy: EditTestStrategyPanel,17testingServiceCollection: TestingServiceCollection,18scenario: IScenario19): Promise<void> {20return simulatePanelCodeMapper(testingServiceCollection, scenario, strategy);21}2223function forEditsAndAgent(callback: (strategy: EditTestStrategyPanel, variant: string | undefined, model: string | undefined, configurations: Configuration<any>[] | undefined) => void): void {24callback(EditTestStrategy.Edits, '', undefined, undefined);25callback(EditTestStrategy.Edits, '-claude', CHAT_MODEL.CLAUDE_SONNET, undefined);26// callback(EditTestStrategy.Agent, '-agent', undefined);27}2829forEditsAndAgent((strategy, variant, model, configurations) => {30ssuite({ title: `multifile-edit${variant}`, location: 'panel', configurations }, () => {31stest({ description: 'issue #8098: extract function to unseen file', language: 'typescript', model }, (testingServiceCollection) => {32return executeEditTest(strategy, testingServiceCollection, {33files: [34fromFixture('multiFileEdit/issue-8098/debugUtils.ts'),35fromFixture('multiFileEdit/issue-8098/debugTelemetry.ts'),36],37queries: [38{39file: 'debugUtils.ts',40selection: [34, 0, 34, 0],41visibleRanges: [[3, 0, 44, 0]],42query: 'Extract filterExceptionsFromTelemetry to debugTelemetry #file:debugUtils.ts',43validate: async (outcome, workspace, accessor) => {44assertWorkspaceEdit(outcome);45assert.ok(outcome.files.length === 2, 'Expected two files to be edited');4647const utilsTs = assertFileContent(outcome.files, 'debugUtils.ts');48assert.ok(!utilsTs.includes('function filterExceptionsFromTelemetry'), 'Expected filterExceptionsFromTelemetry to be extracted');49const telemetryFile = assertFileContent(outcome.files, 'debugTelemetry.ts');50assert.ok(telemetryFile.includes('filterExceptionsFromTelemetry'), 'Expected filterExceptionsFromTelemetry to be extracted');5152assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);53assertNoElidedCodeComments(outcome);54}55}56]57});58});5960stest({ description: 'issue #8131: properly using dotenv in this file', model }, (testingServiceCollection) => {61return executeEditTest(strategy, testingServiceCollection, {62files: [63fromFixture('multiFileEdit/issue-8131/extension.ts'),64],65queries: [66{67file: 'extension.ts',68selection: [29, 20, 29, 20],69visibleRanges: [[18, 0, 46, 0]],70query: '#file:extension.ts Am I properly using dotenv in this file. The process.env.OPENAI_API_KEY keeps being undefined',71validate: async (outcome, workspace, accessor) => {72// TODO@add a good validation function here73assert.fail('not implemented');74}75}76]77});78});7980stest({ description: 'import new helper function', language: 'typescript', model }, (testingServiceCollection) => {81return executeEditTest(strategy, testingServiceCollection, {82files: [83fromFixture('multiFileEdit/fibonacci/version1.ts'),84fromFixture('multiFileEdit/fibonacci/version2.ts'),85fromFixture('multiFileEdit/fibonacci/foo.ts'),86fromFixture('multiFileEdit/fibonacci/bar.ts'),87],88queries: [89{90file: 'foo.ts',91selection: [0, 0, 4, 0],92visibleRanges: [[9, 0, 4, 0]],93query: 'Update #file:foo.ts and #file:bar.ts to use the fibonacci function from #file:version2.ts instead of #file:version1.ts',94validate: async (outcome, workspace, accessor) => {95assertWorkspaceEdit(outcome);96assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).length, 0);97assert.strictEqual(outcome.files.length, 2, 'Expected two files to be edited');98for (const file of outcome.files) {99const content = getFileContent(file);100assert.ok(content.includes('./version2'), 'Expected file to include updated import');101assert.ok(!content.includes('./version1'), 'Expected file to not include original import');102assertNoElidedCodeComments(content);103}104}105}106]107});108});109110stest({ description: 'change library used by two files', language: 'typescript', model }, (testingServiceCollection) => {111return executeEditTest(strategy, testingServiceCollection, {112files: [113fromFixture('multiFileEdit/filepaths/1.ts'),114fromFixture('multiFileEdit/filepaths/2.ts'),115],116queries: [117{118file: '1.ts',119selection: [0, 0, 26, 0],120visibleRanges: [[0, 0, 26, 0]],121query: 'Update #file:1.ts and #file:2.ts to replace usage of "path" with vscode apis',122validate: async (outcome, workspace, accessor) => {123assertWorkspaceEdit(outcome);124assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);125assert.strictEqual(outcome.files.length, 2, 'Expected two files to be edited');126for (const file of outcome.files) {127const content = getFileContent(file);128assert.ok(!content.includes('path.join'), 'Expected file to not include path usage');129assert.ok(!content.includes('path.relative'), 'Expected file to not include path usage');130assertNoElidedCodeComments(content);131}132}133}134]135});136});137138stest({ description: 'add validation logic to three files', language: 'typescript', model }, (testingServiceCollection) => {139return executeEditTest(strategy, testingServiceCollection, {140files: [141fromFixture('multiFileEdit/filepaths/1.ts'),142fromFixture('multiFileEdit/filepaths/2.ts'),143fromFixture('multiFileEdit/filepaths/3.ts'),144],145queries: [146{147file: '1.ts',148selection: [0, 0, 26, 0],149visibleRanges: [[0, 0, 26, 0]],150query: 'Throw an error if we see a "http" uri #file:1.ts #file:2.ts #file:3.ts',151validate: async (outcome, workspace, accessor) => {152assertWorkspaceEdit(outcome);153assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);154assert.strictEqual(outcome.files.length, 3, 'Expected three files to be edited');155for (const file of outcome.files) {156const content = getFileContent(file);157assert.ok(content.includes('throw new Error'), 'Expected file to not include original import');158assertNoElidedCodeComments(content);159}160}161}162]163});164});165166stest({ description: 'does not delete code (big file) #15475', language: 'typescript' }, (testingServiceCollection) => {167return executeEditTest(strategy, testingServiceCollection, {168files: [fromFixture('codeMapper/notebookEditorWidget.ts')],169queries: [170{171file: 'notebookEditorWidget.ts',172selection: [497, 0, 501, 0],173visibleRanges: [[480, 0, 520, 0]],174query: 'add return types for getSelections',175validate: async (outcome, workspace, accessor) => {176assertInlineEdit(outcome);177const edit = assertInlineEditShape(outcome, {178line: 497,179originalLength: 2957,180modifiedLength: 2957,181});182assertContainsAllSnippets(edit.changedModifiedLines.join('\n'), ['getSelections(): ICellRange[] {'], 'Edit not applied');183assert.deepStrictEqual(184edit.changedModifiedLines.join('\n'),185`getSelections(): ICellRange[] {`,186'Unrelated edits applied'187);188assertNoElidedCodeComments(outcome.fileContents);189}190}191]192});193});194195stest({ description: 'add a command and dependency to a VS Code extension', language: 'typescript', model }, (testingServiceCollection) => {196return executeEditTest(strategy, testingServiceCollection, {197files: [198fromFixture('multiFileEdit/asciiart/package.json'),199fromFixture('multiFileEdit/asciiart/src/extension.ts'),200],201queries: [202{203query: [204`In #file:extension.ts add a new command 'Hello ASCII World' which shows an information dialog that displays 'hello world' as ASCII art.`,205`You can use the 'ascii-art' node module to generate the string.`,206`Please also update #file:package.json with the new command and the new dependency.`207].join(' '),208validate: async (outcome, workspace, accessor) => {209assertWorkspaceEdit(outcome);210assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);211assert.strictEqual(outcome.files.length, 2, 'Expected two files to be edited');212const packageJson = assertFileContent(outcome.files, 'package.json');213const extensionTs = assertFileContent(outcome.files, 'extension.ts');214const packageJsonObj = assertJSON(packageJson);215assert.ok(!packageJsonObj.devDependencies?.['ascii-art'], 'ascii-art dependency was added to devDependencies');216assert.ok(packageJsonObj.dependencies?.['ascii-art'], 'Expected package.json to include ascii-art dependency');217const commands = packageJsonObj.contributes.commands;218assert.ok(Array.isArray(commands), 'Expected package.json to include a commands array');219assert.ok(commands.length === 2, 'Expected package.json to include a new command');220const newCommand = commands.find((c: { command: string }) => c.command !== 'test-multifile-1.helloWorld');221assert.ok(newCommand, 'Expected package.json to include a command other than helloWorld');222assert.ok(extensionTs.match(/\bimport\b[^;\n]+from ['"]ascii-art['"]/), 'Expected an import for ascii-art');223assert.ok(extensionTs.match(new RegExp(`\\bregisterCommand\\b[^;\n]['"]${escapeRegExpCharacters(newCommand.command)}['"]`)), 'expected that the new command is registered');224assertNoElidedCodeComments(outcome);225}226},227{228query: [229`Better use figlet for creating the ascii art. Please remove the dependency on 'ascii-art'.`,230].join(' '),231validate: async (outcome, workspace, accessor) => {232assertWorkspaceEdit(outcome);233assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);234assert.strictEqual(outcome.files.length, 2, 'Expected two files to be edited');235const packageJson = assertFileContent(outcome.files, 'package.json');236const extensionTs = assertFileContent(outcome.files, 'extension.ts');237const packageJsonObj = assertJSON(packageJson);238assert.ok(packageJsonObj.dependencies['figlet'], 'Expected package.json to include figlet dependency');239assert.ok(!packageJsonObj.dependencies['ascii-art'], 'Expected package.json no longer to contain the ascii-art dependency');240const commands = packageJsonObj.contributes.commands;241assert.ok(Array.isArray(commands), 'Expected package.json to include a commands array');242assert.ok(commands.length === 2, 'Expected package.json to still include 2 command');243const newCommand = commands.find((c: { command: string }) => c.command !== 'test-multifile-1.helloWorld');244assert.ok(newCommand, 'Expected package.json to include a command other than helloWorld');245assert.ok(extensionTs.match(/\bimport\b[^;\n]+from ['"]figlet['"]/), 'Expected an import for figlet');246assertNoElidedCodeComments(outcome);247}248}249]250});251});252253stest.skip({ description: 'Issue #8336', model }, (testingServiceCollection) => {254return executeEditTest(strategy, testingServiceCollection, {255files: [256toFile({ fileName: 'roadmap-parser.ts', fileContents: `export interface ParseOptions {\n startLine?: string,\n endLine?: string\n};\n\nexport interface FilterOptions {\n markers?: string[],\n extractMatchingMarkers?: boolean\n};\n\nexport function filter(markdown: string, options?: ParseOptions & FilterOptions): string {\n return parse(markdown, options).filter(options).join();\n}\n\n\nexport class Marker {\n constructor(public label: string, public offset: number) { }\n\n public get length() {\n return this.label.length + 1;\n }\n}\n\nexport class Line {\n\n public static nonHeaderLevel(computedLevel: number): number {\n return computedLevel + 1000;\n }\n\n public static headerLevel(computedLevel: number) : number {\n return computedLevel;\n }\n\n\n public children: Line[] = [];\n public parent: Line | null = null;\n public isHeader: boolean = false;\n public level: number = Line.nonHeaderLevel(0);\n public markers: Marker[] = [];\n\n constructor(public markdown: string) {\n let count = -1;\n let iterator = {\n next: () => count === this.markdown.length ? undefined : this.markdown.charAt(++count),\n index: () => count\n }\n this.parse(iterator);\n };\n\n parse(i: { next: () => string | undefined, index: () => number }) {\n let c = i.next();\n\n // eat headers\n let hashes = 0;\n while ('#' === c) {\n hashes++;\n c = i.next();\n }\n if (hashes > 0) {\n this.isHeader = true;\n this.level = Line.headerLevel(hashes);\n return;\n }\n\n // eat spaces\n let spaces = 0;\n while (' ' === c) {\n spaces++;\n c = i.next();\n }\n\n if ('-' === c) {\n // isBullet === true, remember indentation\n this.level = Line.nonHeaderLevel(Math.floor(spaces / 3));\n c = i.next();\n }\n\n // skip spaces\n while (' ' === c) {\n c = i.next();\n }\n\n // skip check mark\n if ('[' === c) {\n c = i.next();\n while (']' === c || ' ' === c || 'x' === c || 'X' === c ) {\n c = i.next();\n }\n }\n\n // eat markers\n markers: while (':' === c) {\n const offset = i.index();\n const label = [];\n c = i.next();\n while (':' !== c) {\n label.push(c);\n c = i.next();\n if (c === undefined) {\n break markers;\n }\n }\n this.markers.push(new Marker(label.join(''), offset));\n c = i.next();\n while (' ' === c) {\n c = i.next();\n }\n }\n\n }\n\n add(line: Line): Line {\n this.children.push(line);\n return this;\n }\n\n matchesMarkers(markers: string[]): boolean {\n if (this.hasMarkers(markers)) {\n return true;\n }\n return this.children.some(c => c.matchesMarkers(markers));\n }\n\n hasMarkers(markers: string[]): boolean {\n return this.markers.filter(marker => markers.includes(marker.label)).length === markers.length;\n }\n\n sanitizedMarkdown(options: FilterOptions) {\n if (options.extractMatchingMarkers) {\n const markers = this.markers.sort((m1, m2) => m1.offset - m2.offset);\n let sanitized = '';\n let offset = 0;\n markers.forEach(m => {\n if (options.markers?.includes(m.label)) {\n let behindMarker = m.offset + m.length + 1;\n let c = this.markdown.charAt(behindMarker);\n while (c === ' ') {\n c = this.markdown.charAt(++behindMarker);\n }\n sanitized += this.markdown.substring(offset, behindMarker);\n offset = behindMarker;\n }\n });\n sanitized += this.markdown.substring(offset);\n return sanitized;\n }\n const markers = this.markers.sort((m1, m2) => m1.offset - m2.offset);\n let sanitized = '';\n let offset = 0;\n markers.forEach(m => {\n if (options.markers?.includes(m.label)) {\n let behindMarker = m.offset + m.length + 1;\n let c = this.markdown.charAt(behindMarker);\n while (c === ' ') {\n c = this.markdown.charAt(++behindMarker);\n }\n sanitized += this.markdown.substring(offset, m.offset);\n offset = behindMarker;\n }\n });\n sanitized += this.markdown.substring(offset);\n return sanitized;\n }\n return this.markdown;\n }\n\n isEmpty() : boolean {\n return this.markdown.length === 0;\n }\n}\n\nexport class LineTree {\n\n public lines: Line[] = []\n private lastAddition: Line | null = null;\n\n public add(line: Line) {\n if (this.lastAddition) {\n if (line.level > this.lastAddition.level) {\n line.parent = this.lastAddition;\n line.parent.add(line);\n } else if (line.level === this.lastAddition.level) {\n line.parent = this.lastAddition.parent;\n if (line.parent) {\n line.parent.add(line);\n } else {\n this.lines.push(line);\n }\n } else {\n let last: Line | null = this.lastAddition;\n while (last && line.level <= last.level) {\n last = last.parent;\n }\n if (last) {\n line.parent = last;\n line.parent.add(line);\n } else {\n this.lines.push(line);\n }\n }\n } else {\n this.lines.push(line);\n }\n this.lastAddition = line;\n }\n\n public filter(options?: FilterOptions): LineTree {\n if (options && options.markers) {\n const filteredTree = new LineTree();\n const filter = (lines: Line[]) => {\n lines.forEach(l => {\n if (l.matchesMarkers(options.markers!)) {\n filteredTree.add(l);\n }\n filter(l.children);\n });\n };\n filter(this.lines);\n return filteredTree;\n }\n return this;\n }\n\n public join(): string {\n const contents: string[] = [];\n const join = (lines: Line[]) => {\n lines.forEach(l => {\n contents.push(l.markdown);\n join(l.children);\n });\n };\n join(this.lines);\n return contents.join('\\n');\n }\n}\n\nexport function parse(markdown: string, options?: ParseOptions): LineTree {\n const tree = new LineTree();\n\n const input = markdown.split('\\n');\n let acceptLine = options?.startLine ? false : true;\n input.forEach(m => {\n if (options && options.startLine && options.endLine) {\n if (!acceptLine) {\n if (options.startLine === m.trim()) {\n acceptLine = true;\n }\n } else {\n if (options.endLine === m.trim()) {\n acceptLine = false;\n }\n }\n }\n if (acceptLine) {\n tree.add(new Line(m));\n }\n });\n\n return tree;\n}` }),257toFile({ fileName: 'roadmap.ts', fileContents: 'import commandLineArgs from \'command-line-args\';\nimport { resolve, dirname } from \'path\'\nimport { readFileSync, writeFileSync, mkdirSync } from \'fs\';\nimport { filter } from \'./roadmap-parser\';\n\nconst optionDefinitions = [\n { name: \'scope\', type: String },\n { name: \'year\', type: String },\n { name: \'source\', type: String },\n { name: \'template\', type: String },\n { name: \'startLine\', type: String, defaultValue: \'<!-- BEGIN -->\' },\n { name: \'endLine\', type: String, defaultValue: \'<!-- END -->\' },\n { name: \'output\', type: String }\n];\n\ninterface Options {\n scope: string | undefined,\n year: string | undefined,\n source: string | undefined,\n template: string | undefined,\n startLine: string,\n endLine: string,\n output: string | undefined\n}\n\nconst options = commandLineArgs(optionDefinitions, { partial: true }) as Options\nif (!(options.source || options.year) || !options.source) {\n console.log(\n`\nroadmap {--scope <public|internal>} {--year <2021>} --source <perpetual roadmap> {--template <template roadmap to generate>} {--output <file name of the generated roadmap>}\n\nYou may also use \'--startLine <line content>\' and \'--endLine <line content>\' to specify\n- the section(s) of the source roadmap to extract and process\n- the section in the template that should be replaced.\n\nThe defaults are \'<!-- BEGIN -->\' for \'--startLine\' and \'<!-- END -->\' for \'--endLine\'.\n`\n );\n} else {\n\n const cwd = process.cwd();\n const source = resolve(cwd, options.source!);\n try {\n\n const markers: string[] = [];\n if (options.year) {\n markers.push(options.year);\n }\n if (options.scope) {\n markers.push(options.scope)\n }\n\n const rawInput = readFileSync(source);\n const filteredInput = filter(rawInput.toString(), { startLine: options.startLine, endLine: options.endLine, markers, extractMatchingMarkers: true });\n\n if (!options.template) {\n console.log(filteredInput);\n } else {\n\n const template = readFileSync(resolve(cwd, options.template!));\n const processedOutput = replace(template.toString(), filteredInput, options);\n\n if (!options.output) {\n console.log(processedOutput)\n } else {\n\n const outputFile = resolve(cwd, options.output!);\n mkdirSync(dirname(outputFile), { recursive: true });\n writeFileSync(outputFile, processedOutput);\n }\n }\n\n } catch (e) {\n console.error(e.message);\n }\n}\n\nfunction replace(source: string, replacement: string, options: Options): string {\n const replacementRangeStart = source.indexOf(options.startLine) + options.startLine.length;\n const replacementRangeEnd = source.indexOf(options.endLine);\n return `${source.substring(0, replacementRangeStart)}\\n${replacement}\\n${source.substring(replacementRangeEnd)}`;\n}\n' }),258],259queries: [260{261query: 'change the code so that rather than markers to remove it works with markers to survive #file:roadmap.ts #file:roadmap-parser.ts ',262validate: async (outcome, workspace, accessor) => {263assertWorkspaceEdit(outcome);264const d = (await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic');265assert.strictEqual(d.length, 0);266assertNoElidedCodeComments(outcome);267}268}269]270});271});272273274stest({ description: 'fs provider: move function from one file to another', language: 'typescript', model }, (testingServiceCollection) => {275return executeEditTest(strategy, testingServiceCollection, {276files: [277fromFixture('multiFileEdit/fsprovider/package.json'),278fromFixture('multiFileEdit/fsprovider/src/extension.ts'),279fromFixture('multiFileEdit/fsprovider/src/fileSystemProvider.ts'),280],281queries: [282{283query: [284`In #file:extension.ts move the function 'randomData' to the end of #file:fileSystemProvider.ts . Make sure to update the imports in #file:extension.ts and to export the function at the new location`,285].join(' '),286validate: async (outcome, workspace, accessor) => {287assertWorkspaceEdit(outcome);288assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);289assert.strictEqual(outcome.files.length, 2, 'Expected two files to be edited');290const fileSystemProviderTs = assertFileContent(outcome.files, 'fileSystemProvider.ts');291const extensionTs = assertFileContent(outcome.files, 'extension.ts');292assert.ok(!extensionTs.includes('function randomData(lineCnt: number, lineLen = 155): Buffer'), 'randomData still found in extension.ts');293const newIndex = fileSystemProviderTs.indexOf('function randomData(lineCnt: number, lineLen = 155): Buffer');294assert.ok(newIndex !== -1, 'randomData not found in fileSystemProvider.ts');295const endOfMemFSIndex = fileSystemProviderTs.indexOf('// end of MemFS');296assert.ok(endOfMemFSIndex !== -1, `can no longer find the '// end of MemFS' comment in fileSystemProvider.ts`);297assert.ok(newIndex > endOfMemFSIndex, 'randomData was not placed at the end of fileSystemProvider.ts');298assert.ok(fileSystemProviderTs.indexOf('export function randomData') !== -1, 'randomData not exported in fileSystemProvider.ts');299assert.ok(extensionTs.match(/\bimport {[^}]+randomData[^}]+} from '.\/fileSystemProvider'/), 'Expected an import for randomData in extension.ts');300assertNoElidedCodeComments(outcome);301}302},303{304query: [305`Better move 'randomData in its own file: /Users/someone/Projects/proj01/utils.ts'. Don't forget to remove again randomData from #file:fileSystemProvider.ts`,306].join(' '),307validate: async (outcome, workspace, accessor) => {308assertWorkspaceEdit(outcome);309assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);310const fileSystemProviderTs = assertFileContent(outcome.files, 'fileSystemProvider.ts');311const extensionTs = assertFileContent(outcome.files, 'extension.ts');312const utilsTs = assertFileContent(outcome.files, 'utils.ts');313assert.ok(!fileSystemProviderTs.includes('function randomData(lineCnt: number, lineLen = 155): Buffer'), 'randomData still found in fileSystemProviderTs.ts');314const newIndex = utilsTs.indexOf('function randomData(lineCnt: number, lineLen = 155): Buffer');315assert.ok(newIndex !== -1, 'randomData not found in utilsTs.ts');316assert.ok(utilsTs.indexOf('export function randomData') !== -1, 'randomData not exported in fileSystemProvider.ts');317assert.ok(extensionTs.match(/\bimport { MemFS } from '.\/fileSystemProvider'/), 'Expected only MemFs import from fileSystemProvider');318assert.ok(extensionTs.match(/\bimport {[^}]+randomData[^}]+} from '.\/utils'/), 'Expected an import for randomData from utils');319assertNoElidedCodeComments(outcome);320}321},322{323query: [324`Please add a copyright statement to the new file #file:utils.ts. You can use the same statement as for #file:fileSystemProvider.ts`,325].join(' '),326validate: async (outcome, workspace, accessor) => {327assertWorkspaceEdit(outcome);328assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);329const utilsTs = assertFileContent(outcome.files, 'utils.ts');330assert.ok(utilsTs.includes('Copyright (c) Microsoft Corporation. All rights reserved.'), 'copyright (c) Microsoft not found');331assert.ok(utilsTs.includes('Licensed under the MIT License. See License.txt in the project root for license information'), 'Licensed under the MIT License not found');332assertNoElidedCodeComments(outcome);333}334}335]336});337});338339stest({ description: 'Issue #9647', language: 'typescript', model }, (testingServiceCollection) => {340return executeEditTest(strategy, testingServiceCollection, {341files: [342fromFixture('multiFileEdit/issue-9647/.env'),343],344queries: [345{346file: '.env',347selection: [0, 0, 0, 0],348query: 'Add OPENAI to .env',349validate: async (outcome, workspace, accessor) => {350assertInlineEdit(outcome);351assert.ok(outcome.fileContents.includes('OPENAI='), 'Expected OPENAI to be added to .env');352assertNoElidedCodeComments(outcome.fileContents);353}354}355]356});357});358359stest({ description: 'unicode string sequences', model }, (testingServiceCollection) => {360return executeEditTest(strategy, testingServiceCollection, {361files: [362fromFixture('multiFile/unicode-string-sequences/example.js')363],364queries: [365{366file: 'example.js',367selection: [8, 0, 8, 0],368query: 'Avoid recursion in fib ',369validate: async (outcome, workspace, accessor) => {370assertInlineEdit(outcome);371assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);372assertContainsAllSnippets(outcome.fileContents, ['\\u002D', '\\x2D']);373assertNoElidedCodeComments(outcome.fileContents);374}375}376]377});378});379380stest({ description: 'multiple questions', model }, (testingServiceCollection) => {381return executeEditTest(strategy, testingServiceCollection, {382files: [383fromFixture('multiFile/multiple-questions/package.json')384],385queries: [386{387file: 'package.json',388selection: [13, 77, 13, 77],389query: 'what is the latest version of typescript?',390validate: async (outcome, workspace, accessor) => {391assertNoErrorOutcome(outcome);392await assertCriteriaMetAsync(accessor, outcome.chatResponseMarkdown, 'Does the response answer the question what the latest version of typescript is?');393}394},395{396file: 'package.json',397selection: [13, 77, 13, 77],398query: 'What is the latest version of mocha?',399validate: async (outcome, workspace, accessor) => {400assertNoErrorOutcome(outcome);401await assertCriteriaMetAsync(accessor, outcome.chatResponseMarkdown, 'Does the response answer the question what the latest version of mocha is?');402}403}404]405});406});407stest({ description: 'create a README from two other files', model }, (testingServiceCollection) => {408return executeEditTest(strategy, testingServiceCollection, {409files: [410fromFixture('multiFileEdit/readme-generation/.devcontainer/devcontainer.json'),411fromFixture('multiFileEdit/readme-generation/.devcontainer/post-install.sh')412],413queries: [414{415file: 'devcontainer.json',416selection: [0, 0, 0, 0],417query: 'add a readme based on these files',418validate: async (outcome, workspace, accessor) => {419assertWorkspaceEdit(outcome);420assertFileContent(outcome.files, 'README.md');421assertNoElidedCodeComments(outcome);422await assertCriteriaMetAsync(accessor, getFileContent(outcome.files[0]), 'Does the content look like a Readme file, properly formatted, with multiple lines?');423}424}425]426});427});428429stest({ description: 'multiple edits on the same file', model }, (testingServiceCollection) => {430return executeEditTest(strategy, testingServiceCollection, {431files: [432fromFixture('multiFileEdit/two-edits/generate-command-ts.js'),433],434queries: [435{436file: 'generate-command-ts.js',437selection: [0, 0, 0, 0],438query: [439`Replace all occurrences of 'generator.fs.copy' with a call to a top level function 'copy' that takes 'generator', 'extensionConfig', 'from' and 'to' as parameters. `,440`Then do the same for 'generator.fs.copyTpl'. Do not emit the full solution in one go. Emit a code block for every change.`,441].join(''),442validate: async (outcome, workspace, accessor) => {443assertInlineEdit(outcome);444assertNoElidedCodeComments(outcome);445assertContainsAllSnippets(outcome.fileContents, [446'function copy(generator, extensionConfig, from, to) {',447'function copyTpl(generator, extensionConfig, from, to) {',448`copy(generator, extensionConfig, generator.templatePath(bundlerPath, 'vscode'), generator.destinationPath('.vscode'));`,449`copyTpl(generator, extensionConfig, 'vsc-extension-quickstart.md', 'vsc-extension-quickstart.md');`450]);451}452}453]454});455});456457stest({ description: 'work with untitled files', model }, (testingServiceCollection) => {458return executeEditTest(strategy, testingServiceCollection, {459files: [460toFile({ uri: URI.parse('untitled:Untitled-1'), fileContents: 'Hello\n' }),461],462queries: [463{464query: [465`In #file:Untitled-1 add a new line with the following content: 'World'`,466].join(' '),467validate: async (outcome, workspace, accessor) => {468assertWorkspaceEdit(outcome);469assert.strictEqual(outcome.files.length, 1, 'Expected one file to be edited');470const file = outcome.files[0];471assertQualifiedFile(file);472assert.strictEqual(file.uri.toString(), 'untitled:Untitled-1', 'Expected the URI to be unchanged');473assert.strictEqual(file.fileContents, 'Hello\nWorld\n', 'Expected the file contents to be Hello\\nWorld');474}475},476]477});478});479});480});481482483