From 240517c26834fd0c0ef1da0cdb2b516aab0cdca2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 07:49:11 +0000 Subject: [PATCH 1/4] Add cache integration and edge-case tests --- proxy.js | 2 +- test/proxy.test.js | 174 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/proxy.js b/proxy.js index 19972ec..6d4ac6e 100644 --- a/proxy.js +++ b/proxy.js @@ -196,7 +196,7 @@ async function getContent(httpModule, origReq, origRes) { let isHit = ''; const requestDetails = { - host: mappedUrl.host || origUrl.host, + host: mappedUrl.hostname || origUrl.hostname || mappedUrl.host || origUrl.host, port: mappedUrl.port || origUrl.port, path: mappedUrl.path || origUrl.path, username: origUrl.username, diff --git a/test/proxy.test.js b/test/proxy.test.js index f0be4ee..2e7a930 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -4,6 +4,9 @@ process.env.PROXY_KUTTI_CONFIG = '/nonexistent/proxy-kutti-test-config'; const os = require('os'); const fs = require('fs'); const path = require('path'); +const http = require('http'); +const net = require('net'); +const { spawn } = require('child_process'); const tmpCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxy-kutti-test-')); process.env.PROXY_KUTTI_cache_dir = tmpCacheDir; @@ -26,6 +29,17 @@ const { after(() => fs.rmSync(tmpCacheDir, { recursive: true, force: true })); const hoursAgo = hours => new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); + +const getFreePort = () => + new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const { port } = server.address(); + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); describe('parseUrlMappings', () => { it('parses a sed-style pattern into search regex and replacement', () => { @@ -175,6 +189,58 @@ describe('addHoursToDate', () => { }); }); +describe('cacheHit', () => { + it('returns null for stale entries that match cache_control', async () => { + const cachedFile = path.join(tmpCacheDir, 'stale-hit.data'); + const cachedFileMeta = `${cachedFile}.meta`; + fs.writeFileSync(cachedFile, 'stale-content'); + fs.writeFileSync( + cachedFileMeta, + JSON.stringify({ + headers: { 'content-type': 'text/plain' }, + statusCode: 200, + 'proxy-kutti-orig-request': { 'cache-date': hoursAgo(25) }, + }) + ); + + const result = await cacheHit({ + host: 'registry.npmjs.org', + path: '/lodash', + method: 'GET', + headers: {}, + state: { cachedFile, cachedFileMeta }, + }); + assert.strictEqual(result, null); + }); + + it('returns an empty body stream for HEAD cache hits', async () => { + const cachedFile = path.join(tmpCacheDir, 'head-hit.data'); + const cachedFileMeta = `${cachedFile}.meta`; + fs.writeFileSync(cachedFile, 'should-not-be-read-for-head'); + fs.writeFileSync( + cachedFileMeta, + JSON.stringify({ + headers: { 'content-type': 'application/octet-stream' }, + statusCode: 204, + 'proxy-kutti-orig-request': { 'cache-date': hoursAgo(1) }, + }) + ); + + const proxyRes = await cacheHit({ + host: 'example.com', + path: '/head', + method: 'HEAD', + headers: {}, + state: { cachedFile, cachedFileMeta }, + }); + assert.notStrictEqual(proxyRes, null); + assert.strictEqual(proxyRes.statusCode, 204); + let body = ''; + for await (const chunk of proxyRes) body += chunk; + assert.strictEqual(body, ''); + }); +}); + describe('guessContentType', () => { it('maps known extensions case-insensitively', () => { assert.strictEqual(guessContentType('foo-1.2.3.zip'), 'application/zip'); @@ -242,4 +308,112 @@ describe('importIntoCache', () => { const metaData = JSON.parse(fs.readFileSync(`${cachedFile}.meta`, 'utf8')); assert.strictEqual(metaData.headers['content-type'], 'application/x-mystery'); }); + + it('supports --content-type= syntax', async () => { + const srcFile = path.join(tmpCacheDir, 'src-asset-equals.bin'); + fs.writeFileSync(srcFile, 'binary stuff'); + + const importUrl = 'https://example.com/downloads/asset-equals.bin'; + await importIntoCache([importUrl, srcFile, '--content-type=application/x-equals']); + + const { cachedFile } = computeCacheDetails('https', 'GET', importUrl); + const metaData = JSON.parse(fs.readFileSync(`${cachedFile}.meta`, 'utf8')); + assert.strictEqual(metaData.headers['content-type'], 'application/x-equals'); + }); +}); + +describe('proxy integration cache behavior', () => { + it('serves identical second request from cache without hitting origin again', { timeout: 15000 }, async () => { + const integrationCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxy-kutti-e2e-')); + const originPort = await getFreePort(); + const proxyPort = await getFreePort(); + let originRequestCount = 0; + const originServer = http.createServer((req, res) => { + originRequestCount++; + const body = JSON.stringify({ requestCount: originRequestCount, path: req.url }); + res.writeHead(200, { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(body), + }); + res.end(body); + }); + await new Promise(resolve => originServer.listen(originPort, '127.0.0.1', resolve)); + + const proxyProcess = spawn('node', ['/tmp/workspace/forkeith/node-proxy-kutti/proxy.js'], { + cwd: '/tmp/workspace/forkeith/node-proxy-kutti', + env: { + ...process.env, + PROXY_KUTTI_CONFIG: '/nonexistent/proxy-kutti-e2e-config', + PROXY_KUTTI_host: '127.0.0.1', + PROXY_KUTTI_port: String(proxyPort), + PROXY_KUTTI_cache_dir: integrationCacheDir, + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + const waitForProxyReady = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timed out waiting for proxy to start')), 5000); + proxyProcess.stdout.on('data', data => { + if (data.toString().includes('Proxy-kutti is running')) { + clearTimeout(timeout); + resolve(); + } + }); + proxyProcess.on('exit', code => { + clearTimeout(timeout); + reject(new Error(`Proxy exited early with code ${code}`)); + }); + }); + + const requestViaProxy = pathName => + new Promise((resolve, reject) => { + const req = http.request( + { + hostname: '127.0.0.1', + port: proxyPort, + method: 'GET', + path: `http://127.0.0.1:${originPort}${pathName}`, + }, + res => { + let body = ''; + res.on('data', chunk => (body += chunk)); + res.on('end', () => resolve({ statusCode: res.statusCode, body })); + } + ); + req.setTimeout(3000, () => req.destroy(new Error('Timed out waiting for proxy response'))); + req.on('error', reject); + req.end(); + }); + + try { + await waitForProxyReady; + const first = await requestViaProxy('/api/e2e-cache-hit'); + const countAfterFirst = originRequestCount; + const second = await requestViaProxy('/api/e2e-cache-hit'); + + assert.strictEqual(first.statusCode, 200); + assert.strictEqual(second.statusCode, 200); + assert.strictEqual(originRequestCount, countAfterFirst); + assert.strictEqual( + JSON.parse(first.body).requestCount, + JSON.parse(second.body).requestCount + ); + + const cachedFile = `${integrationCacheDir}/http/127.0.0.1:${originPort}/GET/api/e2e-cache-hit.data`; + assert.ok(fs.existsSync(cachedFile)); + assert.ok(fs.existsSync(`${cachedFile}.meta`)); + } finally { + await new Promise(resolve => originServer.close(resolve)); + const proxyExit = new Promise(resolve => proxyProcess.once('exit', resolve)); + if (proxyProcess.exitCode === null) { + proxyProcess.kill('SIGTERM'); + } + await Promise.race([proxyExit, wait(1000)]); + if (proxyProcess.exitCode === null) { + proxyProcess.kill('SIGKILL'); + await proxyExit; + } + fs.rmSync(integrationCacheDir, { recursive: true, force: true }); + } + }); }); From 35ee26bd977ef1cefbecf0279bc35a24fcf28d30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 07:52:31 +0000 Subject: [PATCH 2/4] Expand cache coverage with integration and TTL edge tests --- test/proxy.test.js | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/test/proxy.test.js b/test/proxy.test.js index 2e7a930..ce78774 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -8,6 +8,11 @@ const http = require('http'); const net = require('net'); const { spawn } = require('child_process'); const tmpCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxy-kutti-test-')); +const proxyScriptPath = path.resolve(__dirname, '../proxy.js'); +const PROXY_STARTUP_TIMEOUT_MS = 5000; +const PROXY_REQUEST_TIMEOUT_MS = 3000; +const PROXY_SHUTDOWN_TIMEOUT_MS = 1000; +const PROXY_INTEGRATION_TEST_TIMEOUT_MS = 15000; process.env.PROXY_KUTTI_cache_dir = tmpCacheDir; const { describe, it, after } = require('node:test'); @@ -323,8 +328,11 @@ describe('importIntoCache', () => { }); describe('proxy integration cache behavior', () => { - it('serves identical second request from cache without hitting origin again', { timeout: 15000 }, async () => { - const integrationCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxy-kutti-e2e-')); + it( + 'serves identical second request from cache without hitting origin again', + { timeout: PROXY_INTEGRATION_TEST_TIMEOUT_MS }, + async () => { + const e2eCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxy-kutti-e2e-')); const originPort = await getFreePort(); const proxyPort = await getFreePort(); let originRequestCount = 0; @@ -339,20 +347,20 @@ describe('proxy integration cache behavior', () => { }); await new Promise(resolve => originServer.listen(originPort, '127.0.0.1', resolve)); - const proxyProcess = spawn('node', ['/tmp/workspace/forkeith/node-proxy-kutti/proxy.js'], { - cwd: '/tmp/workspace/forkeith/node-proxy-kutti', + const proxyProcess = spawn('node', [proxyScriptPath], { + cwd: path.dirname(proxyScriptPath), env: { ...process.env, PROXY_KUTTI_CONFIG: '/nonexistent/proxy-kutti-e2e-config', PROXY_KUTTI_host: '127.0.0.1', PROXY_KUTTI_port: String(proxyPort), - PROXY_KUTTI_cache_dir: integrationCacheDir, + PROXY_KUTTI_cache_dir: e2eCacheDir, }, stdio: ['ignore', 'pipe', 'pipe'], }); const waitForProxyReady = new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timed out waiting for proxy to start')), 5000); + const timeout = setTimeout(() => reject(new Error('Timed out waiting for proxy to start')), PROXY_STARTUP_TIMEOUT_MS); proxyProcess.stdout.on('data', data => { if (data.toString().includes('Proxy-kutti is running')) { clearTimeout(timeout); @@ -362,7 +370,8 @@ describe('proxy integration cache behavior', () => { proxyProcess.on('exit', code => { clearTimeout(timeout); reject(new Error(`Proxy exited early with code ${code}`)); - }); + } + ); }); const requestViaProxy = pathName => @@ -380,7 +389,7 @@ describe('proxy integration cache behavior', () => { res.on('end', () => resolve({ statusCode: res.statusCode, body })); } ); - req.setTimeout(3000, () => req.destroy(new Error('Timed out waiting for proxy response'))); + req.setTimeout(PROXY_REQUEST_TIMEOUT_MS, () => req.destroy(new Error('Timed out waiting for proxy response'))); req.on('error', reject); req.end(); }); @@ -399,7 +408,7 @@ describe('proxy integration cache behavior', () => { JSON.parse(second.body).requestCount ); - const cachedFile = `${integrationCacheDir}/http/127.0.0.1:${originPort}/GET/api/e2e-cache-hit.data`; + const cachedFile = `${e2eCacheDir}/http/127.0.0.1:${originPort}/GET/api/e2e-cache-hit.data`; assert.ok(fs.existsSync(cachedFile)); assert.ok(fs.existsSync(`${cachedFile}.meta`)); } finally { @@ -408,12 +417,15 @@ describe('proxy integration cache behavior', () => { if (proxyProcess.exitCode === null) { proxyProcess.kill('SIGTERM'); } - await Promise.race([proxyExit, wait(1000)]); - if (proxyProcess.exitCode === null) { + const exitedAfterTerm = await Promise.race([ + proxyExit.then(() => true), + wait(PROXY_SHUTDOWN_TIMEOUT_MS).then(() => false), + ]); + if (!exitedAfterTerm && proxyProcess.exitCode === null) { proxyProcess.kill('SIGKILL'); await proxyExit; } - fs.rmSync(integrationCacheDir, { recursive: true, force: true }); + fs.rmSync(e2eCacheDir, { recursive: true, force: true }); } }); }); From 6cfd5712b0e0f2f6644bdb9e2388e61d6ddbf4ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:05:49 +0000 Subject: [PATCH 3/4] Add extra cache/import edge case tests --- test/proxy.test.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/proxy.test.js b/test/proxy.test.js index ce78774..da9d263 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -244,6 +244,36 @@ describe('cacheHit', () => { for await (const chunk of proxyRes) body += chunk; assert.strictEqual(body, ''); }); + + it('backfills missing proxy-kutti-orig-request in legacy metadata', async () => { + const cachedFile = path.join(tmpCacheDir, 'legacy-hit.data'); + const cachedFileMeta = `${cachedFile}.meta`; + fs.writeFileSync(cachedFile, 'legacy-body'); + fs.writeFileSync( + cachedFileMeta, + JSON.stringify({ + headers: { 'content-type': 'text/plain' }, + statusCode: 200, + }) + ); + + const requestDetails = { + host: 'legacy.example.com', + path: '/legacy-hit', + method: 'GET', + headers: {}, + state: { cachedFile, cachedFileMeta }, + }; + const proxyRes = await cacheHit(requestDetails); + assert.notStrictEqual(proxyRes, null); + await wait(25); + + const updatedMeta = JSON.parse(fs.readFileSync(cachedFileMeta, 'utf8')); + assert.ok(updatedMeta['proxy-kutti-orig-request']); + assert.strictEqual(updatedMeta['proxy-kutti-orig-request'].host, requestDetails.host); + assert.strictEqual(updatedMeta['proxy-kutti-orig-request'].state, null); + assert.ok(updatedMeta['proxy-kutti-orig-request']['cache-date']); + }); }); describe('guessContentType', () => { @@ -325,6 +355,30 @@ describe('importIntoCache', () => { const metaData = JSON.parse(fs.readFileSync(`${cachedFile}.meta`, 'utf8')); assert.strictEqual(metaData.headers['content-type'], 'application/x-equals'); }); + + it('supports --content-type before positional args', async () => { + const srcFile = path.join(tmpCacheDir, 'src-asset-leading-flag.tgz'); + fs.writeFileSync(srcFile, 'binary stuff'); + + const importUrl = 'https://example.com/downloads/asset-leading-flag.tgz'; + await importIntoCache(['--content-type', 'application/custom-tgz', importUrl, srcFile]); + + const { cachedFile } = computeCacheDetails('https', 'GET', importUrl); + const metaData = JSON.parse(fs.readFileSync(`${cachedFile}.meta`, 'utf8')); + assert.strictEqual(metaData.headers['content-type'], 'application/custom-tgz'); + }); + + it('falls back to inferred content type when --content-type has no value', async () => { + const srcFile = path.join(tmpCacheDir, 'src-asset-missing-override.tgz'); + fs.writeFileSync(srcFile, 'binary stuff'); + + const importUrl = 'https://example.com/downloads/asset-missing-override.tgz'; + await importIntoCache([importUrl, srcFile, '--content-type']); + + const { cachedFile } = computeCacheDetails('https', 'GET', importUrl); + const metaData = JSON.parse(fs.readFileSync(`${cachedFile}.meta`, 'utf8')); + assert.strictEqual(metaData.headers['content-type'], 'application/gzip'); + }); }); describe('proxy integration cache behavior', () => { From d15565fb0b47858ebc3c14a8697225e239318dd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:06:40 +0000 Subject: [PATCH 4/4] Stabilize legacy metadata backfill test --- test/proxy.test.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/proxy.test.js b/test/proxy.test.js index da9d263..aa23d92 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -35,6 +35,14 @@ after(() => fs.rmSync(tmpCacheDir, { recursive: true, force: true })); const hoursAgo = hours => new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); +const waitFor = async (predicate, timeoutMs = 1000, intervalMs = 10) => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (predicate()) return; + await wait(intervalMs); + } + throw new Error(`Timed out after ${timeoutMs}ms`); +}; const getFreePort = () => new Promise((resolve, reject) => { @@ -266,7 +274,15 @@ describe('cacheHit', () => { }; const proxyRes = await cacheHit(requestDetails); assert.notStrictEqual(proxyRes, null); - await wait(25); + + await waitFor(() => { + try { + const updatedMeta = JSON.parse(fs.readFileSync(cachedFileMeta, 'utf8')); + return Boolean(updatedMeta['proxy-kutti-orig-request']); + } catch { + return false; + } + }); const updatedMeta = JSON.parse(fs.readFileSync(cachedFileMeta, 'utf8')); assert.ok(updatedMeta['proxy-kutti-orig-request']);