diff --git a/packages/dashmate/src/status/scopes/platform.js b/packages/dashmate/src/status/scopes/platform.js index 467e8e4ce0e..3be7baf69f0 100644 --- a/packages/dashmate/src/status/scopes/platform.js +++ b/packages/dashmate/src/status/scopes/platform.js @@ -144,7 +144,15 @@ export default function getPlatformScopeFactory( } info.version = version; - info.protocolVersion = parseInt(tenderdashStatus.node_info.protocol_version.app, 10); + // Prefer live application_info.version (current after in-process upgrades). + // Tenderdash may omit application_info entirely (json omitempty when ABCIInfo + // fails, or older builds without the field). Fall back to + // node_info.protocol_version.app only then — that snapshot can be stale, so it + // must not win when the live field is present. + const appProtocolVersion = tenderdashStatus.application_info?.version + ?? tenderdashStatus.node_info.protocol_version.app; + info.protocolVersion = parseInt(appProtocolVersion, 10); + // abci_info app_version reflects the installed software's desired/supported version. info.desiredProtocolVersion = tenderdashAbciInfo.response.app_version; info.listening = listening; info.latestBlockHeight = latestBlockHeight; diff --git a/packages/dashmate/test/unit/status/scopes/platform.spec.js b/packages/dashmate/test/unit/status/scopes/platform.spec.js index 14f0d88cffe..4055330e1e1 100644 --- a/packages/dashmate/test/unit/status/scopes/platform.spec.js +++ b/packages/dashmate/test/unit/status/scopes/platform.spec.js @@ -78,17 +78,22 @@ describe('getPlatformScopeFactory', () => { mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci version').resolves({ exitCode: 0, out: '1.4.1' }); mockMNOWatchProvider.returns(Promise.resolve('OPEN')); + // node_info.protocol_version.app can be stale after in-process upgrades; + // protocolVersion must come from application_info.version (live). const mockStatus = { node_info: { protocol_version: { p2p: '10', block: '14', - app: '3', + app: '11', }, version: '0', network: 'test', moniker: 'test', }, + application_info: { + version: '3', + }, sync_info: { catching_up: false, latest_app_hash: 'DEADBEEF', @@ -157,6 +162,67 @@ describe('getPlatformScopeFactory', () => { expect(scope).to.deep.equal(expectedScope); }); + it('should fall back to node_info app protocol when application_info is absent', async () => { + mockDetermineDockerStatus.returns(DockerStatusEnum.running); + mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } }); + mockRpcClient.getBlockchainInfo.returns({ + result: { + softforks: { + mn_rr: { active: true, height: 1337 }, + }, + }, + }); + mockDockerCompose.isServiceRunning.returns(true); + mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci status').resolves({ exitCode: 0, out: '' }); + mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci version').resolves({ exitCode: 0, out: '1.4.1' }); + mockMNOWatchProvider.returns(Promise.resolve('OPEN')); + + // When application_info is omitted (omitempty / unavailable), status must still + // report a numeric protocolVersion from node_info.protocol_version.app. + const mockStatus = { + node_info: { + protocol_version: { + p2p: '10', + block: '14', + app: '3', + }, + version: '0', + network: 'test', + moniker: 'test', + }, + sync_info: { + catching_up: false, + latest_app_hash: 'DEADBEEF', + latest_block_height: 1, + latest_block_hash: 'DEADBEEF', + latest_block_time: 1337, + }, + }; + const mockNetInfo = { n_peers: 6, listening: true }; + const mockAbciInfo = { + response: { + version: '1.4.1', + app_version: 4, + last_block_height: 90, + last_block_app_hash: 's0CySQxgRg96DrnJ7HCsql+k/Sk4JiT3y0psCaUI3TI=', + }, + }; + + mockFetch + .onFirstCall() + .returns(Promise.resolve({ json: () => Promise.resolve(mockStatus) })) + .onSecondCall() + .returns(Promise.resolve({ json: () => Promise.resolve(mockNetInfo) })) + .onThirdCall() + .resolves({ json: () => Promise.resolve(mockAbciInfo) }); + + const scope = await getPlatformScope(config); + + expect(scope.tenderdash.serviceStatus).to.equal(ServiceStatusEnum.up); + expect(scope.tenderdash.protocolVersion).to.equal(3); + expect(scope.tenderdash.desiredProtocolVersion).to.equal(4); + }); + it('should return platform syncing when it is catching up', async () => { mockDetermineDockerStatus.returns(DockerStatusEnum.running); mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } }); @@ -182,6 +248,9 @@ describe('getPlatformScopeFactory', () => { network: 'test', moniker: 'test', }, + application_info: { + version: '3', + }, sync_info: { catching_up: true, latest_app_hash: 'DEADBEEF', @@ -445,6 +514,9 @@ describe('getPlatformScopeFactory', () => { network: 'test', moniker: 'test', }, + application_info: { + version: '3', + }, sync_info: { catching_up: false, latest_app_hash: 'DEADBEEF',