From e26cb32c3a9f0d7a39a7898ebba7c0c0d13860d5 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 3 Sep 2026 15:06:58 +0300 Subject: [PATCH 1/4] cs 2e2 tests fixes --- .../chain-simulator/utils/testSequencer.js | 2 +- .../websocket.subscriptions.cs-e2e.ts | 53 +++++++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/test/chain-simulator/utils/testSequencer.js b/src/test/chain-simulator/utils/testSequencer.js index f1a70a4a8..5fcdfa1b9 100644 --- a/src/test/chain-simulator/utils/testSequencer.js +++ b/src/test/chain-simulator/utils/testSequencer.js @@ -13,7 +13,7 @@ class CustomSequencer extends Sequencer { 'delegation-legacy.cs-e2e.ts', 'accounts.cs-e2e.ts', 'stake.cs-e2e.ts', - 'round.cs-e2e.ts', + 'rounds.cs-e2e.ts', 'results.cs-e2e.ts', 'miniblocks.cs-e2e.ts', ]; diff --git a/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts b/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts index 7721f4508..2d155cf18 100644 --- a/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts +++ b/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts @@ -80,6 +80,33 @@ const aliceEsdts: string[] = []; describe('Websocket subscriptions e2e tests', () => { const clients: Socket[] = []; + const connectionErrors: string[] = []; + + // auto-reconnect is disabled on purpose: the subscriptions below are emitted from the 'connect' + // handler, so every reconnect would re-subscribe and the shared response arrays would collect the + // same message twice. connections are instead retried explicitly, before any operation is sent + const socketOptions = { path: '/ws/subscription', reconnection: false }; + + const waitForConnections = async (timeoutMs: number) => { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + if (clients.every(client => client.connected)) { + return; + } + + for (const client of clients) { + if (!client.connected) { + client.connect(); + } + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + const pending = clients.filter(client => !client.connected).length; + throw new Error(`${pending} of ${clients.length} websocket clients did not connect within ${timeoutMs}ms. Errors: ${connectionErrors.join('; ') || 'none reported'}`); + }; // --- Connect Helper --- const connectAndSubscribe = ( @@ -97,13 +124,13 @@ describe('Websocket subscriptions e2e tests', () => { eventResponses.set(filterKey, receivedEvents); transferResponses.set(filterKey, receivedTransfers); - const client: Socket = io(WS_SERVER_URL, { - path: '/ws/subscription', - }); + const client: Socket = io(WS_SERVER_URL, socketOptions); clients.push(client); + // never throw from a socket callback: it escapes as an uncaughtException that jest attributes + // to whichever test happens to be running, in any file. waitForConnections reports it instead client.on("connect_error", (err) => { - throw new Error(`${clientId} connection failed: ${err.message}`); + connectionErrors.push(`${clientId}: ${err.message}`); }); client.on("customTransactionUpdate", (data: { transactions: any[] }) => { @@ -137,12 +164,10 @@ describe('Websocket subscriptions e2e tests', () => { }; const connectAndSubscribeGeneral = (clientId: string, subConfig: typeof client4SubscriptionConfig) => { - const client: Socket = io(WS_SERVER_URL, { - path: '/ws/subscription', - }); + const client: Socket = io(WS_SERVER_URL, socketOptions); clients.push(client); - client.on("connect_error", (err) => { throw new Error(`${clientId} connection failed: ${err.message}`); }); + client.on("connect_error", (err) => { connectionErrors.push(`${clientId}: ${err.message}`); }); client.on("poolUpdate", (data: any) => generalResponses.pool.push(data)); client.on("eventsUpdate", (data: any) => generalResponses.events.push(data)); @@ -185,6 +210,8 @@ describe('Websocket subscriptions e2e tests', () => { connectAndSubscribeGeneral("client4", client4SubscriptionConfig); + await waitForConnections(30000); + await new Promise(resolve => setTimeout(resolve, 10000)); log("\n--- Starting Operations ---"); @@ -216,7 +243,15 @@ describe('Websocket subscriptions e2e tests', () => { }); afterAll(() => { - clients.forEach(client => client.connected && client.disconnect()); + // unconditionally: a client that is disconnected or mid-handshake would otherwise be skipped + // and keep its handlers and timers alive for the rest of the run, which is shared (--runInBand) + for (const client of clients) { + client.removeAllListeners(); + client.disconnect(); + client.close(); + } + + clients.length = 0; }); it('should receive TXs sent by Alice for Client 1', () => { From 82be978dc692c41697c8457347f936af0bf52b0a Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 3 Sep 2026 15:37:54 +0300 Subject: [PATCH 2/4] fix ws subscriptions --- .../chain-simulator/websocket.subscriptions.cs-e2e.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts b/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts index 2d155cf18..cf6df420b 100644 --- a/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts +++ b/src/test/chain-simulator/websocket.subscriptions.cs-e2e.ts @@ -234,7 +234,15 @@ describe('Websocket subscriptions e2e tests', () => { await axios.post(`${config.chainSimulatorUrl}/simulator/generate-blocks/10`); log("Waiting for WS messages..."); - await new Promise(resolve => setTimeout(resolve, 35000)); + + // the broadcaster commits its cursor only after it sees the round that follows the one it just + // sent. a simulator that has stopped producing blocks never provides that round, so it times + // out and replays the whole window on its next tick. one block per wait step keeps it moving, + // which is why this is a loop of short sleeps rather than a single long one + for (let i = 0; i < 30; i++) { + await axios.post(`${config.chainSimulatorUrl}/simulator/generate-blocks/1`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } } catch (e: any) { console.error("Error in beforeAll:", e.message); From ca893a2221180f4998142c100fd1808687dd198e Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 3 Sep 2026 15:54:21 +0300 Subject: [PATCH 3/4] wait for api instead of hardcoded timeout --- .../utils/prepare-test-data.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/test/chain-simulator/utils/prepare-test-data.ts b/src/test/chain-simulator/utils/prepare-test-data.ts index e02a454b9..c78889cd6 100644 --- a/src/test/chain-simulator/utils/prepare-test-data.ts +++ b/src/test/chain-simulator/utils/prepare-test-data.ts @@ -1,7 +1,29 @@ +import axios from 'axios'; import { config } from '../config/env.config'; import { fundAddress, issueMultipleEsdts, issueMultipleMetaESDTCollections, issueMultipleNftsCollections } from './chain.simulator.operations'; import { ChainSimulatorUtils } from './test.utils'; +async function waitForApi(description: string, url: string, expected: number, timeoutMs: number = 180000) { + const deadline = Date.now() + timeoutMs; + let last = 0; + + while (Date.now() < deadline) { + try { + last = (await axios.get(url)).data; + if (last >= expected) { + console.log(`✓ ${description}: ${last}`); + return; + } + } catch (error: any) { + last = -1; + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + + throw new Error(`${description}: reached ${last}, expected at least ${expected}, after ${timeoutMs}ms (${url})`); +} + async function prepareTestData() { try { console.log('Starting test data preparation...'); @@ -25,7 +47,11 @@ async function prepareTestData() { await ChainSimulatorUtils.deployPingPongSc(config.aliceAddress); console.log('✓ Deployed PingPong smart contract'); - await new Promise((resolve) => setTimeout(resolve, 30000)); + await waitForApi('Tokens listed by the API', `${config.apiServiceUrl}/tokens/count`, 5); + await waitForApi('Tokens listed on the issuer account', `${config.apiServiceUrl}/accounts/${config.aliceAddress}/tokens/count`, 5); + // 2 NFT + 2 SFT collections, five items each; the meta-esdt ones come on top of that + await waitForApi('Collections listed by the API', `${config.apiServiceUrl}/collections/count`, 4); + await waitForApi('NFTs listed by the API', `${config.apiServiceUrl}/nfts/count`, 20); console.log('Test data preparation completed successfully!'); } catch (error) { From e0dc00e8d2dcee1d6a38d3b470bcc38a326924d5 Mon Sep 17 00:00:00 2001 From: GuticaStefan Date: Thu, 3 Sep 2026 16:14:44 +0300 Subject: [PATCH 4/4] wati for api to index --- .../utils/prepare-test-data.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/test/chain-simulator/utils/prepare-test-data.ts b/src/test/chain-simulator/utils/prepare-test-data.ts index c78889cd6..9e2ce0e25 100644 --- a/src/test/chain-simulator/utils/prepare-test-data.ts +++ b/src/test/chain-simulator/utils/prepare-test-data.ts @@ -3,25 +3,28 @@ import { config } from '../config/env.config'; import { fundAddress, issueMultipleEsdts, issueMultipleMetaESDTCollections, issueMultipleNftsCollections } from './chain.simulator.operations'; import { ChainSimulatorUtils } from './test.utils'; -async function waitForApi(description: string, url: string, expected: number, timeoutMs: number = 180000) { +// the api serves most of this from caches the cache warmer fills on its own crons, so what the chain +// reports says nothing about what the tests will see. wait on the api's own view instead, per kind of +// data, rather than on a single interval that has to be long enough for the slowest of them +async function waitForApi(description: string, url: string, isReady: (data: any) => boolean, timeoutMs: number = 180000) { const deadline = Date.now() + timeoutMs; - let last = 0; + let last: any = 'no response yet'; while (Date.now() < deadline) { try { last = (await axios.get(url)).data; - if (last >= expected) { - console.log(`✓ ${description}: ${last}`); + if (isReady(last)) { + console.log(`✓ ${description}`); return; } } catch (error: any) { - last = -1; + last = error.message; } await new Promise((resolve) => setTimeout(resolve, 5000)); } - throw new Error(`${description}: reached ${last}, expected at least ${expected}, after ${timeoutMs}ms (${url})`); + throw new Error(`${description}: still not ready after ${timeoutMs}ms. ${url} last returned ${JSON.stringify(last).slice(0, 300)}`); } async function prepareTestData() { @@ -47,11 +50,17 @@ async function prepareTestData() { await ChainSimulatorUtils.deployPingPongSc(config.aliceAddress); console.log('✓ Deployed PingPong smart contract'); - await waitForApi('Tokens listed by the API', `${config.apiServiceUrl}/tokens/count`, 5); - await waitForApi('Tokens listed on the issuer account', `${config.apiServiceUrl}/accounts/${config.aliceAddress}/tokens/count`, 5); + await waitForApi('Tokens listed by the API', `${config.apiServiceUrl}/tokens/count`, count => count >= 5); + await waitForApi('Tokens listed on the issuer account', `${config.apiServiceUrl}/accounts/${config.aliceAddress}/tokens/count`, count => count >= 5); // 2 NFT + 2 SFT collections, five items each; the meta-esdt ones come on top of that - await waitForApi('Collections listed by the API', `${config.apiServiceUrl}/collections/count`, 4); - await waitForApi('NFTs listed by the API', `${config.apiServiceUrl}/nfts/count`, 20); + await waitForApi('Collections listed by the API', `${config.apiServiceUrl}/collections/count`, count => count >= 4); + await waitForApi('NFTs listed by the API', `${config.apiServiceUrl}/nfts/count`, count => count >= 20); + + // node and validator statistics are filled in by warmers on a one minute cron, and shards are + // derived from them. until those have run at least once, /shards is empty and nodes come back + // with a null rating and no status + await waitForApi('Shards reported by the API', `${config.apiServiceUrl}/shards`, shards => shards.length >= 4); + await waitForApi('Node ratings filled in by the API', `${config.apiServiceUrl}/nodes?size=1`, nodes => nodes.length > 0 && nodes[0].status !== undefined && nodes[0].tempRating !== null); console.log('Test data preparation completed successfully!'); } catch (error) {