From b71137bed2f2d216e4259b23a1e7e2f866af5017 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Mon, 20 Jul 2026 17:08:28 +0530 Subject: [PATCH 1/2] Fix: Reconcile completed progress bar counts with processed items Co-Authored-By: Claude Opus 4.8 --- .../progress-summary/cli-progress-manager.ts | 13 ++-- .../test/unit/cliProgressManager.test.ts | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts b/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts index bf0d843e74..07a9dd25b3 100644 --- a/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts +++ b/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts @@ -421,14 +421,19 @@ export default class CLIProgressManager { if (!this.showConsoleLogs && process.progressBar) { const totalProcessed = process.current; - const percentage = Math.round((totalProcessed / process.total) * 100); + // Reconcile the bar's total with the number of items actually processed so the + // rendered "value/total" matches the "Complete (success/processed)" summary. A + // process registered with an estimated total larger than the items actually ticked + // would otherwise render mismatched counts, e.g. "37/37 | Complete (27/27)". + process.progressBar.setTotal(totalProcessed); + const percentage = totalProcessed > 0 ? 100 : 0; const formattedPercentage = this.formatPercentage(percentage); const statusText = success - ? getChalk().green(`✓ Complete (${process.successCount}/${process.current})`) - : getChalk().red(`✗ Failed (${process.successCount}/${process.current})`); + ? getChalk().green(`✓ Complete (${process.successCount}/${totalProcessed})`) + : getChalk().red(`✗ Failed (${process.successCount}/${totalProcessed})`); const displayName = this.formatProcessName(processName); const indentedLabel = ` ├─ ${displayName}`.padEnd(25); - process.progressBar.update(process.total, { + process.progressBar.update(totalProcessed, { label: success ? getChalk().green(indentedLabel) : getChalk().red(indentedLabel), status: statusText, percentage: formattedPercentage, diff --git a/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts b/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts index cb0752b48d..3d1db0dd11 100644 --- a/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts +++ b/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts @@ -34,6 +34,7 @@ const mockProgressBar = { stop: sinon.stub(), increment: sinon.stub(), update: sinon.stub(), + setTotal: sinon.stub(), }; const mockMultiBar = { @@ -335,6 +336,64 @@ describe('CLIProgressManager', () => { }); }); + describe('Complete count reconciliation (DX-7521)', () => { + beforeEach(() => { + mockProgressBar.update.resetHistory(); + mockProgressBar.setTotal.resetHistory(); + mockProgressBar.increment.resetHistory(); + progressManager = new CLIProgressManager({ + enableNestedProgress: true, + moduleName: 'RECONCILE_TEST', + showConsoleLogs: false, + }); + }); + + fancy.it('renders the processed count, not the registered estimate, when fewer items were ticked', () => { + progressManager.addProcess('gf-update', 37); + for (let i = 0; i < 27; i++) { + progressManager.tick(true, `item-${i}`, null, 'gf-update'); + } + + progressManager.completeProcess('gf-update', true); + + expect(mockProgressBar.setTotal.calledWith(27)).to.equal(true); + expect(mockProgressBar.update.lastCall.args[0]).to.equal(27); + }); + + fancy.it('renders the full count unchanged when every registered item was ticked', () => { + progressManager.addProcess('entries-create', 59); + for (let i = 0; i < 59; i++) { + progressManager.tick(true, `item-${i}`, null, 'entries-create'); + } + + progressManager.completeProcess('entries-create', true); + + expect(mockProgressBar.update.lastCall.args[0]).to.equal(59); + }); + + fancy.it('renders the registered total when a process completes with no ticks (skip case)', () => { + progressManager.addProcess('skipped', 5); + + progressManager.completeProcess('skipped', true); + + expect(mockProgressBar.update.lastCall.args[0]).to.equal(5); + }); + + fancy.it('reconciles the denominator for a failed process with partial ticks', () => { + progressManager.addProcess('entries', 59); + for (let i = 0; i < 38; i++) { + progressManager.tick(true, `ok-${i}`, null, 'entries'); + } + for (let i = 0; i < 3; i++) { + progressManager.tick(false, `err-${i}`, 'boom', 'entries'); + } + + progressManager.completeProcess('entries', false); + + expect(mockProgressBar.update.lastCall.args[0]).to.equal(41); + }); + }); + describe('Progress Tracking', () => { beforeEach(() => { progressManager = new CLIProgressManager({ From bb5465987658fce7d923f468c6ecd2f9468e14d7 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Mon, 20 Jul 2026 17:20:39 +0530 Subject: [PATCH 2/2] test: make progress reconciliation tests independent of cli-progress mock The module-level cli-progress mock binds by load order and does not take effect in CI, so the reconciliation tests saw the real bar and failed. Inject a stub bar at the rendering seam instead, and drop the ticket number from the describe title. Co-Authored-By: Claude Opus 4.8 --- .../test/unit/cliProgressManager.test.ts | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts b/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts index 3d1db0dd11..ab6f20ab35 100644 --- a/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts +++ b/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts @@ -336,11 +336,17 @@ describe('CLIProgressManager', () => { }); }); - describe('Complete count reconciliation (DX-7521)', () => { + describe('Complete count reconciliation', () => { + let bar: { update: sinon.SinonStub; setTotal: sinon.SinonStub; increment: sinon.SinonStub }; + + // Inject a stub bar at the rendering seam so assertions do not depend on the + // load-order-sensitive cli-progress module mock (which does not bind in CI). + function injectBar(processName: string) { + bar = { update: sinon.stub(), setTotal: sinon.stub(), increment: sinon.stub() }; + (progressManager as any).processes.get(processName).progressBar = bar; + } + beforeEach(() => { - mockProgressBar.update.resetHistory(); - mockProgressBar.setTotal.resetHistory(); - mockProgressBar.increment.resetHistory(); progressManager = new CLIProgressManager({ enableNestedProgress: true, moduleName: 'RECONCILE_TEST', @@ -350,37 +356,41 @@ describe('CLIProgressManager', () => { fancy.it('renders the processed count, not the registered estimate, when fewer items were ticked', () => { progressManager.addProcess('gf-update', 37); + injectBar('gf-update'); for (let i = 0; i < 27; i++) { progressManager.tick(true, `item-${i}`, null, 'gf-update'); } progressManager.completeProcess('gf-update', true); - expect(mockProgressBar.setTotal.calledWith(27)).to.equal(true); - expect(mockProgressBar.update.lastCall.args[0]).to.equal(27); + expect(bar.setTotal.calledWith(27)).to.equal(true); + expect(bar.update.lastCall.args[0]).to.equal(27); }); fancy.it('renders the full count unchanged when every registered item was ticked', () => { progressManager.addProcess('entries-create', 59); + injectBar('entries-create'); for (let i = 0; i < 59; i++) { progressManager.tick(true, `item-${i}`, null, 'entries-create'); } progressManager.completeProcess('entries-create', true); - expect(mockProgressBar.update.lastCall.args[0]).to.equal(59); + expect(bar.update.lastCall.args[0]).to.equal(59); }); fancy.it('renders the registered total when a process completes with no ticks (skip case)', () => { progressManager.addProcess('skipped', 5); + injectBar('skipped'); progressManager.completeProcess('skipped', true); - expect(mockProgressBar.update.lastCall.args[0]).to.equal(5); + expect(bar.update.lastCall.args[0]).to.equal(5); }); fancy.it('reconciles the denominator for a failed process with partial ticks', () => { progressManager.addProcess('entries', 59); + injectBar('entries'); for (let i = 0; i < 38; i++) { progressManager.tick(true, `ok-${i}`, null, 'entries'); } @@ -390,7 +400,7 @@ describe('CLIProgressManager', () => { progressManager.completeProcess('entries', false); - expect(mockProgressBar.update.lastCall.args[0]).to.equal(41); + expect(bar.update.lastCall.args[0]).to.equal(41); }); });