From d4d6aadea298cdfe0ced348923d2f39d37a90371 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 23 Sep 2026 01:12:39 -0400 Subject: [PATCH] test(mobile): page the range fetch fixture on a small advertised grid The fake host names a 4 KiB range grid and a 1 KiB chunk grid on its manifest reply, which the phone pages as it would the real ones, so the fixtures shrink to a few KiB with the same shapes and each asset is hashed once. The file runs in about 360 ms instead of 3.5 s, which a loaded CI runner pushed past the 5 s test timeout. The desktop range suite still pins that the real host names MOBILE_WEB_BUNDLE_RANGE_BYTES. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../mobile-web-bundle-fetch-range.test.ts | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts b/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts index 0d58d2fad52..37cdf736df9 100644 --- a/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts +++ b/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts @@ -1,16 +1,18 @@ import { sha256 } from '@noble/hashes/sha256' import { gzipSync } from 'fflate' import { describe, expect, it, vi } from 'vitest' -import { - MOBILE_WEB_BUNDLE_CHUNK_BYTES, - MOBILE_WEB_BUNDLE_RANGE_BYTES -} from '../../../src/shared/mobile-web-bundle/bundle-rpc-contract' import { computeMobileWebBundleId } from '../../../src/shared/mobile-web-bundle/manifest-contract' import { fetchMobileWebBundle } from './mobile-web-bundle-fetch' import { MobileWebBundleFetchError } from './mobile-web-bundle-fetch-refusal' import type { RpcClient } from './rpc-client' import type { RpcResponse } from './types' +/** The grids this fake host names on its manifest reply. The phone pages whatever grid the host + * names, so a few KiB exercises the same shapes as the real 384 KiB range and 48 KiB chunk; the + * desktop suite pins that the real host names `MOBILE_WEB_BUNDLE_RANGE_BYTES`. */ +const RANGE_GRID = 4096 +const CHUNK_GRID = 1024 + type Inflation = { readonly outLength: number; readonly resultLength: number } type LoggedInflation = Inflation & { readonly body: Uint8Array } @@ -76,10 +78,13 @@ function rangeHost( files: Record, options: { ranges?: boolean; tamper?: (call: HostCall, body: string) => string } = {} ) { + const digests = new Map( + Object.entries(files).map(([path, content]) => [path, toHex(sha256(content))]) + ) const assets = Object.entries(files) .map(([path, content]) => ({ path, - sha256: toHex(sha256(content)), + sha256: digests.get(path)!, byteLength: content.byteLength, contentType: 'text/javascript' })) @@ -104,27 +109,24 @@ function rangeHost( const answer = (call: HostCall): unknown => { if (call.method === 'mobileWeb.bundle.manifest') { return options.ranges === false - ? { manifest, chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES } + ? { manifest, chunkBytes: CHUNK_GRID } : { manifest, - chunkBytes: MOBILE_WEB_BUNDLE_CHUNK_BYTES, - rangeBytes: MOBILE_WEB_BUNDLE_RANGE_BYTES + chunkBytes: CHUNK_GRID, + rangeBytes: RANGE_GRID } } const path = String(call.params.path) const offset = Number(call.params.offset) const content = files[path]! - const grid = - call.method === 'mobileWeb.bundle.range' - ? MOBILE_WEB_BUNDLE_RANGE_BYTES - : MOBILE_WEB_BUNDLE_CHUNK_BYTES + const grid = call.method === 'mobileWeb.bundle.range' ? RANGE_GRID : CHUNK_GRID const slice = content.subarray(offset, offset + grid) const header = { buildId, path, offset, assetByteLength: content.byteLength, - sha256: toHex(sha256(content)), + sha256: digests.get(path)!, eof: offset + slice.byteLength >= content.byteLength } if (call.method === 'mobileWeb.bundle.chunk') { @@ -193,9 +195,9 @@ const NOISE = 'assets/noise.bin' const EMPTY = 'assets/empty.txt' /** One asset over a range, one an exact multiple of it, one incompressible, one empty. */ const FILES: Record = { - 'assets/app.js': scriptBytes(MOBILE_WEB_BUNDLE_RANGE_BYTES * 2 + 5000, 1), - [EXACT]: scriptBytes(MOBILE_WEB_BUNDLE_RANGE_BYTES * 2, 4), - [NOISE]: noiseBytes(70_000), + 'assets/app.js': scriptBytes(RANGE_GRID * 2 + 500, 1), + [EXACT]: scriptBytes(RANGE_GRID * 2, 4), + [NOISE]: noiseBytes(3000), [EMPTY]: new Uint8Array(0), 'index.html': scriptBytes(600, 3) } @@ -230,10 +232,10 @@ describe('fetchMobileWebBundle from a host whose manifest names a range grid', ( } expect(readsOf(host.calls, 'mobileWeb.bundle.chunk')).toHaveLength(0) const ranges = readsOf(host.calls, 'mobileWeb.bundle.range') - expect(ranges).toHaveLength(slotsOn(MOBILE_WEB_BUNDLE_RANGE_BYTES)) + expect(ranges).toHaveLength(slotsOn(RANGE_GRID)) for (const range of ranges) { expect(Object.keys(range.params).sort()).toEqual(['buildId', 'offset', 'path']) - expect(Number(range.params.offset) % MOBILE_WEB_BUNDLE_RANGE_BYTES).toBe(0) + expect(Number(range.params.offset) % RANGE_GRID).toBe(0) } expect(host.peakInFlight()).toBe(4) }) @@ -248,7 +250,7 @@ describe('fetchMobileWebBundle from a host whose manifest names a range grid', ( const exact = readsOf(host.calls, 'mobileWeb.bundle.range').filter( (call) => call.params.path === EXACT ) - expect(exact.map((call) => call.params.offset)).toEqual([0, MOBILE_WEB_BUNDLE_RANGE_BYTES]) + expect(exact.map((call) => call.params.offset)).toEqual([0, RANGE_GRID]) }) it('pages a host whose manifest names no range grid in chunks, as before', async () => { @@ -257,9 +259,7 @@ describe('fetchMobileWebBundle from a host whose manifest names a range grid', ( expect(fetched.assets.get('assets/app.js')).toEqual(FILES['assets/app.js']) expect(readsOf(host.calls, 'mobileWeb.bundle.range')).toHaveLength(0) - expect(readsOf(host.calls, 'mobileWeb.bundle.chunk')).toHaveLength( - slotsOn(MOBILE_WEB_BUNDLE_CHUNK_BYTES) - ) + expect(readsOf(host.calls, 'mobileWeb.bundle.chunk')).toHaveLength(slotsOn(CHUNK_GRID)) }) it('carries the same bundle in fewer reads and fewer bytes than chunks', async () => { @@ -285,14 +285,14 @@ describe('fetchMobileWebBundle from a host whose manifest names a range grid', ( expect(await refusalOf(fetchMobileWebBundle({ client: host.client }))).toBe('range-undecodable') }) - // A 4 MiB inflation answering the first 384 KiB window: fflate fills the bounded buffer and stops + // A 4 MiB inflation answering the first window: fflate fills the bounded buffer and stops // there. Twenty reads are planned, so a fetch that kept going after the refusal is visible. it('refuses a gzip bomb at one byte over the window and sends little after it', async () => { const bomb = encodeBase64(gzipSync(new Uint8Array(4 * 1024 * 1024), { level: 9 })) const many = Object.fromEntries( Array.from({ length: 10 }, (_, index) => [ `assets/part-${String(index)}.js`, - scriptBytes(MOBILE_WEB_BUNDLE_RANGE_BYTES * 2, index) + scriptBytes(RANGE_GRID * 2, index) ]) ) const host = rangeHost(many, { @@ -302,7 +302,7 @@ describe('fetchMobileWebBundle from a host whose manifest names a range grid', ( expect(await refusalOf(fetchMobileWebBundle({ client: host.client }))).toBe('chunk-oversize') await host.drain() - const window = MOBILE_WEB_BUNDLE_RANGE_BYTES + 1 + const window = RANGE_GRID + 1 // Exactly one body filled the spare byte: the bomb, stopped there. expect(host.inflations().filter((inflation) => inflation.resultLength === window)).toEqual([ { outLength: window, resultLength: window }