Skip to content

Commit bff62fc

Browse files
authored
Optimize unit-test runtime (~89% reduction) (#406)
* test(sdk): pass pollIntervalSeconds:0 in cartridge import tests Nine `addCartridge`/`removeCartridge`/`setCartridgePath` tests in `cartridges.test.ts` exercise the Business-Manager import-export fallback, which calls `waitForJob` with a default 3s poll interval. Because the MSW handler resolves the job immediately, the 3s sleep is pure dead time. This change passes `{waitOptions: {pollIntervalSeconds: 0}}` (existing public option on `CartridgeUpdateOptions`) so the tests skip the artificial wait. No production-code change. Metric: pnpm run test:agent 40.2s → 14.4s (-64%). * chore: run package test suites concurrently with pnpm -r --parallel By default `pnpm -r run` walks the workspace in topological order, so SDK + mrt run first, then CLI + MCP. None of the package test:agent commands actually need each other's build artifacts (tests import source TS via the `development` workspace condition), so we can run them all concurrently. Metric: pnpm run test:agent 14.4s → 8.8s (-39%). * test(cli): pass interval:0 in scapi replications wait tests Four `scapi replications wait` tests pass `interval: 1` (one real second per poll) along with a mocked `fetch` that returns immediately. Set `interval: 0` so the in-test polling loop runs at memory speed. The flag is already integer-typed and treats 0 as a valid no-wait value. Metric: cli package test 4s → 3s. * feat(sdk): inject `now()` clock into waitForEnv `waitForEnv` already accepts a custom `sleep` function for tests, but the timeout check still reads `Date.now()` directly, so the existing 'should timeout after specified duration' test waits ~1s of real wall time even though `instantSleep` is in use. Add an optional `now?: () => number` option that mirrors the existing `sleep` injection. The MRT timeout test passes a virtual clock that advances 1s per call, making the test instant. Metric: SDK package test 4s → 3s. * test(sdk): run mocha --parallel --jobs 4 The SDK package has 114 unit-test files and is the wall-clock bottleneck of the workspace test run. Mocha's parallel mode shards files across worker processes; jobs=4 is empirically the sweet spot under root pnpm -r --parallel (jobs=8 oversubscribes the 16-CPU machine and degrades wall time when CLI is also running parallel workers). Metric: pnpm run test:agent 8.9s → 5.2s (-41%). * test(cli): run mocha --parallel with per-worker auth-store isolation The CLI's stateful auth store is a JSON file in the oclif data directory (e.g. `~/Library/Application Support/@salesforce/b2c-cli/auth-session.json`). Under mocha --parallel, every worker process shares this file, so concurrent `setStoredSession`/`clearStoredSession` calls race and tests fail intermittently. This change: - Adds an optional `B2C_TEST_DATA_DIR` env override in `BaseCommand.init()` so tests can redirect the store path. - Has the CLI test setup create a unique per-process `mkdtemp` directory, set `B2C_TEST_DATA_DIR`, and call `initializeStatefulStore(testDataDir)` for the worker. The mocha `beforeEach` hook re-asserts the path before each test (in case a test called `resetStatefulStoreForTesting`). - Enables `mocha --parallel --jobs 4` for the CLI package. Metric: pnpm run test:agent 5.2s → 4.5s (-13%). Cumulative: 40.2s → 4.5s (-89%).
1 parent 23205eb commit bff62fc

9 files changed

Lines changed: 69 additions & 30 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"start": "pnpm --filter @salesforce/b2c-cli run dev",
88
"test": "pnpm -r test",
99
"test:unit": "pnpm -r run test:unit",
10-
"test:agent": "pnpm -r run test:agent",
10+
"test:agent": "pnpm -r --parallel run test:agent",
1111
"coverage": "pnpm -r run coverage",
1212
"format": "pnpm -r run format",
1313
"lint": "pnpm -r run lint",

packages/b2c-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@
337337
"test:ci": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
338338
"test:ci:win": "c8 --check-coverage=false env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
339339
"test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
340-
"test:agent": "env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter min --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
340+
"test:agent": "env OCLIF_TEST_ROOT=. mocha --forbid-only --parallel --jobs 4 --reporter min --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
341341
"test:e2e": "env TEST_USE_SHARED_SANDBOX=true OCLIF_TEST_ROOT=. mocha --forbid-only --require test/functional/e2e/hooks.ts --node-option import=tsx --timeout 30000 --retries 2 --reporter spec \"test/functional/e2e/**/*.test.ts\"",
342342
"test:e2e:ci": "env TEST_USE_SHARED_SANDBOX=true OCLIF_TEST_ROOT=. mocha --forbid-only --require test/functional/e2e/hooks.ts --node-option import=tsx --timeout 30000 --retries 2 --reporter json --reporter-option output=test-results.json \"test/functional/e2e/**/*.test.ts\"",
343343
"test:e2e:auth": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/functional/e2e/auth-token.test.ts\"",

packages/b2c-cli/test/commands/scapi/replications/wait.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('scapi replications wait', () => {
3030

3131
it('waits for process to complete', async () => {
3232
const command: any = new ReplicationsWait([], config);
33-
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 1}, {'process-id': 'proc-123'});
33+
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 0}, {'process-id': 'proc-123'});
3434
await command.init();
3535

3636
sinon.stub(command, 'requireOAuthCredentials').returns(void 0);
@@ -74,7 +74,7 @@ describe('scapi replications wait', () => {
7474

7575
it('returns failed status', async () => {
7676
const command: any = new ReplicationsWait([], config);
77-
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 1}, {'process-id': 'proc-456'});
77+
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 0}, {'process-id': 'proc-456'});
7878
await command.init();
7979

8080
sinon.stub(command, 'requireOAuthCredentials').returns(void 0);
@@ -102,7 +102,7 @@ describe('scapi replications wait', () => {
102102

103103
it('logs status updates in non-JSON mode', async () => {
104104
const command: any = new ReplicationsWait([], config);
105-
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 1}, {'process-id': 'proc-789'});
105+
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 0}, {'process-id': 'proc-789'});
106106
await command.init();
107107

108108
sinon.stub(command, 'requireOAuthCredentials').returns(void 0);
@@ -164,7 +164,7 @@ describe('scapi replications wait', () => {
164164

165165
it('handles API errors during polling', async () => {
166166
const command: any = new ReplicationsWait([], config);
167-
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 1}, {'process-id': 'proc-error'});
167+
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 0}, {'process-id': 'proc-error'});
168168
await command.init();
169169

170170
sinon.stub(command, 'requireOAuthCredentials').returns(void 0);

packages/b2c-cli/test/setup.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,35 @@
1212
* - Clears all SDK global registries before each test to prevent state leakage
1313
*/
1414
import {globalMiddlewareRegistry} from '@salesforce/b2c-tooling-sdk/clients';
15-
import {globalAuthMiddlewareRegistry} from '@salesforce/b2c-tooling-sdk/auth';
15+
import {
16+
globalAuthMiddlewareRegistry,
17+
initializeStatefulStore,
18+
clearStoredSession,
19+
} from '@salesforce/b2c-tooling-sdk/auth';
1620
import {globalConfigSourceRegistry} from '@salesforce/b2c-tooling-sdk/config';
21+
import {mkdtempSync} from 'node:fs';
22+
import {tmpdir} from 'node:os';
23+
import {join} from 'node:path';
1724

1825
// Prevent BaseCommand from running plugin hooks during tests
1926
process.env.B2C_SKIP_PLUGIN_HOOKS = '1';
2027

28+
// Isolate the stateful auth session store to a unique per-process temp dir.
29+
// Both the test setup and BaseCommand.init() will use this path, so tests
30+
// (or parallel mocha workers) don't race on the developer's real auth file at
31+
// ~/Library/Application Support/@salesforce/b2c-cli/auth-session.json.
32+
const testDataDir = mkdtempSync(join(tmpdir(), 'b2c-cli-test-'));
33+
process.env.B2C_TEST_DATA_DIR = testDataDir;
34+
initializeStatefulStore(testDataDir);
35+
2136
export const mochaHooks = {
2237
beforeEach() {
2338
globalMiddlewareRegistry.clear();
2439
globalAuthMiddlewareRegistry.clear();
2540
globalConfigSourceRegistry.clear();
41+
// Re-isolate stateful store path before every test in case a prior test
42+
// called resetStatefulStoreForTesting(), and clear any leftover session.
43+
initializeStatefulStore(testDataDir);
44+
clearStoredSession();
2645
},
2746
};

packages/b2c-tooling-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@
411411
"test": "c8 mocha --forbid-only \"test/**/*.test.ts\"",
412412
"test:ci": "c8 mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
413413
"test:unit": "mocha --forbid-only \"test/**/*.test.ts\"",
414-
"test:agent": "mocha --forbid-only --reporter min \"test/**/*.test.ts\"",
414+
"test:agent": "mocha --forbid-only --parallel --jobs 4 --reporter min \"test/**/*.test.ts\"",
415415
"test:watch": "mocha --watch \"test/**/*.test.ts\"",
416416
"coverage": "c8 report",
417417
"posttest": "pnpm run lint",

packages/b2c-tooling-sdk/src/cli/base-command.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,10 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
166166
this.configureLogging();
167167

168168
// Initialize stateful auth store with oclif's data directory so session
169-
// files are stored alongside other CLI data (e.g. ~/Library/Application Support/@salesforce/b2c-cli)
170-
initializeStatefulStore(this.config.dataDir);
169+
// files are stored alongside other CLI data (e.g. ~/Library/Application Support/@salesforce/b2c-cli).
170+
// Tests may override the path via B2C_TEST_DATA_DIR to isolate the auth-session.json
171+
// file (e.g. per mocha worker) so they don't race on the developer's real session file.
172+
initializeStatefulStore(process.env.B2C_TEST_DATA_DIR ?? this.config.dataDir);
171173

172174
// Set CLI User-Agent (CLI name/version only, without @salesforce/ prefix)
173175
// This must happen before any API clients are created

packages/b2c-tooling-sdk/src/operations/mrt/env.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ export interface WaitForEnvOptions extends GetEnvOptions {
390390
* Custom sleep function for testing.
391391
*/
392392
sleep?: (ms: number) => Promise<void>;
393+
394+
/**
395+
* Custom clock for testing. Defaults to Date.now.
396+
*/
397+
now?: () => number;
393398
}
394399

395400
async function defaultSleep(ms: number): Promise<void> {
@@ -441,7 +446,8 @@ export async function waitForEnv(options: WaitForEnvOptions, auth: AuthStrategy)
441446
const {projectSlug, slug, pollIntervalSeconds = 10, timeoutSeconds = 2700, onPoll, origin} = options;
442447

443448
const sleepFn = options.sleep ?? defaultSleep;
444-
const startTime = Date.now();
449+
const nowFn = options.now ?? Date.now;
450+
const startTime = nowFn();
445451
const pollIntervalMs = pollIntervalSeconds * 1000;
446452
const timeoutMs = timeoutSeconds * 1000;
447453

@@ -450,9 +456,9 @@ export async function waitForEnv(options: WaitForEnvOptions, auth: AuthStrategy)
450456
await sleepFn(pollIntervalMs);
451457

452458
while (true) {
453-
const elapsedSeconds = Math.round((Date.now() - startTime) / 1000);
459+
const elapsedSeconds = Math.round((nowFn() - startTime) / 1000);
454460

455-
if (timeoutSeconds > 0 && Date.now() - startTime > timeoutMs) {
461+
if (timeoutSeconds > 0 && nowFn() - startTime > timeoutMs) {
456462
throw new Error(`Timeout waiting for environment "${slug}" after ${timeoutSeconds}s`);
457463
}
458464

packages/b2c-tooling-sdk/test/operations/mrt/env.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,13 @@ describe('operations/mrt/env', () => {
312312

313313
const auth = new MockAuthStrategy();
314314

315+
// Virtual clock that advances by 1s on every call to simulate timeout without real waiting.
316+
let virtualNow = 0;
317+
const fakeNow = () => {
318+
virtualNow += 1000;
319+
return virtualNow;
320+
};
321+
315322
try {
316323
await waitForEnv(
317324
{
@@ -320,6 +327,7 @@ describe('operations/mrt/env', () => {
320327
pollIntervalSeconds: 1,
321328
timeoutSeconds: 1,
322329
sleep: instantSleep,
330+
now: fakeNow,
323331
},
324332
auth,
325333
);

packages/b2c-tooling-sdk/test/operations/sites/cartridges.test.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
} from '../../../src/operations/sites/cartridges.js';
2121

2222
const TEST_HOST = 'test.demandware.net';
23+
// Skip the 3s job-poll interval in tests; the mocked job completes immediately.
24+
const FAST_POLL = {waitOptions: {pollIntervalSeconds: 0}};
2325
const BASE_URL = `https://${TEST_HOST}/s/-/dw/data/v25_6`;
2426
const WEBDAV_BASE = `https://${TEST_HOST}/on/demandware.servlet/webdav/Sites`;
2527

@@ -167,7 +169,7 @@ describe('operations/sites/cartridges', () => {
167169
}),
168170
);
169171

170-
const result = await addCartridge(mockInstance, 'Sites-Site', {name: 'new_cart', position: 'first'});
172+
const result = await addCartridge(mockInstance, 'Sites-Site', {name: 'new_cart', position: 'first'}, FAST_POLL);
171173

172174
expect(result.cartridges).to.equal('new_cart:existing_cart');
173175
expect(importedBuffer).to.exist;
@@ -195,7 +197,7 @@ describe('operations/sites/cartridges', () => {
195197
...createImportHandlers(),
196198
);
197199

198-
const result = await addCartridge(mockInstance, 'RefArch', {name: 'cart_c', position: 'last'});
200+
const result = await addCartridge(mockInstance, 'RefArch', {name: 'cart_c', position: 'last'}, FAST_POLL);
199201

200202
expect(result.cartridges).to.equal('cart_a:cart_b:cart_c');
201203
});
@@ -222,7 +224,7 @@ describe('operations/sites/cartridges', () => {
222224
...createImportHandlers(),
223225
);
224226

225-
const result = await removeCartridge(mockInstance, 'Sites-Site', 'cart_a');
227+
const result = await removeCartridge(mockInstance, 'Sites-Site', 'cart_a', FAST_POLL);
226228

227229
expect(result.cartridges).to.equal('cart_b');
228230
});
@@ -269,7 +271,7 @@ describe('operations/sites/cartridges', () => {
269271
}),
270272
);
271273

272-
const result = await setCartridgePath(mockInstance, 'Sites-Site', 'bm_cart1:bm_cart2');
274+
const result = await setCartridgePath(mockInstance, 'Sites-Site', 'bm_cart1:bm_cart2', FAST_POLL);
273275

274276
expect(result.cartridges).to.equal('bm_cart1:bm_cart2');
275277

@@ -293,7 +295,7 @@ describe('operations/sites/cartridges', () => {
293295
}),
294296
);
295297

296-
await setCartridgePath(mockInstance, 'RefArch', 'cart1:cart2');
298+
await setCartridgePath(mockInstance, 'RefArch', 'cart1:cart2', FAST_POLL);
297299

298300
const zip = await JSZip.loadAsync(importedBuffer!);
299301
const files = Object.keys(zip.files);
@@ -317,30 +319,32 @@ describe('operations/sites/cartridges', () => {
317319
});
318320

319321
it('should add at first position', async () => {
320-
const result = await addCartridge(mockInstance, 'Sites-Site', {name: 'new', position: 'first'});
322+
const result = await addCartridge(mockInstance, 'Sites-Site', {name: 'new', position: 'first'}, FAST_POLL);
321323
expect(result.cartridgeList).to.deep.equal(['new', 'cart_a', 'cart_b', 'cart_c']);
322324
});
323325

324326
it('should add at last position', async () => {
325-
const result = await addCartridge(mockInstance, 'Sites-Site', {name: 'new', position: 'last'});
327+
const result = await addCartridge(mockInstance, 'Sites-Site', {name: 'new', position: 'last'}, FAST_POLL);
326328
expect(result.cartridgeList).to.deep.equal(['cart_a', 'cart_b', 'cart_c', 'new']);
327329
});
328330

329331
it('should add before target', async () => {
330-
const result = await addCartridge(mockInstance, 'Sites-Site', {
331-
name: 'new',
332-
position: 'before',
333-
target: 'cart_b',
334-
});
332+
const result = await addCartridge(
333+
mockInstance,
334+
'Sites-Site',
335+
{name: 'new', position: 'before', target: 'cart_b'},
336+
FAST_POLL,
337+
);
335338
expect(result.cartridgeList).to.deep.equal(['cart_a', 'new', 'cart_b', 'cart_c']);
336339
});
337340

338341
it('should add after target', async () => {
339-
const result = await addCartridge(mockInstance, 'Sites-Site', {
340-
name: 'new',
341-
position: 'after',
342-
target: 'cart_b',
343-
});
342+
const result = await addCartridge(
343+
mockInstance,
344+
'Sites-Site',
345+
{name: 'new', position: 'after', target: 'cart_b'},
346+
FAST_POLL,
347+
);
344348
expect(result.cartridgeList).to.deep.equal(['cart_a', 'cart_b', 'new', 'cart_c']);
345349
});
346350

0 commit comments

Comments
 (0)