#!/usr/bin/env node import assert from 'node:assert/strict'; const [baseUrl, candidateUrl = process.env.RUST_URL || 'http://127.0.0.1:3000'] = process.argv.slice(2); if (!baseUrl) { console.error('usage: node alt/compare.mjs [rust-url]'); process.exit(2); } const mediaId = process.env.TEST_MEDIA_ID || 'dQw4w9WgXcQ'; const testQuery = encodeURIComponent(process.env.TEST_QUERY || 'ytplayer-bench-seed'); const results = []; const shape = value => Array.isArray(value) ? value.map(shape) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, shape(value[key])])) : typeof value; const headerSet = response => ({ contentType: (response.headers.get('content-type') || '').split(';')[0], cache: response.headers.get('cache-control'), range: response.headers.get('content-range'), acceptRanges: response.headers.get('accept-ranges'), encoding: response.headers.get('content-encoding'), assetHash: response.headers.get('x-asset-hash'), etag: response.headers.get('etag'), }); async function inspect(url, path, { method = 'GET', headers = {} } = {}) { const response = await fetch(new URL(path, url), { method, headers }); const type = response.headers.get('content-type') || ''; let body = null; if (type.includes('json')) body = await response.json(); else if (type.startsWith('text/')) body = await response.text(); else await response.arrayBuffer(); return { status: response.status, headers: headerSet(response), shape: body ? shape(body) : null, body }; } function check(name, a, b, fields) { const headersMatch = fields.every(field => a.headers[field] === b.headers[field]); const compatible = a.status === b.status && JSON.stringify(a.shape) === JSON.stringify(b.shape) && headersMatch; results.push({ name, compatible, base: { status: a.status, headers: a.headers, shape: a.shape }, candidate: { status: b.status, headers: b.headers, shape: b.shape } }); console.log(`${compatible ? 'PASS' : 'DIFF'} ${name}: ${a.status} / ${b.status}${headersMatch ? '' : ` headers ${JSON.stringify(a.headers)} / ${JSON.stringify(b.headers)}`}`); } const [baseManifest, candidateManifest] = await Promise.all([inspect(baseUrl, '/api/manifest'), inspect(candidateUrl, '/api/manifest')]); check('manifest', baseManifest, candidateManifest, ['cache', 'etag', 'contentType']); for (const [name, url, manifest] of [['base', baseUrl, baseManifest.body], ['candidate', candidateUrl, candidateManifest.body]]) { const [index, sw] = await Promise.all([inspect(url, '/'), inspect(url, '/sw.js')]); const htmlText = index.body; const swText = sw.body; const tagPattern = new RegExp(`]*name="ytp-build"[^>]*content="${manifest.buildTag}"`); const stamped = index.status === 200 && tagPattern.test(htmlText) && (htmlText.includes(`styles.css?v=${manifest.files['/styles.css']?.h}`) || htmlText.includes(`styles.css?v=${manifest.buildTag}`)); const workerStamped = sw.status === 200 && swText.includes(`"${manifest.buildTag}"`) && !swText.includes("typeof __BUILD_TAG__ !== 'undefined'"); const compatible = stamped && workerStamped; results.push({ name: `${name}-shell-stamping`, compatible, stamped, workerStamped }); console.log(`${compatible ? 'PASS' : 'DIFF'} ${name}-shell-stamping`); } const cssHash = baseManifest.body?.files?.['/styles.css']?.h || ''; const checks = [ ['version', '/api/version'], ['static', `/styles.css?v=${cssHash}`], ['search-local', `/api/search/local?q=${testQuery}`], ['search', `/api/search?q=${testQuery}`], ['streams', `/api/streams?v=${mediaId}`], ['download-prepare', `/api/download/${mediaId}/prepare`], ['media-status', `/api/media/${mediaId}/status`], ]; for (const [name, path] of checks) { const [base, candidate] = await Promise.all([inspect(baseUrl, path), inspect(candidateUrl, path)]); const fields = name === 'static' ? ['contentType', 'cache', 'encoding', 'assetHash'] : ['contentType']; check(name, base, candidate, fields); } const [baseRange, rustRange] = await Promise.all([ inspect(baseUrl, `/api/media/${mediaId}?g=1`, { headers: { Range: 'bytes=0-63' } }), inspect(candidateUrl, `/api/media/${mediaId}?g=1`, { headers: { Range: 'bytes=0-63' } }), ]); check('media-range', baseRange, rustRange, ['contentType', 'cache', 'range', 'acceptRanges', 'etag']); if (results.some(result => !result.compatible)) process.exitCode = 1;