Skip to content

Commit 1a83d93

Browse files
fix: always clean up pre events W-20002028 (#828)
* fix: always clean up `pre` events * chore: lint * test: fix unit test failures W-20002028 (#829) * test: fix removeSourceTracking UTs * test: fix pathIsInFolder UTs * chore: getNonSequential handle empty pkgDir * test: platform-specific test * chore: bump sdr * chore: update sdr lib to latest --------- Co-authored-by: soridalac <sorida.lac@salesforce.com>
1 parent 7bbea8f commit 1a83d93

9 files changed

Lines changed: 137 additions & 109 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"dependencies": {
5353
"@salesforce/core": "^8.24.0",
5454
"@salesforce/kit": "^3.2.4",
55-
"@salesforce/source-deploy-retrieve": "^12.31.5",
55+
"@salesforce/source-deploy-retrieve": "^12.31.8",
5656
"@salesforce/ts-types": "^2.0.12",
5757
"fast-xml-parser": "^4.5.3",
5858
"graceful-fs": "^4.2.11",

src/shared/functions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export const excludeLwcLocalOnlyTest = (filePath: string): boolean =>
6464
export const pathIsInFolder =
6565
(folder: string) =>
6666
(filePath: string): boolean => {
67+
// empty paths should not match anything; an empty folder would normalize to '/' and match everything
68+
if (!folder || !filePath) {
69+
return false;
70+
}
6771
if (folder === filePath) {
6872
return true;
6973
}

src/shared/localComponentSetArray.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,16 @@ const getNonSequential = ({
7878
packageDirs,
7979
nonDeletes: nonDeletes,
8080
deletes: deletes,
81-
}: GroupedFileInput): GroupedFile[] => [
82-
{
83-
nonDeletes,
84-
deletes,
85-
path: packageDirs.map((dir) => dir.name).join(';'),
86-
},
87-
];
81+
}: GroupedFileInput): GroupedFile[] => {
82+
if (packageDirs.length === 0) return [];
83+
return [
84+
{
85+
nonDeletes,
86+
deletes,
87+
path: packageDirs.map((dir) => dir.name).join(';'),
88+
},
89+
];
90+
};
8891

8992
export const getComponentSets = ({
9093
groupings,

src/sourceTracking.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,14 @@ export class SourceTracking extends AsyncCreatable {
700700
private async maybeSubscribeLifecycleEvents(): Promise<void> {
701701
if (this.subscribeSDREvents && (await this.org.tracksSource())) {
702702
const lifecycle = Lifecycle.getInstance();
703+
704+
// Always remove the pre events when `maybeSubscribeLifecycleEvents` is called.
705+
// Events are attached to a singleton (sfdx-core's Lifecycle), so when
706+
// instantiating `SourceTracking` multiple times in the same process we need
707+
// each instance starts clean.
708+
lifecycle.removeAllListeners('scopedPreDeploy')
709+
lifecycle.removeAllListeners('scopedPreRetrieve')
710+
703711
// the only thing STL uses pre events for is to check conflicts. So if you don't care about conflicts, don't listen!
704712
if (!this.ignoreConflicts) {
705713
this.logger.debug('subscribing to predeploy/retrieve events');

test/unit/localDetectMovedFiles.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ describe('local detect moved files', () => {
272272
}
273273
});
274274

275-
it.only('automatically commits moved files and leaves other changes alone', async () => {
275+
it('automatically commits moved files and leaves other changes alone', async () => {
276276
let projectDir!: string;
277277
try {
278278
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'localShadowRepoTest'));

test/unit/pathIsInFolder.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { normalize } from 'node:path';
16+
import { normalize, sep } from 'node:path';
1717
import { expect } from 'chai';
1818
import { pathIsInFolder } from '../../src/shared/functions';
1919

@@ -73,7 +73,10 @@ describe('pathIsInFolder', () => {
7373
});
7474

7575
it('handles paths with mixed separators', () => {
76-
expect(pathIsInFolder(normalize('/foo\\bar'))(normalize('/foo/bar/baz'))).to.equal(true);
76+
// On Windows, backslash is a path separator, so normalize('/foo\\bar') becomes '/foo/bar'
77+
// On Unix, backslash is a valid filename character, so it stays as '/foo\\bar'
78+
const isWindows = sep === '\\';
79+
expect(pathIsInFolder(normalize('/foo\\bar'))(normalize('/foo/bar/baz'))).to.equal(isWindows);
7780
});
7881

7982
it('handles exact paths', () => {

test/unit/remote/remoteSourceTracking.test.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,12 @@ describe('remoteSourceTrackingService', () => {
601601

602602
queryStub.onFirstCall().resolves([]);
603603
const queryResult = [1, 2, 3].map((rev) => getSourceMember(rev));
604-
// @ts-ignore
605604
queryStub.onSecondCall().resolves(queryResult);
606605

607-
// @ts-ignore
606+
// @ts-expect-error stubbing private method for testing
608607
const trackSpy = $$.SANDBOX.stub(remoteSourceTrackingService, 'trackSourceMembers');
609608

610-
// @ts-ignore
611-
await remoteSourceTrackingService.pollForSourceTracking(memberNames, 2);
609+
await remoteSourceTrackingService.pollForSourceTracking(new RegistryAccess(), memberNames);
612610
// this test changed from toolbelt because each server query now update the tracking files
613611
expect(
614612
trackSpy.calledTwice,
@@ -704,11 +702,10 @@ describe('remoteSourceTrackingService', () => {
704702
it('should stop if the computed pollingTimeout is exceeded', async () => {
705703
const queryStub = $$.SANDBOX.stub(orgQueryMocks, 'querySourceMembersFrom').resolves([]);
706704

707-
// @ts-ignore
705+
// @ts-expect-error stubbing private method for testing
708706
const trackSpy = $$.SANDBOX.stub(remoteSourceTrackingService, 'trackSourceMembers');
709707

710-
// @ts-ignore
711-
await remoteSourceTrackingService.pollForSourceTracking(memberNames);
708+
await remoteSourceTrackingService.pollForSourceTracking(new RegistryAccess(), memberNames);
712709
// changed from toolbelt because each query result goes to tracking
713710
expect(trackSpy.callCount).to.equal(6);
714711
expect(warns.size).to.be.greaterThan(0);
@@ -722,11 +719,10 @@ describe('remoteSourceTrackingService', () => {
722719
reResolveEnvVars();
723720
const queryStub = $$.SANDBOX.stub(orgQueryMocks, 'querySourceMembersFrom').resolves([]);
724721

725-
// @ts-ignore
722+
// @ts-expect-error stubbing private method for testing
726723
const trackSpy = $$.SANDBOX.stub(remoteSourceTrackingService, 'trackSourceMembers');
727724

728-
// @ts-ignore
729-
await remoteSourceTrackingService.pollForSourceTracking(memberNames);
725+
await remoteSourceTrackingService.pollForSourceTracking(new RegistryAccess(), memberNames);
730726
expect(trackSpy.called).to.equal(true);
731727

732728
expect(warns.size).to.be.greaterThan(0);
@@ -739,11 +735,10 @@ describe('remoteSourceTrackingService', () => {
739735
reResolveEnvVars();
740736
const queryStub = $$.SANDBOX.stub(orgQueryMocks, 'querySourceMembersFrom').resolves([]);
741737

742-
// @ts-ignore
738+
// @ts-expect-error stubbing private method for testing
743739
const trackSpy = $$.SANDBOX.stub(remoteSourceTrackingService, 'trackSourceMembers');
744740

745-
// @ts-ignore
746-
await remoteSourceTrackingService.pollForSourceTracking(memberNames);
741+
await remoteSourceTrackingService.pollForSourceTracking(new RegistryAccess(), memberNames);
747742
expect(trackSpy.called).to.equal(true);
748743

749744
expect(warns.size).to.be.greaterThan(0);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import sinon from 'sinon';
17+
import { expect } from 'chai';
18+
import { MockTestOrgData, instantiateContext, stubContext, restoreContext } from '@salesforce/core/testSetup';
19+
import { Lifecycle, Org, SfProject } from '@salesforce/core';
20+
import { SourceTracking } from '../../src/sourceTracking';
21+
22+
describe('SourceTracking lifecycle events cleanup', () => {
23+
const $$ = instantiateContext();
24+
const username = 'test@example.com';
25+
26+
afterEach(() => {
27+
restoreContext($$);
28+
sinon.restore();
29+
});
30+
31+
it('removes pre-event listeners when subscribing to lifecycle events', async () => {
32+
stubContext($$);
33+
const orgData = new MockTestOrgData();
34+
orgData.username = username;
35+
orgData.tracksSource = true;
36+
await $$.stubAuths(orgData);
37+
38+
const org = await Org.create({ aliasOrUsername: username });
39+
const project = SfProject.getInstance();
40+
41+
sinon.stub(project, 'getPackageDirectories').returns([
42+
{
43+
name: 'force-app',
44+
path: 'force-app',
45+
fullPath: '/test/force-app',
46+
default: true,
47+
},
48+
]);
49+
50+
const lifecycle = Lifecycle.getInstance();
51+
const removeAllListenersSpy = sinon.spy(lifecycle, 'removeAllListeners');
52+
53+
await SourceTracking.create({
54+
org,
55+
project,
56+
subscribeSDREvents: true,
57+
});
58+
59+
expect(removeAllListenersSpy.calledWith('scopedPreDeploy')).to.equal(true);
60+
expect(removeAllListenersSpy.calledWith('scopedPreRetrieve')).to.equal(true);
61+
});
62+
63+
it('does not remove listeners when subscribeSDREvents is false', async () => {
64+
stubContext($$);
65+
const orgData = new MockTestOrgData();
66+
orgData.username = username;
67+
orgData.tracksSource = true;
68+
await $$.stubAuths(orgData);
69+
70+
const org = await Org.create({ aliasOrUsername: username });
71+
const project = SfProject.getInstance();
72+
73+
sinon.stub(project, 'getPackageDirectories').returns([
74+
{
75+
name: 'force-app',
76+
path: 'force-app',
77+
fullPath: '/test/force-app',
78+
default: true,
79+
},
80+
]);
81+
82+
const lifecycle = Lifecycle.getInstance();
83+
const removeAllListenersSpy = sinon.spy(lifecycle, 'removeAllListeners');
84+
85+
await SourceTracking.create({
86+
org,
87+
project,
88+
subscribeSDREvents: false,
89+
});
90+
91+
expect(removeAllListenersSpy.calledWith('scopedPreDeploy')).to.equal(false);
92+
expect(removeAllListenersSpy.calledWith('scopedPreRetrieve')).to.equal(false);
93+
});
94+
});

yarn.lock

Lines changed: 6 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -586,22 +586,6 @@
586586
node-fetch "^2.6.1"
587587
xml2js "^0.6.2"
588588

589-
"@jsforce/jsforce-node@^3.10.8":
590-
version "3.10.8"
591-
resolved "https://registry.npmjs.org/@jsforce/jsforce-node/-/jsforce-node-3.10.8.tgz"
592-
integrity sha512-XGD/ivZz+htN5SgctFyEZ+JNG6C8FXzaEwvPbRSdsIy/hpWlexY38XtTpdT5xX3KnYSnOE4zA1M/oIbTm7RD/Q==
593-
dependencies:
594-
"@sindresorhus/is" "^4"
595-
base64url "^3.0.1"
596-
csv-parse "^5.5.2"
597-
csv-stringify "^6.6.0"
598-
faye "^1.4.0"
599-
form-data "^4.0.4"
600-
https-proxy-agent "^5.0.0"
601-
multistream "^3.1.0"
602-
node-fetch "^2.6.1"
603-
xml2js "^0.6.2"
604-
605589
"@jsonjoy.com/base64@^1.1.2":
606590
version "1.1.2"
607591
resolved "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz"
@@ -692,32 +676,7 @@
692676
strip-ansi "6.0.1"
693677
ts-retry-promise "^0.8.1"
694678

695-
"@salesforce/core@^8.23.1", "@salesforce/core@^8.8.0":
696-
version "8.23.3"
697-
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.23.3.tgz#23d92d6eb887e946e26989552a605fa085e626e8"
698-
integrity sha512-BD9cOUOw3wTR8ud6dBacLvA4x0KAfQXkNGdxtU9ujz5nEW86ms5tU1AEUzVXnhuDrrtdQZh7/yTGxqg5mS7rZg==
699-
dependencies:
700-
"@jsforce/jsforce-node" "^3.10.8"
701-
"@salesforce/kit" "^3.2.4"
702-
"@salesforce/schemas" "^1.10.3"
703-
"@salesforce/ts-types" "^2.0.12"
704-
ajv "^8.17.1"
705-
change-case "^4.1.2"
706-
fast-levenshtein "^3.0.0"
707-
faye "^1.4.1"
708-
form-data "^4.0.4"
709-
js2xmlparser "^4.0.1"
710-
jsonwebtoken "9.0.2"
711-
jszip "3.10.1"
712-
memfs "^4.30.1"
713-
pino "^9.7.0"
714-
pino-abstract-transport "^1.2.0"
715-
pino-pretty "^11.3.0"
716-
proper-lockfile "^4.1.2"
717-
semver "^7.7.3"
718-
ts-retry-promise "^0.8.1"
719-
720-
"@salesforce/core@^8.24.0":
679+
"@salesforce/core@^8.23.1", "@salesforce/core@^8.24.0", "@salesforce/core@^8.8.0":
721680
version "8.24.0"
722681
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.24.0.tgz#13426f9f3b5ed0ec126b8009e5eda68e03db0401"
723682
integrity sha512-8Ra5RT95bRkmHmaaFgABwkXbnHNSNS7l9gbJzJgO6VQpaEeytGPPyymnAE7TcTM2xp/QwlXn+PgX4biX7Lb7JA==
@@ -796,10 +755,10 @@
796755
resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.10.3.tgz#52c867fdd60679cf216110aa49542b7ad391f5d1"
797756
integrity sha512-FKfvtrYTcvTXE9advzS25/DEY9yJhEyLvStm++eQFtnAaX1pe4G3oGHgiQ0q55BM5+0AlCh0+0CVtQv1t4oJRA==
798757

799-
"@salesforce/source-deploy-retrieve@^12.31.5":
800-
version "12.31.5"
801-
resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.31.5.tgz#2765c7422acf064d63dea5d2e83b4072610dfb6f"
802-
integrity sha512-x5EQJsvLBzg6IAOYvVjPYHNSNOmKNbzjqwwaeMBgxiVfWzmkYjz6igGY4oUkRw9VgBFrzMEK2sq2SOypSEq/+w==
758+
"@salesforce/source-deploy-retrieve@^12.31.7":
759+
version "12.31.7"
760+
resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.31.7.tgz#d61b85a871fac0a1c7d636190d93917aad76e48f"
761+
integrity sha512-SLfGjJnDdB0J+gwhY7Tt+Hcd6/4Qknqjnk94cTfXxrJy3gepP1SdfR0T0zimMWRXOZ/BH3yLlfBbj5EBRWvznA==
803762
dependencies:
804763
"@salesforce/core" "^8.24.0"
805764
"@salesforce/kit" "^3.2.4"
@@ -3933,22 +3892,6 @@ jsonparse@^1.2.0:
39333892
resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz"
39343893
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
39353894

3936-
jsonwebtoken@9.0.2:
3937-
version "9.0.2"
3938-
resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz"
3939-
integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
3940-
dependencies:
3941-
jws "^3.2.2"
3942-
lodash.includes "^4.3.0"
3943-
lodash.isboolean "^3.0.3"
3944-
lodash.isinteger "^4.0.4"
3945-
lodash.isnumber "^3.0.3"
3946-
lodash.isplainobject "^4.0.6"
3947-
lodash.isstring "^4.0.1"
3948-
lodash.once "^4.0.0"
3949-
ms "^2.1.1"
3950-
semver "^7.5.4"
3951-
39523895
jsonwebtoken@9.0.3:
39533896
version "9.0.3"
39543897
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2"
@@ -3985,15 +3928,6 @@ just-extend@^6.2.0:
39853928
resolved "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz"
39863929
integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==
39873930

3988-
jwa@^1.4.1:
3989-
version "1.4.2"
3990-
resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz"
3991-
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
3992-
dependencies:
3993-
buffer-equal-constant-time "^1.0.1"
3994-
ecdsa-sig-formatter "1.0.11"
3995-
safe-buffer "^5.0.1"
3996-
39973931
jwa@^2.0.1:
39983932
version "2.0.1"
39993933
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
@@ -4003,14 +3937,6 @@ jwa@^2.0.1:
40033937
ecdsa-sig-formatter "1.0.11"
40043938
safe-buffer "^5.0.1"
40053939

4006-
jws@^3.2.2:
4007-
version "3.2.2"
4008-
resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz"
4009-
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
4010-
dependencies:
4011-
jwa "^1.4.1"
4012-
safe-buffer "^5.0.1"
4013-
40143940
jws@^4.0.1:
40153941
version "4.0.1"
40163942
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690"
@@ -5453,12 +5379,7 @@ semver@^6.0.0, semver@^6.3.1:
54535379
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
54545380
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
54555381

5456-
semver@^7.3.4, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3:
5457-
version "7.7.2"
5458-
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
5459-
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
5460-
5461-
semver@^7.7.3:
5382+
semver@^7.3.4, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.3:
54625383
version "7.7.3"
54635384
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
54645385
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==

0 commit comments

Comments
 (0)