diff --git a/messages/delete.json b/messages/delete.json index 94f2a934b..aef54933d 100644 --- a/messages/delete.json +++ b/messages/delete.json @@ -39,5 +39,6 @@ }, "localPrompt": "This operation will delete the following files on your computer and in your org: \n%s", "remotePrompt": "This operation will delete the following metadata in your org: \n%s", + "deployPrompt": "This operation will deploy the following: \n%s", "areYouSure": "\n\nAre you sure you want to proceed (y/n)?" } diff --git a/src/commands/force/source/delete.ts b/src/commands/force/source/delete.ts index c81ed9b13..6b72957ec 100644 --- a/src/commands/force/source/delete.ts +++ b/src/commands/force/source/delete.ts @@ -6,26 +6,30 @@ */ import * as os from 'os'; import * as fs from 'fs'; +import * as path from 'path'; import { confirm } from 'cli-ux/lib/prompt'; import { flags, FlagsConfig } from '@salesforce/command'; import { Messages } from '@salesforce/core'; import { ComponentSet, + ComponentStatus, DestructiveChangesType, + FileResponse, MetadataComponent, RequestStatus, SourceComponent, } from '@salesforce/source-deploy-retrieve'; import { Duration, env, once } from '@salesforce/kit'; -import { getString } from '@salesforce/ts-types'; import { DeployCommand } from '../../../deployCommand'; import { ComponentSetBuilder } from '../../../componentSetBuilder'; -import { DeployCommandResult } from '../../../formatters/deployResultFormatter'; +import { DeployCommandResult, DeployResultFormatter } from '../../../formatters/deployResultFormatter'; import { DeleteResultFormatter } from '../../../formatters/deleteResultFormatter'; import { ProgressFormatter } from '../../../formatters/progressFormatter'; import { DeployProgressBarFormatter } from '../../../formatters/deployProgressBarFormatter'; import { DeployProgressStatusFormatter } from '../../../formatters/deployProgressStatusFormatter'; +const fsPromises = fs.promises; + Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-source', 'delete'); @@ -79,9 +83,14 @@ export class Delete extends DeployCommand { protected xorFlags = ['metadata', 'sourcepath']; protected readonly lifecycleEventNames = ['predeploy', 'postdeploy']; private isRest = false; - private deleteResultFormatter: DeleteResultFormatter; + private deleteResultFormatter: DeleteResultFormatter | DeployResultFormatter; private aborted = false; private components: MetadataComponent[]; + // create the delete FileResponse as we're parsing the comp. set to use in the output + private mixedDeployDelete: { deploy: string[]; delete: FileResponse[] } = { delete: [], deploy: [] }; + // map of component in project, to where it is stashed + private stashPath = new Map(); + private tempDir = path.join(os.tmpdir(), 'source_delete'); private updateDeployId = once((id: string) => { this.displayDeployId(id); @@ -90,11 +99,11 @@ export class Delete extends DeployCommand { public async run(): Promise { await this.delete(); - this.resolveSuccess(); + await this.resolveSuccess(); const result = this.formatResult(); // The DeleteResultFormatter will use SDR and scan the directory, if the files have been deleted, it will throw an error // so we'll delete the files locally now - this.deleteFilesLocally(); + await this.deleteFilesLocally(); return result; } @@ -102,11 +111,12 @@ export class Delete extends DeployCommand { this.deleteResultFormatter = new DeleteResultFormatter(this.logger, this.ux, {}); // verify that the user defined one of: metadata, sourcepath this.validateFlags(); + const sourcepaths = this.getFlag('sourcepath'); this.componentSet = await ComponentSetBuilder.build({ apiversion: this.getFlag('apiversion'), sourceapiversion: await this.getSourceApiVersion(), - sourcepath: this.getFlag('sourcepath'), + sourcepath: sourcepaths, metadata: this.flags.metadata && { metadataEntries: this.getFlag('metadata'), directoryPaths: this.getPackageDirs(), @@ -117,7 +127,7 @@ export class Delete extends DeployCommand { if (!this.components.length) { // if we didn't find any components to delete, let the user know and exit - this.deleteResultFormatter.displayNoResultsFound(); + (this.deleteResultFormatter as DeleteResultFormatter).displayNoResultsFound(); return; } @@ -133,6 +143,21 @@ export class Delete extends DeployCommand { }); this.componentSet = cs; + if (sourcepaths) { + // determine if user is trying to delete a single file from a bundle, which is actually just an fs delete operation + // and then a constructive deploy on the "new" bundle + this.components + .filter((comp) => comp.type.strategies?.adapter === 'bundle' && comp instanceof SourceComponent) + .map((bundle: SourceComponent) => { + sourcepaths.map(async (sourcepath) => { + // walkContent returns absolute paths while sourcepath will usually be relative + if (bundle.walkContent().find((content) => content.endsWith(sourcepath))) { + await this.moveBundleToManifest(bundle, sourcepath); + } + }); + }); + } + this.aborted = !(await this.handlePrompt()); if (this.aborted) return; @@ -165,11 +190,22 @@ export class Delete extends DeployCommand { /** * Checks the response status to determine whether the delete was successful. */ - protected resolveSuccess(): void { - const status = getString(this.deployResult, 'response.status'); + protected async resolveSuccess(): Promise { + const status = this.deployResult?.response?.status; if (status !== RequestStatus.Succeeded && !this.aborted) { this.setExitCode(1); } + // if deploy failed OR the operation was cancelled, restore the stashed files if they exist + else if (status !== RequestStatus.Succeeded || this.aborted) { + await Promise.all( + this.mixedDeployDelete.delete.map(async (file) => { + await this.restoreFileFromStash(file.filePath); + }) + ); + } else if (this.mixedDeployDelete.delete.length) { + // successful delete -> delete the stashed file + await this.deleteStash(); + } } protected formatResult(): DeployCommandResult { @@ -177,39 +213,104 @@ export class Delete extends DeployCommand { verbose: this.getFlag('verbose', false), }; - this.deleteResultFormatter = new DeleteResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult); + this.deleteResultFormatter = this.mixedDeployDelete.deploy.length + ? new DeployResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult) + : new DeleteResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult); // Only display results to console when JSON flag is unset. if (!this.isJsonOutput()) { this.deleteResultFormatter.display(); } + if (this.mixedDeployDelete.deploy.length && !this.aborted) { + // override JSON output when we actually deployed + const json = this.deleteResultFormatter.getJson(); + json.deletedSource = this.mixedDeployDelete.delete; // to match toolbelt json output + json.outboundFiles = []; // to match toolbelt version + json.deletes = json.deploys; // to match toolbelt version + delete json.deploys; + return json; + } + + if (this.aborted) { + return { + status: 0, + result: { + deletedSource: [], + outboundFiles: [], + deletes: [{}], + }, + } as unknown as DeployCommandResult; + } + return this.deleteResultFormatter.getJson(); } - private deleteFilesLocally(): void { - if (!this.getFlag('checkonly') && getString(this.deployResult, 'response.status') === 'Succeeded') { + private async deleteFilesLocally(): Promise { + if (!this.getFlag('checkonly') && this.deployResult?.response?.status === RequestStatus.Succeeded) { + const promises = []; this.components.map((component: SourceComponent) => { - // delete the content and/or the xml of the components - if (component.content) { - const stats = fs.lstatSync(component.content); - if (stats.isDirectory()) { - fs.rmdirSync(component.content, { recursive: true }); - } else { - fs.unlinkSync(component.content); + // mixed delete/deploy operations have already been deleted and stashed + if (!this.mixedDeployDelete.delete.length) { + if (component.content) { + const stats = fs.lstatSync(component.content); + if (stats.isDirectory()) { + promises.push(fsPromises.rm(component.content, { recursive: true })); + } else { + promises.push(fsPromises.unlink(component.content)); + } + } + if (component.xml) { + promises.push(fsPromises.unlink(component.xml)); } - } - if (component.xml) { - fs.unlinkSync(component.xml); } }); + await Promise.all(promises); } } + private async moveFileToStash(file: string): Promise { + await fsPromises.mkdir(path.dirname(this.stashPath.get(file)), { recursive: true }); + await fsPromises.copyFile(file, this.stashPath.get(file)); + await fsPromises.unlink(file); + } + + private async restoreFileFromStash(file: string): Promise { + await fsPromises.rename(this.stashPath.get(file), file); + } + + private async deleteStash(): Promise { + await fsPromises.rm(this.tempDir, { recursive: true, force: true }); + } + + private async moveBundleToManifest(bundle: SourceComponent, sourcepath: string): Promise { + // if one of the passed in sourcepaths is to a bundle component + const fileName = path.basename(sourcepath); + const fullName = path.join(bundle.name, fileName); + this.mixedDeployDelete.delete.push({ + state: ComponentStatus.Deleted, + fullName, + type: bundle.type.name, + filePath: sourcepath, + }); + // stash the file in case we need to restore it due to failed deploy/aborted command + this.stashPath.set(sourcepath, path.join(this.tempDir, fullName)); + await this.moveFileToStash(sourcepath); + + // re-walk the directory to avoid picking up the deleted file + this.mixedDeployDelete.deploy.push(...bundle.walkContent()); + + // now remove the bundle from destructive changes and add to manifest + // set the bundle as NOT marked for delete + this.componentSet.destructiveChangesPost.delete(`${bundle.type.id}#${bundle.fullName}`); + bundle.setMarkedForDelete(false); + this.componentSet.add(bundle); + } + private async handlePrompt(): Promise { if (!this.getFlag('noprompt')) { const remote: string[] = []; - const local: string[] = []; + let local: string[] = []; const message: string[] = []; this.components.flatMap((component) => { @@ -221,6 +322,14 @@ export class Delete extends DeployCommand { } }); + if (this.mixedDeployDelete.delete.length) { + local = this.mixedDeployDelete.delete.map((fr) => fr.fullName); + } + + if (this.mixedDeployDelete.deploy.length) { + message.push(messages.getMessage('deployPrompt', [[...new Set(this.mixedDeployDelete.deploy)].join('\n')])); + } + if (remote.length) { message.push(messages.getMessage('remotePrompt', [[...new Set(remote)].join('\n')])); } diff --git a/test/commands/source/delete.test.ts b/test/commands/source/delete.test.ts index 7882dd0e8..c5eeeca41 100644 --- a/test/commands/source/delete.test.ts +++ b/test/commands/source/delete.test.ts @@ -18,10 +18,13 @@ import { ComponentSetBuilder, ComponentSetOptions } from '../../../src/component import { Delete } from '../../../src/commands/force/source/delete'; import { exampleDeleteResponse, exampleSourceComponent } from './testConsts'; +const fsPromises = fs.promises; + describe('force:source:delete', () => { const sandbox = sinon.createSandbox(); const username = 'delete-test@org.com'; const defaultPackagePath = 'defaultPackagePath'; + let confirm = true; const oclifConfigStub = fromStub(stubInterface(sandbox)); @@ -30,6 +33,9 @@ describe('force:source:delete', () => { let lifecycleEmitStub: sinon.SinonStub; let resolveProjectConfigStub: sinon.SinonStub; let fsUnlink: sinon.SinonStub; + let moveToStashStub: sinon.SinonStub; + let restoreFromStashStub: sinon.SinonStub; + let deleteStashStub: sinon.SinonStub; class TestDelete extends Delete { public async runIt() { @@ -72,7 +78,11 @@ describe('force:source:delete', () => { return exampleDeleteResponse; }, }); - fsUnlink = stubMethod(sandbox, fs, 'unlinkSync').returns(true); + stubMethod(sandbox, cmd, 'handlePrompt').returns(confirm); + fsUnlink = stubMethod(sandbox, fsPromises, 'unlink').resolves(true); + moveToStashStub = stubMethod(sandbox, cmd, 'moveFileToStash'); + restoreFromStashStub = stubMethod(sandbox, cmd, 'restoreFileFromStash'); + deleteStashStub = stubMethod(sandbox, cmd, 'deleteStash'); return cmd.runIt(); }; @@ -168,4 +178,69 @@ describe('force:source:delete', () => { }); ensureHookArgs(); }); + + const stubLWC = (): string => { + buildComponentSetStub.restore(); + const comp = new SourceComponent({ + name: 'mylwc', + type: { + id: 'lightningcomponentbundle', + name: 'LightningComponentBundle', + strategies: { + adapter: 'bundle', + }, + }, + }); + stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ + toArray: () => { + return [comp]; + }, + }); + const helperPath = join('dreamhouse-lwc', 'force-app', 'main', 'default', 'lwc', 'mylwc', 'helper.js'); + + stubMethod(sandbox, comp, 'walkContent').returns([ + join('dreamhouse-lwc', 'force-app', 'main', 'default', 'lwc', 'mylwc', 'mylwc.js'), + helperPath, + ]); + + stubMethod(sandbox, fs, 'lstatSync').returns({ isDirectory: () => false }); + return helperPath; + }; + + it('will use stash and delete stash upon successful delete', async () => { + const sourcepath = stubLWC(); + const result = await runDeleteCmd(['--sourcepath', sourcepath, '--json', '-r']); + // successful delete will move files to the stash, delete the stash, and won't restore from it + expect(moveToStashStub.calledOnce).to.be.true; + expect(deleteStashStub.calledOnce).to.be.true; + expect(restoreFromStashStub.called).to.be.false; + expect(result.deletedSource).to.deep.equal([ + { + filePath: sourcepath, + fullName: join('mylwc', 'helper.js'), + state: 'Deleted', + type: 'LightningComponentBundle', + }, + ]); + }); + + it('restores from stash during aborted delete', async () => { + const sourcepath = stubLWC(); + + confirm = false; + const result = await runDeleteCmd(['--sourcepath', sourcepath, '--json', '-r']); + // aborted delete will move files to the stash, and restore from it + expect(moveToStashStub.calledOnce).to.be.true; + expect(deleteStashStub.called).to.be.false; + expect(restoreFromStashStub.calledOnce).to.be.true; + // ensure JSON output from aborted delete + expect(result).to.deep.equal({ + result: { + deletedSource: [], + deletes: [{}], + outboundFiles: [], + }, + status: 0, + }); + }); }); diff --git a/test/nuts/delete.nut.ts b/test/nuts/delete.nut.ts index 6319b75eb..3ceb23363 100644 --- a/test/nuts/delete.nut.ts +++ b/test/nuts/delete.nut.ts @@ -12,11 +12,20 @@ import { expect } from 'chai'; import { execCmd } from '@salesforce/cli-plugins-testkit'; import { SourceTestkit } from '@salesforce/source-testkit'; import { exec } from 'shelljs'; +import { FileResponse } from '@salesforce/source-deploy-retrieve'; +import { isNameObsolete } from './deployDestructive.nut'; describe('source:delete NUTs', () => { const executable = path.join(process.cwd(), 'bin', 'run'); let testkit: SourceTestkit; + const queryOrgAndFS = async (name: string, fsPath: string): Promise => { + // ensure the LWC is still in the org + expect(await isNameObsolete(testkit.username, 'LightningComponentBundle', name)).to.be.false; + // while the helper.js file was deleted + expect(fs.existsSync(fsPath)).to.be.false; + }; + const createApexClass = () => { // create and deploy an ApexClass that can be deleted without dependency issues const apexName = 'myApexClass'; @@ -148,4 +157,89 @@ describe('source:delete NUTs', () => { // ensure a failed delete attempt won't delete local files expect(fs.existsSync(pathToClass)).to.be.true; }); + + it('should delete a bundle component and deploy as a "new" bundle', async () => { + // use the brokerCard LWC + const lwcPath = path.join(testkit.projectDir, 'force-app', 'main', 'default', 'lwc', 'brokerCard', 'helper.js'); + fs.writeFileSync(lwcPath, '//', { encoding: 'utf8' }); + execCmd(`force:source:deploy -p ${lwcPath}`); + const deleteResult = execCmd<{ deletedSource: [FileResponse] }>( + `force:source:delete -p ${lwcPath} --noprompt --json` + ).jsonOutput.result; + + expect(deleteResult.deletedSource.length).to.equal(1); + expect(deleteResult.deletedSource[0].filePath, 'filepath').to.include(lwcPath); + expect(deleteResult.deletedSource[0].fullName, 'fullname').to.include(path.join('brokerCard', 'helper.js')); + expect(deleteResult.deletedSource[0].state, 'state').to.equal('Deleted'); + expect(deleteResult.deletedSource[0].type, 'type').to.equal('LightningComponentBundle'); + + await queryOrgAndFS('brokerCard', lwcPath); + }); + + it('should delete a bundle component and deploy as a "new" bundle to two different bundles', async () => { + // use the brokerCard and daysOnMarket LWC each with a helper.js file + const lwcPath1 = path.join(testkit.projectDir, 'force-app', 'main', 'default', 'lwc', 'brokerCard', 'helper.js'); + const lwcPath2 = path.join(testkit.projectDir, 'force-app', 'main', 'default', 'lwc', 'daysOnMarket', 'helper.js'); + fs.writeFileSync(lwcPath1, '//', { encoding: 'utf8' }); + fs.writeFileSync(lwcPath2, '//', { encoding: 'utf8' }); + execCmd(`force:source:deploy -p ${lwcPath1},${lwcPath2}`); + // delete both helper.js files at the same time + const deleteResult = execCmd<{ deletedSource: FileResponse[] }>( + `force:source:delete -p "${lwcPath1},${lwcPath2}" --noprompt --json` + ).jsonOutput.result; + + expect(deleteResult.deletedSource.length).to.equal(2); + expect(deleteResult.deletedSource[0].filePath, 'filepath').to.include(lwcPath1); + expect(deleteResult.deletedSource[0].fullName, 'fullname').to.include(path.join('brokerCard', 'helper.js')); + expect(deleteResult.deletedSource[0].state, 'state').to.equal('Deleted'); + expect(deleteResult.deletedSource[0].type, 'type').to.equal('LightningComponentBundle'); + + expect(deleteResult.deletedSource[1].filePath, 'filepath').to.include(lwcPath2); + expect(deleteResult.deletedSource[1].fullName, 'fullname').to.include(path.join('daysOnMarket', 'helper.js')); + expect(deleteResult.deletedSource[1].state, 'state').to.equal('Deleted'); + expect(deleteResult.deletedSource[1].type, 'type').to.equal('LightningComponentBundle'); + + await queryOrgAndFS('brokerCard', lwcPath1); + await queryOrgAndFS('daysOnMarket', lwcPath2); + }); + + it('should delete an entire LWC', async () => { + const lwcPath = path.join(testkit.projectDir, 'force-app', 'main', 'default', 'lwc'); + const mylwcPath = path.join(lwcPath, 'mylwc'); + execCmd(`force:lightning:component:create -n mylwc --type lwc -d ${lwcPath}`); + execCmd(`force:source:deploy -p ${mylwcPath}`); + expect(await isNameObsolete(testkit.username, 'LightningComponentBundle', 'mylwc')).to.be.false; + const deleteResult = execCmd<{ deletedSource: [FileResponse] }>( + `force:source:delete -p ${mylwcPath} --noprompt --json` + ).jsonOutput.result; + + expect(deleteResult.deletedSource.length).to.equal(3); + expect(deleteResult.deletedSource[0].filePath, 'filepath').to.include(mylwcPath); + expect(deleteResult.deletedSource[0].fullName, 'fullname').to.include(path.join('mylwc')); + expect(deleteResult.deletedSource[0].state, 'state').to.equal('Deleted'); + expect(deleteResult.deletedSource[0].type, 'type').to.equal('LightningComponentBundle'); + + expect(fs.existsSync(mylwcPath)).to.be.false; + expect(await isNameObsolete(testkit.username, 'LightningComponentBundle', 'mylwc')).to.be.true; + }); + + it('a failed delete will NOT delete files locally', async () => { + const lwcPath = path.join(testkit.projectDir, 'force-app', 'main', 'default', 'lwc'); + const brokerPath = path.join(lwcPath, 'brokerCard'); + const deleteResult = execCmd<{ deletedSource: [FileResponse & { error: string }] }>( + `force:source:delete -p ${brokerPath} --noprompt --json`, + { ensureExitCode: 1 } + ).jsonOutput.result; + + expect(deleteResult.deletedSource.length).to.equal(1); + expect(deleteResult.deletedSource[0].fullName, 'fullname').to.include(path.join('brokerCard')); + expect(deleteResult.deletedSource[0].state, 'state').to.equal('Failed'); + expect(deleteResult.deletedSource[0].type, 'type').to.equal('LightningComponentBundle'); + expect(deleteResult.deletedSource[0].error, 'error').to.include( + 'Referenced by a component instance inside the Lightning page Property Record Page : Lightning Page.' + ); + + expect(await isNameObsolete(testkit.username, 'LightningComponentBundle', 'brokerCard')).to.be.false; + expect(fs.existsSync(brokerPath)).to.be.true; + }); }); diff --git a/test/nuts/deployDestructive.nut.ts b/test/nuts/deployDestructive.nut.ts index 11a64588c..d17a78648 100644 --- a/test/nuts/deployDestructive.nut.ts +++ b/test/nuts/deployDestructive.nut.ts @@ -12,7 +12,20 @@ import { execCmd } from '@salesforce/cli-plugins-testkit'; import { SourceTestkit } from '@salesforce/source-testkit'; import { AuthInfo, Connection } from '@salesforce/core'; -describe('source:delete NUTs', () => { +export const isNameObsolete = async (username: string, memberType: string, memberName: string): Promise => { + const connection = await Connection.create({ + authInfo: await AuthInfo.create({ username }), + }); + + const res = await connection.singleRecordQuery<{ IsNameObsolete: boolean }>( + `SELECT IsNameObsolete FROM SourceMember WHERE MemberType='${memberType}' AND MemberName='${memberName}'`, + { tooling: true } + ); + + return res.IsNameObsolete; +}; + +describe('source:deploy --destructive NUTs', () => { const executable = path.join(process.cwd(), 'bin', 'run'); let testkit: SourceTestkit; @@ -29,19 +42,6 @@ describe('source:delete NUTs', () => { execCmd(`force:source:manifest:create --metadata ${metadata} --manifesttype ${manifesttype}`); }; - const isNameObsolete = async (memberType: string, memberName: string): Promise => { - const connection = await Connection.create({ - authInfo: await AuthInfo.create({ username: testkit.username }), - }); - - const res = await connection.singleRecordQuery<{ IsNameObsolete: boolean }>( - `SELECT IsNameObsolete FROM SourceMember WHERE MemberType='${memberType}' AND MemberName='${memberName}'`, - { tooling: true } - ); - - return res.IsNameObsolete; - }; - before(async () => { testkit = await SourceTestkit.create({ nut: __filename, @@ -58,7 +58,7 @@ describe('source:delete NUTs', () => { describe('destructive changes POST', () => { it('should deploy and then delete an ApexClass ', async () => { const { apexName } = createApexClass(); - let deleted = await isNameObsolete('ApexClass', apexName); + let deleted = await isNameObsolete(testkit.username, 'ApexClass', apexName); expect(deleted).to.be.false; createManifest('ApexClass:GeocodingService', 'package'); @@ -68,7 +68,7 @@ describe('source:delete NUTs', () => { ensureExitCode: 0, }); - deleted = await isNameObsolete('ApexClass', apexName); + deleted = await isNameObsolete(testkit.username, 'ApexClass', apexName); expect(deleted).to.be.true; }); }); @@ -76,7 +76,7 @@ describe('source:delete NUTs', () => { describe('destructive changes PRE', () => { it('should delete an ApexClass and then deploy a class', async () => { const { apexName } = createApexClass(); - let deleted = await isNameObsolete('ApexClass', apexName); + let deleted = await isNameObsolete(testkit.username, 'ApexClass', apexName); expect(deleted).to.be.false; createManifest('ApexClass:GeocodingService', 'package'); @@ -86,7 +86,7 @@ describe('source:delete NUTs', () => { ensureExitCode: 0, }); - deleted = await isNameObsolete('ApexClass', apexName); + deleted = await isNameObsolete(testkit.username, 'ApexClass', apexName); expect(deleted).to.be.true; }); }); @@ -95,8 +95,8 @@ describe('source:delete NUTs', () => { it('should delete a class, then deploy and then delete an ApexClass', async () => { const pre = createApexClass('pre').apexName; const post = createApexClass('post').apexName; - let preDeleted = await isNameObsolete('ApexClass', pre); - let postDeleted = await isNameObsolete('ApexClass', post); + let preDeleted = await isNameObsolete(testkit.username, 'ApexClass', pre); + let postDeleted = await isNameObsolete(testkit.username, 'ApexClass', post); expect(preDeleted).to.be.false; expect(postDeleted).to.be.false; @@ -111,8 +111,8 @@ describe('source:delete NUTs', () => { } ); - preDeleted = await isNameObsolete('ApexClass', pre); - postDeleted = await isNameObsolete('ApexClass', post); + preDeleted = await isNameObsolete(testkit.username, 'ApexClass', pre); + postDeleted = await isNameObsolete(testkit.username, 'ApexClass', post); expect(preDeleted).to.be.true; expect(postDeleted).to.be.true; });