From 86e4625740b3b1aede1152c6048735b485eb710f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 31 Jul 2025 11:08:33 +0200 Subject: [PATCH 1/5] Fix flaky snapshot-testing Signed-off-by: Matteo Collina --- test/snapshot-testing.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/snapshot-testing.js b/test/snapshot-testing.js index 39c50f01fde..b7f0ab54a1a 100644 --- a/test/snapshot-testing.js +++ b/test/snapshot-testing.js @@ -347,9 +347,18 @@ describe('SnapshotAgent - Request Handling', () => { setGlobalDispatcher(recordingAgent) // Make multiple requests to record sequential responses - await request(`${origin}/api/test`) - await request(`${origin}/api/test`) - await request(`${origin}/api/test`) + { + const res = await request(`${origin}/api/test`) + await res.body.text() + } + { + const res = await request(`${origin}/api/test`) + await res.body.text() + } + { + const res = await request(`${origin}/api/test`) + await res.body.text() + } // Ensure all recordings are saved and verify the recording state await recordingAgent.saveSnapshots() @@ -368,6 +377,7 @@ describe('SnapshotAgent - Request Handling', () => { mode: 'playback', snapshotPath }) + setGlobalDispatcher(playbackAgent) setupCleanup(t, { agent: playbackAgent }) From dff98123e1bf33780cf8c100a75d390cf4841c6a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 31 Jul 2025 11:16:21 +0200 Subject: [PATCH 2/5] moar logging Signed-off-by: Matteo Collina --- lib/mock/snapshot-recorder.js | 2 ++ test/snapshot-testing.js | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mock/snapshot-recorder.js b/lib/mock/snapshot-recorder.js index 7482b5c1914..99371867556 100644 --- a/lib/mock/snapshot-recorder.js +++ b/lib/mock/snapshot-recorder.js @@ -288,6 +288,8 @@ class SnapshotRecorder { const hash = createRequestHash(request) const snapshot = this.snapshots.get(hash) + console.log('>>> findSnapshot', requestOpts, snapshot) + if (!snapshot) return undefined // Handle sequential responses diff --git a/test/snapshot-testing.js b/test/snapshot-testing.js index b7f0ab54a1a..b44b2bb5cd6 100644 --- a/test/snapshot-testing.js +++ b/test/snapshot-testing.js @@ -377,9 +377,9 @@ describe('SnapshotAgent - Request Handling', () => { mode: 'playback', snapshotPath }) - setGlobalDispatcher(playbackAgent) setupCleanup(t, { agent: playbackAgent }) + setGlobalDispatcher(playbackAgent) // Ensure snapshots are loaded and call counts are reset before setting dispatcher await playbackAgent.loadSnapshots() @@ -395,8 +395,6 @@ describe('SnapshotAgent - Request Handling', () => { assert.strictEqual(snapshots.length, 1, 'Should have exactly one snapshot') assert.strictEqual(snapshots[0].responses.length, 3, 'Should have three sequential responses') - setGlobalDispatcher(playbackAgent) - // Test sequential responses const response1 = await request(`${origin}/api/test`) const body1 = await response1.body.text() From 50cd7f39d211394a37c38c211123f6507b26705f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 31 Jul 2025 11:25:19 +0200 Subject: [PATCH 3/5] fixup Signed-off-by: Matteo Collina --- lib/mock/snapshot-recorder.js | 73 ++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/lib/mock/snapshot-recorder.js b/lib/mock/snapshot-recorder.js index 99371867556..66f5dffb5f7 100644 --- a/lib/mock/snapshot-recorder.js +++ b/lib/mock/snapshot-recorder.js @@ -4,6 +4,13 @@ const { writeFile, readFile, mkdir } = require('node:fs/promises') const { dirname, resolve } = require('node:path') const { InvalidArgumentError, UndiciError } = require('../core/errors') +let crypto +try { + crypto = require('node:crypto') +} catch { + // Fallback if crypto is not available +} + /** * Formats a request for consistent snapshot storage * Caches normalized headers to avoid repeated processing @@ -137,15 +144,45 @@ function normalizeHeaders (headers) { /** * Creates a hash key for request matching + * Properly orders headers to avoid conflicts and uses crypto hashing when available */ function createRequestHash (request) { const parts = [ request.method, - request.url, - JSON.stringify(request.headers, Object.keys(request.headers).sort()), - request.body || '' + request.url ] - return Buffer.from(parts.join('|')).toString('base64url') + + // Process headers in a deterministic way to avoid conflicts + if (request.headers && typeof request.headers === 'object') { + const headerKeys = Object.keys(request.headers).sort() + for (const key of headerKeys) { + const lowerKey = key.toLowerCase() + const values = Array.isArray(request.headers[key]) + ? request.headers[key] + : [request.headers[key]] + + // Add header name + parts.push(lowerKey) + + // Add all values for this header, sorted for consistency + for (const value of values.sort()) { + parts.push(String(value)) + } + } + } + + // Add body + parts.push(request.body || '') + + const content = parts.join('|') + + // Use crypto hash if available for better collision resistance + if (crypto && crypto.createHash) { + return crypto.createHash('sha256').update(content, 'utf8').digest('base64url') + } + + // Fallback to base64 encoding + return Buffer.from(content).toString('base64url') } /** @@ -293,30 +330,14 @@ class SnapshotRecorder { if (!snapshot) return undefined // Handle sequential responses - if (snapshot.responses && Array.isArray(snapshot.responses)) { - const currentCallCount = snapshot.callCount || 0 - const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) - snapshot.callCount = currentCallCount + 1 - - return { - ...snapshot, - response: snapshot.responses[responseIndex] - } - } + const currentCallCount = snapshot.callCount || 0 + const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) + snapshot.callCount = currentCallCount + 1 - // Legacy format compatibility - convert single response to array format - if (snapshot.response && !snapshot.responses) { - snapshot.responses = [snapshot.response] - snapshot.callCount = 1 - delete snapshot.response - - return { - ...snapshot, - response: snapshot.responses[0] - } + return { + ...snapshot, + response: snapshot.responses[responseIndex] } - - return snapshot } /** From 2e3eb5a723da388281d5f445e55219350c0dd18d Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 31 Jul 2025 11:49:19 +0200 Subject: [PATCH 4/5] fixup Signed-off-by: Matteo Collina --- lib/mock/snapshot-recorder.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/mock/snapshot-recorder.js b/lib/mock/snapshot-recorder.js index 66f5dffb5f7..17e382e7691 100644 --- a/lib/mock/snapshot-recorder.js +++ b/lib/mock/snapshot-recorder.js @@ -325,12 +325,10 @@ class SnapshotRecorder { const hash = createRequestHash(request) const snapshot = this.snapshots.get(hash) - console.log('>>> findSnapshot', requestOpts, snapshot) - if (!snapshot) return undefined // Handle sequential responses - const currentCallCount = snapshot.callCount || 0 + const currentCallCount = snapshot.callCount || 1 const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) snapshot.callCount = currentCallCount + 1 From 68f5a3b535f2ed6182b0968f165dba8f925e41f5 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 31 Jul 2025 12:59:06 +0200 Subject: [PATCH 5/5] fixup Signed-off-by: Matteo Collina --- lib/mock/snapshot-recorder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mock/snapshot-recorder.js b/lib/mock/snapshot-recorder.js index 17e382e7691..298fcac1d66 100644 --- a/lib/mock/snapshot-recorder.js +++ b/lib/mock/snapshot-recorder.js @@ -328,7 +328,7 @@ class SnapshotRecorder { if (!snapshot) return undefined // Handle sequential responses - const currentCallCount = snapshot.callCount || 1 + const currentCallCount = snapshot.callCount || 0 const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) snapshot.callCount = currentCallCount + 1