feat: add gzip toggle and cache mode (normal / no-cache / bust)

- Cache-mode selector: Normal, No-cache headers, Cache-bust URL+headers
- Cache-bust appends random ?_cb= per iteration to bypass CDN/proxy
- gzip toggle: on = k6 default (Accept-Encoding: gzip/br), off = identity
- Both options stored in DB with non-destructive migration
- History items show cache-mode and gzip pills
- Schema migration handles existing DBs gracefully

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 20:03:36 +02:00
parent 0cd0f96bab
commit 6ef7564e87
4 changed files with 119 additions and 11 deletions

View File

@@ -25,6 +25,8 @@ db.exec(`
http_method TEXT NOT NULL DEFAULT 'GET',
request_body TEXT,
headers TEXT,
cache_mode TEXT NOT NULL DEFAULT 'normal',
gzip INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
finished_at INTEGER,
@@ -33,21 +35,43 @@ db.exec(`
)
`);
// Non-destructive migration for existing DBs
try { db.exec(`ALTER TABLE tests ADD COLUMN cache_mode TEXT NOT NULL DEFAULT 'normal'`); } catch {}
try { db.exec(`ALTER TABLE tests ADD COLUMN gzip INTEGER NOT NULL DEFAULT 1`); } catch {}
// Stream active test outputs (testId -> {process, clients})
const activeTests = {};
// Generate a k6 script from test params
function generateScript(params) {
const { url, vus, duration, rpsLimit, httpMethod, requestBody, headers } = params;
const { url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip } = params;
const parsedHeaders = (() => {
// Build the merged headers object
const userHeaders = (() => {
try { return JSON.parse(headers || '{}'); } catch { return {}; }
})();
const headersJs = Object.entries(parsedHeaders)
// Cache control headers
if (cacheMode === 'no-cache' || cacheMode === 'bust') {
userHeaders['Cache-Control'] = 'no-cache, no-store';
userHeaders['Pragma'] = 'no-cache';
}
// Encoding header
if (gzip === false || gzip === 0) {
userHeaders['Accept-Encoding'] = 'identity';
}
// When gzip=true, omit the header entirely so k6 sends its default (gzip, deflate, br)
const headersJs = Object.entries(userHeaders)
.map(([k, v]) => ` '${k.replace(/'/g, "\\'")}': '${v.replace(/'/g, "\\'")}',`)
.join('\n');
// For cache-bust mode, append a random query param per iteration
const targetUrl = cacheMode === 'bust'
? `\`${url}${url.includes('?') ? '&' : '?'}_cb=\${Date.now()}-\${Math.random().toString(36).slice(2)}\``
: `'${url}'`;
const bodyLine = (httpMethod !== 'GET' && httpMethod !== 'HEAD' && requestBody)
? `const body = \`${requestBody.replace(/`/g, '\\`')}\`;`
: 'const body = null;';
@@ -84,7 +108,7 @@ ${headersJs}
timeout: '30s',
};
const res = http.${httpMethod.toLowerCase()}('${url}'${httpMethod !== 'GET' && httpMethod !== 'HEAD' ? ', body' : ''}, params);
const res = http.${httpMethod.toLowerCase()}(${targetUrl}${httpMethod !== 'GET' && httpMethod !== 'HEAD' ? ', body' : ''}, params);
const ok = check(res, {
'status is 2xx': (r) => r.status >= 200 && r.status < 300,
@@ -110,6 +134,8 @@ app.post('/api/tests', (req, res) => {
httpMethod = 'GET',
requestBody = '',
headers = '{}',
cacheMode = 'normal',
gzip = true,
} = req.body;
if (!url || !url.startsWith('http')) {
@@ -120,11 +146,11 @@ app.post('/api/tests', (req, res) => {
const createdAt = Date.now();
db.prepare(`
INSERT INTO tests (id, url, vus, duration, rps_limit, http_method, request_body, headers, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)
`).run(id, url, vus, duration, rpsLimit || null, httpMethod, requestBody, headers, createdAt);
INSERT INTO tests (id, url, vus, duration, rps_limit, http_method, request_body, headers, cache_mode, gzip, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)
`).run(id, url, vus, duration, rpsLimit || null, httpMethod, requestBody, headers, cacheMode, gzip ? 1 : 0, createdAt);
const script = generateScript({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers });
const script = generateScript({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip });
const tmpScript = path.join(os.tmpdir(), `k6-script-${id}.js`);
fs.writeFileSync(tmpScript, script);
@@ -208,7 +234,7 @@ app.get('/api/tests/:id/stream', (req, res) => {
// GET /api/tests — list all tests
app.get('/api/tests', (req, res) => {
const rows = db.prepare('SELECT id, url, vus, duration, rps_limit, http_method, status, created_at, finished_at, summary FROM tests ORDER BY created_at DESC LIMIT 50').all();
const rows = db.prepare('SELECT id, url, vus, duration, rps_limit, http_method, cache_mode, gzip, status, created_at, finished_at, summary FROM tests ORDER BY created_at DESC LIMIT 50').all();
res.json(rows.map(r => ({ ...r, summary: r.summary ? JSON.parse(r.summary) : null })));
});