From 40530893786639f8e1afbb299605ee0938f31e99 Mon Sep 17 00:00:00 2001 From: jagdeep sidhu Date: Tue, 27 Oct 2026 17:53:28 -0700 Subject: [PATCH 1/2] Stabilize governance timeout receipt test --- lib/governanceRpc.test.js | 70 +++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/lib/governanceRpc.test.js b/lib/governanceRpc.test.js index 23311bf..71f9c5d 100644 --- a/lib/governanceRpc.test.js +++ b/lib/governanceRpc.test.js @@ -87,29 +87,85 @@ test('malformed current-vote data is not certified as an empty tally', async () code: 'governance_rpc_unavailable', }); }); -test('actual HTTP timeout after receiving vote is unknown and never automatically retried', async () => { +test('deadline after the real server receives a vote is unknown and never automatically retried', async () => { let count = 0; + let received; const server = http.createServer((req) => { count++; req.resume(); + received(); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const actualSetTimeout = global.setTimeout; + const transport = axios.create(); + // Keep the real HTTP connection, while controlling which deadline fires. + // Axios's own timeout is disabled here so the server receipt is observed + // before the RPC's 30 ms deadline callback is invoked deterministically. + const instance = { + post: jest.fn((url, body, options) => { + expect(options.timeout).toBe(30); + return transport.post(url, body, { ...options, timeout: 0 }); + }), + }; const rpc = createGovernanceRpc( - { - url: `http://127.0.0.1:${server.address().port}`, - instance: axios.create(), - }, + { url: `http://127.0.0.1:${server.address().port}`, instance }, { timeoutMs: 30 } ); try { - await expect(rpc.voteRaw(...args)).rejects.toMatchObject({ + async function afterReceipt(call) { + const requestReceived = new Promise((resolve) => { received = resolve; }); + let expire; + const spy = jest.spyOn(global, 'setTimeout').mockImplementationOnce((fn, ms) => { + expect(ms).toBe(30); + expire = fn; + // Return a real, cleared timer handle for the RPC's normal cleanup. + return actualSetTimeout(() => {}, ms); + }); + let result; + try { + result = call(); + } finally { + spy.mockRestore(); + } + await requestReceived; + expire(); + return result; + } + await expect(afterReceipt(() => rpc.voteRaw(...args))).rejects.toMatchObject({ + code: 'submission_unknown', uncertain: true, }); expect(count).toBe(1); - await expect(rpc.getCurrentVotes(args[2])).rejects.toMatchObject({ + expect(instance.post).toHaveBeenCalledTimes(1); + await expect(afterReceipt(() => rpc.getCurrentVotes(args[2]))).rejects.toMatchObject({ code: 'governance_rpc_unavailable', }); expect(count).toBe(2); + expect(instance.post).toHaveBeenCalledTimes(2); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => server.close(resolve)); + } +}); +test('real HTTP timeout is uncertain even when delivery cannot be established', async () => { + let count = 0; + const server = http.createServer((req) => { + count++; + req.resume(); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const instance = axios.create(); + const post = jest.spyOn(instance, 'post'); + const rpc = createGovernanceRpc( + { url: `http://127.0.0.1:${server.address().port}`, instance }, + { timeoutMs: 30 } + ); + try { + await expect(rpc.voteRaw(...args)).rejects.toMatchObject({ + code: 'submission_unknown', uncertain: true, + }); + expect(post).toHaveBeenCalledTimes(1); + expect(count).toBeLessThanOrEqual(1); } finally { server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)); From e417ed67dfa84f72971554332391563a0d2f5ea6 Mon Sep 17 00:00:00 2001 From: jagdeep sidhu Date: Tue, 27 Oct 2026 17:54:16 -0700 Subject: [PATCH 2/2] Verify full vote body before deadline --- lib/governanceRpc.test.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/governanceRpc.test.js b/lib/governanceRpc.test.js index 71f9c5d..4660bec 100644 --- a/lib/governanceRpc.test.js +++ b/lib/governanceRpc.test.js @@ -92,8 +92,9 @@ test('deadline after the real server receives a vote is unknown and never automa let received; const server = http.createServer((req) => { count++; - req.resume(); - received(); + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', () => received(JSON.parse(Buffer.concat(chunks).toString()))); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const actualSetTimeout = global.setTimeout; @@ -112,7 +113,7 @@ test('deadline after the real server receives a vote is unknown and never automa { timeoutMs: 30 } ); try { - async function afterReceipt(call) { + async function afterReceipt(call, expectedMethod, expectedParams) { const requestReceived = new Promise((resolve) => { received = resolve; }); let expire; const spy = jest.spyOn(global, 'setTimeout').mockImplementationOnce((fn, ms) => { @@ -127,17 +128,18 @@ test('deadline after the real server receives a vote is unknown and never automa } finally { spy.mockRestore(); } - await requestReceived; + const body = await requestReceived; + expect(body).toMatchObject({ method: expectedMethod, params: expectedParams }); expire(); return result; } - await expect(afterReceipt(() => rpc.voteRaw(...args))).rejects.toMatchObject({ + await expect(afterReceipt(() => rpc.voteRaw(...args), 'voteraw', args)).rejects.toMatchObject({ code: 'submission_unknown', uncertain: true, }); expect(count).toBe(1); expect(instance.post).toHaveBeenCalledTimes(1); - await expect(afterReceipt(() => rpc.getCurrentVotes(args[2]))).rejects.toMatchObject({ + await expect(afterReceipt(() => rpc.getCurrentVotes(args[2]), 'gobject_getcurrentvotes', [args[2]])).rejects.toMatchObject({ code: 'governance_rpc_unavailable', }); expect(count).toBe(2);