From 2d51cc91bd79e7830b3426fdec5dca453adcfcae Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 21 Jul 2026 13:48:22 -0600 Subject: [PATCH 1/2] fix: match SourceMembers when server strips entity suffix from parent name @W-21612413@ --- .../remote/remoteSourceTrackingService.ts | 14 +++- test/nuts/platformEventTracking.nut.ts | 77 +++++++++++++++++++ .../config/project-scratch-def.json | 6 ++ .../TestEvent__e/TestEvent__e.object-meta.xml | 7 ++ .../fields/Message__c.field-meta.xml | 10 +++ .../platformEventTracking/sfdx-project.json | 6 ++ .../unit/remote/expectedSourceMembers.test.ts | 13 ++++ test/unit/remote/remoteSourceTracking.test.ts | 66 ++++++++++++++++ 8 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 test/nuts/platformEventTracking.nut.ts create mode 100644 test/nuts/repros/platformEventTracking/config/project-scratch-def.json create mode 100644 test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/TestEvent__e.object-meta.xml create mode 100644 test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/fields/Message__c.field-meta.xml create mode 100644 test/nuts/repros/platformEventTracking/sfdx-project.json diff --git a/src/shared/remote/remoteSourceTrackingService.ts b/src/shared/remote/remoteSourceTrackingService.ts index c342eb9e..b4a54d87 100644 --- a/src/shared/remote/remoteSourceTrackingService.ts +++ b/src/shared/remote/remoteSourceTrackingService.ts @@ -318,7 +318,19 @@ export class RemoteSourceTrackingService { queriedMembers.map((member) => { // remove anything returned from the query list const metadataKey = getMetadataKey(member.MemberType, member.MemberName); - const deleted = outstandingSourceMembers.delete(metadataKey); + let deleted = outstandingSourceMembers.delete(metadataKey); + // SourceMember may store platform event (and other custom object) children without the entity suffix. + // e.g., "MyEvent.Field__c" instead of "MyEvent__e.Field__c" + if (!deleted && member.MemberName.includes('.')) { + const dotIndex = member.MemberName.indexOf('.'); + const parent = member.MemberName.substring(0, dotIndex); + const child = member.MemberName.substring(dotIndex); + for (const suffix of ['__e', '__b', '__x', '__mdt']) { + const alternateKey = getMetadataKey(member.MemberType, `${parent}${suffix}${child}`); + deleted = outstandingSourceMembers.delete(alternateKey); + if (deleted) break; + } + } if (!deleted) { bonusTypes.add(metadataKey); } diff --git a/test/nuts/platformEventTracking.nut.ts b/test/nuts/platformEventTracking.nut.ts new file mode 100644 index 00000000..b6b57612 --- /dev/null +++ b/test/nuts/platformEventTracking.nut.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'node:path'; +import { TestSession } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import { Lifecycle, Org, SfProject } from '@salesforce/core'; +import { ComponentSet } from '@salesforce/source-deploy-retrieve'; +import { SourceTracking } from '../../src/sourceTracking'; + +describe('platform event source tracking after deploy (@W-21612413@)', () => { + let session: TestSession; + let stl: SourceTracking; + const warnings: string[] = []; + + before(async () => { + session = await TestSession.create({ + project: { + sourceDir: path.join('test', 'nuts', 'repros', 'platformEventTracking'), + }, + scratchOrgs: [ + { + config: path.join('config', 'project-scratch-def.json'), + duration: 1, + setDefault: true, + tracksSource: true, + }, + ], + devhubAuthStrategy: 'AUTO', + }); + + const org = await Org.create({ aliasOrUsername: session.orgs.get('default')?.username }); + const project = await SfProject.resolve(session.project.dir); + stl = await SourceTracking.create({ org, project }); + + Lifecycle.getInstance().onWarning((w) => { + warnings.push(w); + return Promise.resolve(); + }); + }); + + after(async () => { + await session?.clean(); + }); + + it('deploys a platform event with a custom field and updates source tracking without timeout', async () => { + const componentSet = ComponentSet.fromSource( + path.join(session.project.dir, 'force-app', 'main', 'default', 'objects', 'TestEvent__e') + ); + + const deploy = await componentSet.deploy({ usernameOrConnection: session.orgs.get('default')!.username! }); + const deployResult = await deploy.pollStatus(); + + expect(deployResult.response.success, 'deploy should succeed').to.equal(true); + + const fileResponses = deployResult.getFileResponses(); + expect(fileResponses.length).to.be.greaterThan(0); + + await stl.updateTrackingFromDeploy(deployResult); + + const pollingTimeoutWarnings = warnings.filter((w) => w.includes('SourceMembers timed out')); + expect(pollingTimeoutWarnings, 'source tracking should not time out polling for SourceMembers').to.have.length(0); + }); +}); diff --git a/test/nuts/repros/platformEventTracking/config/project-scratch-def.json b/test/nuts/repros/platformEventTracking/config/project-scratch-def.json new file mode 100644 index 00000000..fbfcc05f --- /dev/null +++ b/test/nuts/repros/platformEventTracking/config/project-scratch-def.json @@ -0,0 +1,6 @@ +{ + "orgName": "Platform Event Tracking Test", + "edition": "Developer", + "adminEmail": "nut@mailinator.com", + "hasSampleData": false +} diff --git a/test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/TestEvent__e.object-meta.xml b/test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/TestEvent__e.object-meta.xml new file mode 100644 index 00000000..98dcd567 --- /dev/null +++ b/test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/TestEvent__e.object-meta.xml @@ -0,0 +1,7 @@ + + + Deployed + HighVolume + + Test Events + diff --git a/test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/fields/Message__c.field-meta.xml b/test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/fields/Message__c.field-meta.xml new file mode 100644 index 00000000..8ea75813 --- /dev/null +++ b/test/nuts/repros/platformEventTracking/force-app/main/default/objects/TestEvent__e/fields/Message__c.field-meta.xml @@ -0,0 +1,10 @@ + + + Message__c + false + + 255 + false + Text + false + diff --git a/test/nuts/repros/platformEventTracking/sfdx-project.json b/test/nuts/repros/platformEventTracking/sfdx-project.json new file mode 100644 index 00000000..1c19a57b --- /dev/null +++ b/test/nuts/repros/platformEventTracking/sfdx-project.json @@ -0,0 +1,6 @@ +{ + "packageDirectories": [{ "path": "force-app", "default": true }], + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "62.0" +} diff --git a/test/unit/remote/expectedSourceMembers.test.ts b/test/unit/remote/expectedSourceMembers.test.ts index 9139a446..7ff11b48 100644 --- a/test/unit/remote/expectedSourceMembers.test.ts +++ b/test/unit/remote/expectedSourceMembers.test.ts @@ -146,4 +146,17 @@ describe('expectedSourceMembers', () => { expect(result.size).to.equal(1); expect(result.has('ApexClass###MyClass')).to.equal(true); }); + + it('includes custom fields on platform events (__e)', () => { + const input = [ + { + type: 'CustomField', + fullName: 'MyEvent__e.SomeField__c', + filePath: 'src/objects/MyEvent__e/fields/SomeField__c.field-meta.xml', + state: ComponentStatus.Created, + }, + ]; + const result = calculateExpectedSourceMembers(registry, input); + expect(result.has('CustomField###MyEvent__e.SomeField__c')).to.equal(true); + }); }); diff --git a/test/unit/remote/remoteSourceTracking.test.ts b/test/unit/remote/remoteSourceTracking.test.ts index cd1f833c..39ef9162 100644 --- a/test/unit/remote/remoteSourceTracking.test.ts +++ b/test/unit/remote/remoteSourceTracking.test.ts @@ -614,6 +614,72 @@ describe('remoteSourceTrackingService', () => { ).to.equal(true); expect(queryStub.called).to.equal(true); }); + + it('should match SourceMembers when server strips __e suffix from platform event parent', async () => { + const platformEventMembers: RemoteSyncInput[] = [ + { + type: 'CustomField', + fullName: 'MyEvent__e.SomeField__c', + filePath: 'src/objects/MyEvent__e/fields/SomeField__c.field-meta.xml', + state: ComponentStatus.Changed, + }, + ]; + + const queryStub = $$.SANDBOX.stub(orgQueryMocks, 'querySourceMembersFrom'); + // @ts-expect-error it's private + remoteSourceTrackingService.serverMaxRevisionCounter = 9; + + // Server returns the member WITHOUT __e suffix (the bug scenario) + queryStub.onFirstCall().resolves([ + { + ...defaultSourceMemberValues, + RevisionCounter: 10, + MemberType: 'CustomField', + MemberName: 'MyEvent.SomeField__c', + IsNameObsolete: false, + }, + ]); + + // @ts-expect-error stubbing private method for testing + const trackSpy = $$.SANDBOX.stub(remoteSourceTrackingService, 'trackSourceMembers'); + + await remoteSourceTrackingService.pollForSourceTracking(new RegistryAccess(), platformEventMembers); + expect(trackSpy.calledOnce).to.equal(true); + expect(queryStub.calledOnce).to.equal(true); + }); + + it('should match SourceMembers when server strips __mdt suffix from custom metadata parent', async () => { + const mdtMembers: RemoteSyncInput[] = [ + { + type: 'CustomField', + fullName: 'MyConfig__mdt.Value__c', + filePath: 'src/objects/MyConfig__mdt/fields/Value__c.field-meta.xml', + state: ComponentStatus.Changed, + }, + ]; + + const queryStub = $$.SANDBOX.stub(orgQueryMocks, 'querySourceMembersFrom'); + // @ts-expect-error it's private + remoteSourceTrackingService.serverMaxRevisionCounter = 9; + + queryStub.onFirstCall().resolves([ + { + ...defaultSourceMemberValues, + RevisionCounter: 10, + MemberType: 'CustomField', + MemberName: 'MyConfig.Value__c', + IsNameObsolete: false, + }, + ]); + + // @ts-expect-error stubbing private method for testing + const trackSpy = $$.SANDBOX.stub(remoteSourceTrackingService, 'trackSourceMembers'); + + await remoteSourceTrackingService.pollForSourceTracking(new RegistryAccess(), mdtMembers); + expect(trackSpy.calledOnce).to.equal(true); + expect(queryStub.calledOnce).to.equal(true); + }); + it('should sync specific elements', async () => { expect(getContents()).to.deep.equal({ serverMaxRevisionCounter: 0, From 99c528eb83a34e0fd03a5601075a797f7ed710be Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 21 Jul 2026 14:44:50 -0600 Subject: [PATCH 2/2] chore: bump deps --- package.json | 4 ++-- yarn.lock | 51 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 7a09afca..6486e255 100644 --- a/package.json +++ b/package.json @@ -50,9 +50,9 @@ "node": ">=18.0.0" }, "dependencies": { - "@salesforce/core": "^8.31.2", + "@salesforce/core": "^8.32.5", "@salesforce/kit": "^3.2.6", - "@salesforce/source-deploy-retrieve": "^12.36.6", + "@salesforce/source-deploy-retrieve": "^12.37.2", "@salesforce/ts-types": "^2.0.12", "fast-xml-parser": "^5.5.7", "graceful-fs": "^4.2.11", diff --git a/yarn.lock b/yarn.lock index 63b8cee7..b2494a3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -586,6 +586,21 @@ node-fetch "^2.6.1" xml2js "^0.6.2" +"@jsforce/jsforce-node@^3.10.17": + version "3.10.19" + resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.10.19.tgz#ccbc539c12f4f7dff9cfdcc6cfb8f07bd840f731" + integrity sha512-k7i2Tntu1fLvkMtRcKDFU64/Fr2M692ECtbwIGX6hcOh5mj+jrMa1tlvcdwffxAMl+lPYCXnY2bjErxWmP84zA== + dependencies: + "@sindresorhus/is" "^4" + base64url "^3.0.1" + csv-parse "^5.5.2" + csv-stringify "^6.6.0" + faye "^1.4.0" + form-data "^4.0.4" + multistream "^3.1.0" + undici "^8.5.0" + xml2js "^0.6.2" + "@jsonjoy.com/base64@^1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz" @@ -706,12 +721,12 @@ ts-retry-promise "^0.8.1" zod "^4.1.12" -"@salesforce/core@^8.31.2": - version "8.31.2" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.31.2.tgz#968448f423b553f726f42c27da6b55c4ec7eb93a" - integrity sha512-naqnq7Z+gbl1LdnyNvrGrNUoeMUQtCOsnrS6DfqeuLMJTFqcL9Dq0/od+xcuqi0+l7HTyH0/gU1BQitWpd1rag== +"@salesforce/core@^8.32.2", "@salesforce/core@^8.32.5": + version "8.32.5" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.32.5.tgz#e03eb5786a5cd2b8578c1906ee60a83ea7e70b9f" + integrity sha512-8me3JSwUaEkQ9oy6PdmddX6OAA8krESKMmr26HPD0zhuef00vt0Cax1fju2DGDn50LRQh3nGkC/qr/mGMYSXuA== dependencies: - "@jsforce/jsforce-node" "^3.10.13" + "@jsforce/jsforce-node" "^3.10.17" "@salesforce/kit" "^3.2.4" "@salesforce/ts-types" "^2.0.12" ajv "^8.18.0" @@ -792,12 +807,12 @@ resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.10.3.tgz#52c867fdd60679cf216110aa49542b7ad391f5d1" integrity sha512-FKfvtrYTcvTXE9advzS25/DEY9yJhEyLvStm++eQFtnAaX1pe4G3oGHgiQ0q55BM5+0AlCh0+0CVtQv1t4oJRA== -"@salesforce/source-deploy-retrieve@^12.36.6": - version "12.36.6" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.36.6.tgz#4c1e701cff9c9fa2802e8fa7e3ba2188426d47d1" - integrity sha512-1tz3eUyHp0yIAylrz+njlxGejd08cwPys9LjfBTVeY6a+xOBkTTDgQMLA6OBBZVc0HMPqjkJMKvbtDrAD5MJBA== +"@salesforce/source-deploy-retrieve@^12.37.2": + version "12.37.2" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.37.2.tgz#442f3be88d91021de14af6371d9f954689157ce7" + integrity sha512-wgG0RccjQ8CaBEO8woSvvTyxc6V7KIw1GbZlME0bU+Ly+2G8pdPHAm43hrBTBml5MdYVh3J3yZwMyIeRaQxeBA== dependencies: - "@salesforce/core" "^8.31.2" + "@salesforce/core" "^8.32.2" "@salesforce/kit" "^3.2.4" "@salesforce/ts-types" "^2.0.12" "@salesforce/types" "^1.6.0" @@ -2936,17 +2951,6 @@ form-data@^4.0.4, form-data@^4.0.5: hasown "^2.0.4" mime-types "^2.1.35" -form-data@^4.0.5: - version "4.0.6" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" - integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.4" - mime-types "^2.1.35" - fromentries@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz" @@ -6282,6 +6286,11 @@ undici-types@~6.21.0: resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== +undici@^8.5.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-8.8.0.tgz#5e858505ed78074c6b31b0fb7cce57ad56cc6267" + integrity sha512-ubshXMXwF3MQIMF1y/WxZdNBnjEKeSg2wF5mcGUtU55YTw34tnVVpKRlLf7ruDXZ5344KokPVX4RBx1wJm64Bw== + unist-util-is@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz"