import { describe, it, expect } from 'vitest'; import { CORPUS } from './corpusCheckout'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { searchMain, prefetchSources, resolve, } from '@beehexa/hexasync-template-compose'; import { nodeTemplateReader } from '@beehexa/hexasync-template-io-node'; import { createFsResolverIO } from '../commands/compose/composeCommand'; /** * Prefetch reads concurrently, which is the difference between a usable index and a broken one * (reported 2026-08-05: "indexing costs 30 seconds to 5 minutes"). * * The reads were sequential. That is invisible on a local disk and dominates everything on a remote one: * in a browser VS Code every `readFile` is an RPC to the server, so a 217-file project paid 217 round * trips end to end. * * This measures with LATENCY INJECTED into the reader, because that is the property under test. Wall * clock on a local disk would measure this machine's page cache and prove nothing about the host where * the problem was reported. */ /** * The project from the report — and its absence is a failure, not a skip. * * ⛔ Found on 2026-08-12 by the new no-skips guard in `corpusCheckout.spec.ts`, while closing the gap Story 4.4 left * open for the three specs under `packages/`. This was a FOURTH one, in this directory, and nobody had recorded it: it * defers to `corpusCheckout` for the checkout — so an absent CORPUS throws — and then reinstated a private skip for * the one project inside it that this measurement needs. A corpus that no longer contains `001-projects/hapas` (renamed, * moved, pruned) would take the 217-file remote-read regression's only guard with it, silently, on a green run. * * That is the same shape as the guards this epic kept finding: a check that passes by not looking. */ const PROJECT = join(CORPUS, '001-projects/hapas'); if (!existsSync(PROJECT)) { throw new Error( `[prefetch] No project at ${PROJECT}. The concurrent-read measurement — the guard on the 2026-08-05 report ` + 'that "indexing costs 30 seconds to 5 minutes" — has no subject, and it is asserted nowhere else.\n' + 'If the corpus renamed or pruned that project, point this at another project with a comparable file count ' + 'rather than letting the measurement lapse.', ); } const suite = describe; /** A reader that charges a fixed cost per read, standing in for a remote filesystem. */ function slowReader(latencyMs: number) { const base = nodeTemplateReader(); let reads = 0; return { reads: () => reads, reader: { ...base, read: async (path: string) => { reads += 1; await new Promise((resolve) => setTimeout(resolve, latencyMs)); return base.read(path); }, }, }; } suite('prefetch does not pay one round trip per file', () => { it('reads a real 217-file project in far less than the sequential cost', async () => { const LATENCY = 4; const { reader, reads } = slowReader(LATENCY); const { mainYml, componentPath } = await searchMain( PROJECT, reader as never, ); const resolved = await resolve( componentPath, mainYml, createFsResolverIO(), ); const started = performance.now(); const sources = await prefetchSources( resolved.worklist as never, reader as never, componentPath, mainYml, ); const elapsed = performance.now() - started; const sequential = reads() * LATENCY; // eslint-disable-next-line no-console console.error( `\n[prefetch] ${reads()} reads at ${LATENCY}ms latency: ${elapsed.toFixed(0)}ms actual, ` + `${sequential}ms if sequential (${(sequential / Math.max(elapsed, 1)).toFixed(1)}x)`, ); expect(sources.size).toBeGreaterThan(100); // Comfortably under half. A sequential implementation cannot beat `reads * latency`, so this is a // structural assertion rather than a speed contest with the machine — and it is stated as a RATIO so // it holds on a slower CI box, where an absolute millisecond budget would be flaky. expect(elapsed).toBeLessThan(sequential / 2); }, 600_000); });