diff --git a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts index dd0460b9..2dd34477 100644 --- a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts @@ -15,7 +15,7 @@ */ import { z } from 'zod'; -import { Connection, Org, SfError, SfProject } from '@salesforce/core'; +import { Connection, Lifecycle, Org, SfError, SfProject } from '@salesforce/core'; import { SourceTracking } from '@salesforce/source-tracking'; import { ComponentSet, ComponentSetBuilder } from '@salesforce/source-deploy-retrieve'; import { ensureString } from '@salesforce/ts-types'; @@ -35,12 +35,14 @@ import { textResponse } from '../shared/utils.js'; * - apexTests: Apex tests classes to run. * - usernameOrAlias: Username or alias of the Salesforce org to deploy to. * - directory: Directory of the local project. + * - ignoreConflicts: Ignore conflicts and deploy local files, even if they overwrite changes in the org. * * Returns: * - textResponse: Deploy result. */ export const deployMetadataParams = z.object({ + ignoreConflicts: z.boolean().describe(' Ignore conflicts and deploy local files, even if they overwrite changes in the org.').optional(), sourceDir: z .array(z.string()) .describe('Path to the local source files to deploy. Leave this unset if the user is vague about what to deploy.') @@ -106,13 +108,14 @@ export class DeployMetadataMcpTool extends McpTool { expect(deployResult.numberTestsCompleted).to.equal(11); expect(deployResult.runTestsEnabled).to.be.true; }); + + it('should deploy remote edit when ignoreConflicts is set to true', async () => { + const customAppPath = path.join( + testSession.project.dir, + 'force-app', + 'main', + 'default', + 'applications', + 'Dreamhouse.app-meta.xml', + ); + + // deploy the whole project to ensure the file exists + const fullProjectDeploy = await client.callTool(deployMetadataSchema, { + name: 'deploy_metadata', + params: { + usernameOrAlias: orgUsername, + directory: testSession.project.dir, + }, + }); + + expect(fullProjectDeploy.isError).to.be.false; + expect(fullProjectDeploy.content.length).to.equal(1); + + // Make a remote edit using Tooling API + const conn = await Connection.create({ + authInfo: await AuthInfo.create({ username: orgUsername }), + }); + + const customApp = await conn.singleRecordQuery<{ + Id: string; + Metadata: { + description: string | null; + }; + }>( + "SELECT Id, Metadata FROM CustomApplication WHERE DeveloperName = 'Dreamhouse'", + { + tooling: true, + } + ); + + const updatedMetadata = { + ...customApp.Metadata, + description: customApp.Metadata.description + ? `${customApp.Metadata.description} - Remote edit via Tooling API` + : 'Remote edit via Tooling API', + }; + + await conn.tooling.sobject('CustomApplication').update({ + Id: customApp.Id, + Metadata: updatedMetadata, + }); + + // Deploy with ignoreConflicts=true - should override remote edit + const deployResult = await client.callTool(deployMetadataSchema, { + name: 'deploy_metadata', + params: { + sourceDir: [customAppPath], + ignoreConflicts: true, + usernameOrAlias: orgUsername, + directory: testSession.project.dir, + }, + }); + + expect(deployResult.isError).to.equal(false); + expect(deployResult.content.length).to.equal(1); + if (deployResult.content[0].type !== 'text') assert.fail(); + + const deployText = deployResult.content[0].text; + expect(deployText).to.contain('Deploy result:'); + + const deployMatch = deployText.match(/Deploy result: ({.*})/); + expect(deployMatch).to.not.be.null; + + const result = JSON.parse(deployMatch![1]) as { + success: boolean; + done: boolean; + numberComponentsDeployed: number; + }; + + expect(result.success).to.be.true; + expect(result.done).to.be.true; + expect(result.numberComponentsDeployed).to.equal(1); + }); }); diff --git a/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts b/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts index ef34d4ad..76281db8 100644 --- a/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts +++ b/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts @@ -182,4 +182,56 @@ describe('retrieve_metadata', () => { expect(apexClasses.length).to.equal(9); expect(packageXml.length).to.equal(1); }); + + it('should retrieve and overwrite local edits when ignoreConflicts is true', async () => { + const apexClassPath = path.join('force-app', 'main', 'default', 'classes', 'GeocodingService.cls'); + const localFilePath = path.join(testSession.project.dir, apexClassPath); + const originalContent = fs.readFileSync(localFilePath, 'utf8'); + const localEditContent = originalContent + '\n// Local edit'; + + // Make a local edit to the file + fs.writeFileSync(localFilePath, localEditContent, 'utf8'); + + // Retrieve with ignoreConflicts=true - should overwrite local edit + const result = await client.callTool(retrieveMetadataSchema, { + name: 'retrieve_metadata', + params: { + sourceDir: [apexClassPath], + ignoreConflicts: true, + usernameOrAlias: orgUsername, + directory: testSession.project.dir, + }, + }); + + expect(result.isError).to.equal(false); + expect(result.content.length).to.equal(1); + if (result.content[0].type !== 'text') assert.fail(); + + const responseText = result.content[0].text; + expect(responseText).to.contain('Retrieve result:'); + + // Parse the retrieve result JSON + const retrieveMatch = responseText.match(/Retrieve result: ({.*})/); + expect(retrieveMatch).to.not.be.null; + + const retrieveResult = JSON.parse(retrieveMatch![1]) as { + success: boolean; + done: boolean; + fileProperties: Array<{ + type: string; + fullName: string; + fileName: string; + }>; + }; + expect(retrieveResult.success).to.be.true; + expect(retrieveResult.done).to.be.true; + expect(retrieveResult.fileProperties.length).to.equal(2); + + const apexClass = retrieveResult.fileProperties.find( + (fp: { type: string; fullName: string }) => fp.type === 'ApexClass', + ); + if (!apexClass) assert.fail(); + expect(apexClass.fullName).to.equal('GeocodingService'); + expect(apexClass.fileName).to.equal('unpackaged/classes/GeocodingService.cls'); + }); });