Files
openclaude/scripts/openclaude-bin-compile-cache.test.ts
T
BogdanandGitHub d427a4b2bb perf(cli): enable Node module compile cache (#2092)
* perf(cli): enable Node module compile cache

Warm CLI invocations spend substantial time compiling the bundled ESM entrypoint. Enable Node's optional on-disk compile cache only in the process that imports the bundle, while preserving early Node 22 compatibility and making cache failures non-fatal.

Add deterministic launcher coverage, packaging checks, and a reproducible benchmark procedure so the startup benefit can be measured without flaky CI thresholds.

* fix(ci): isolate minimum Node launcher check

The full validation suite depends on knip and oxc-parser behavior unavailable in Node 22.0.0. Keep full CI on the active Node 22 line and exercise the declared runtime floor in a dedicated build-and-launch job.

* fix(benchmark): harden startup measurements

Keep environment setup outside the timed process window, document the API's Node 22.8 floor, and preserve completed benchmark results when git metadata is unavailable.

* test(cli): verify compile cache disable behavior

Pair NODE_DISABLE_COMPILE_CACHE with a temporary cache directory and assert that supported Node releases leave it empty while preserving normal launcher output.
2026-08-07 09:55:01 +08:00

205 lines
6.5 KiB
TypeScript

import { spawnSync } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, test } from 'bun:test'
import { enableNodeCompileCacheIfAvailable } from '../bin/node-compile-cache.mjs'
const REPO_ROOT = join(import.meta.dir, '..')
const BIN_PATH = join(REPO_ROOT, 'bin', 'openclaude')
const INSTRUMENTATION_PATH = join(
import.meta.dir,
'fixtures',
'instrument-node-compile-cache.mjs',
)
const PACKAGE_VERSION = JSON.parse(
readFileSync(join(REPO_ROOT, 'package.json'), 'utf8'),
).version
const EXPECTED_VERSION_OUTPUT = `${PACKAGE_VERSION} (OpenClaude)\n`
type LauncherResult = {
status: number | null
stdout: string
stderr: string
}
function launcherEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
const env = {
...process.env,
CI: '1',
NO_COLOR: '1',
...overrides,
}
delete env.OPENCLAUDE_HEAP_RELAUNCHED
delete env.OPENCLAUDE_DISABLE_HEAP_RELAUNCH
if (!Object.hasOwn(overrides, 'NODE_OPTIONS')) delete env.NODE_OPTIONS
if (!Object.hasOwn(overrides, 'NODE_COMPILE_CACHE')) delete env.NODE_COMPILE_CACHE
if (!Object.hasOwn(overrides, 'NODE_DISABLE_COMPILE_CACHE')) delete env.NODE_DISABLE_COMPILE_CACHE
return env
}
function runLauncher(
env: NodeJS.ProcessEnv,
nodeArgs: string[] = [],
): LauncherResult {
const result = spawnSync('node', [...nodeArgs, BIN_PATH, '--version'], {
cwd: REPO_ROOT,
encoding: 'utf8',
env,
timeout: 30_000,
})
return {
status: result.status,
stdout: result.stdout ?? '',
stderr: result.stderr ?? '',
}
}
function expectNormalVersion(result: LauncherResult): void {
expect(result.status).toBe(0)
expect(result.stdout).toBe(EXPECTED_VERSION_OUTPUT)
expect(result.stderr).toBe('')
}
function nodeSupportsCompileCache(): boolean {
return spawnSync(
'node',
['-e', "process.exit(typeof require('node:module').enableCompileCache === 'function' ? 0 : 1)"],
).status === 0
}
function hasFileContent(path: string): boolean {
if (!existsSync(path)) return false
for (const entry of readdirSync(path, { withFileTypes: true })) {
const child = join(path, entry.name)
if (entry.isDirectory()) {
if (hasFileContent(child)) return true
} else {
return true
}
}
return false
}
describe('enableNodeCompileCacheIfAvailable', () => {
test('leaves startup unchanged when the API is absent', () => {
expect(() => enableNodeCompileCacheIfAvailable({})).not.toThrow()
})
test('invokes an available API exactly once without a directory', () => {
const calls: unknown[][] = []
enableNodeCompileCacheIfAvailable({
enableCompileCache: (...args: unknown[]) => {
calls.push(args)
return { status: 1, directory: '/tmp/cache' }
},
})
expect(calls).toEqual([[]])
})
test('ignores a failed status result', () => {
expect(() => enableNodeCompileCacheIfAvailable({
enableCompileCache: () => ({ status: 0, message: 'not writable' }),
})).not.toThrow()
})
test('swallows unexpected implementation throws', () => {
expect(() => enableNodeCompileCacheIfAvailable({
enableCompileCache: () => {
throw new Error('unexpected')
},
})).not.toThrow()
})
})
describe('openclaude launcher compile cache', () => {
test('does not inherit ambient NODE_OPTIONS into boundary launches', () => {
const originalNodeOptions = process.env.NODE_OPTIONS
try {
process.env.NODE_OPTIONS = '--max-old-space-size=256 --expose-gc'
expect(launcherEnv().NODE_OPTIONS).toBeUndefined()
} finally {
if (originalNodeOptions === undefined) delete process.env.NODE_OPTIONS
else process.env.NODE_OPTIONS = originalNodeOptions
}
})
test('the real absent binding remains non-fatal and silent', () => {
const scratch = mkdtempSync(join(tmpdir(), 'openclaude-compile-cache-absent-'))
try {
const result = runLauncher(
launcherEnv({
OPENCLAUDE_CONFIG_DIR: join(scratch, 'config'),
OPENCLAUDE_TEST_COMPILE_CACHE_BEHAVIOR: 'absent',
}),
['--import', INSTRUMENTATION_PATH],
)
expectNormalVersion(result)
} finally {
rmSync(scratch, { recursive: true, force: true })
}
})
for (const behavior of ['success', 'failed-status', 'throw'] as const) {
test(`the final importing process survives ${behavior} setup`, () => {
if (!nodeSupportsCompileCache()) return
const scratch = mkdtempSync(join(tmpdir(), `openclaude-compile-cache-${behavior}-`))
const markerPath = join(scratch, 'calls.jsonl')
try {
const result = runLauncher(
launcherEnv({
OPENCLAUDE_CONFIG_DIR: join(scratch, 'config'),
OPENCLAUDE_TEST_COMPILE_CACHE_BEHAVIOR: behavior,
OPENCLAUDE_TEST_COMPILE_CACHE_MARKER: markerPath,
}),
['--import', INSTRUMENTATION_PATH],
)
expectNormalVersion(result)
const calls = readFileSync(markerPath, 'utf8')
.trim()
.split('\n')
.map(line => JSON.parse(line))
expect(calls).toHaveLength(1)
expect(calls[0]).toMatchObject({ heapRelaunched: true })
} finally {
rmSync(scratch, { recursive: true, force: true })
}
})
}
test('NODE_DISABLE_COMPILE_CACHE remains authoritative', () => {
const scratch = mkdtempSync(join(tmpdir(), 'openclaude-compile-cache-disabled-'))
const cacheDir = join(scratch, 'cache')
try {
expectNormalVersion(runLauncher(launcherEnv({
NODE_COMPILE_CACHE: cacheDir,
NODE_DISABLE_COMPILE_CACHE: '1',
OPENCLAUDE_CONFIG_DIR: join(scratch, 'config'),
})))
if (nodeSupportsCompileCache()) expect(hasFileContent(cacheDir)).toBe(false)
} finally {
rmSync(scratch, { recursive: true, force: true })
}
})
test('a temporary NODE_COMPILE_CACHE gains content without changing output', () => {
if (!nodeSupportsCompileCache()) return
const scratch = mkdtempSync(join(tmpdir(), 'openclaude-compile-cache-real-'))
const cacheDir = join(scratch, 'cache')
try {
const env = launcherEnv({
NODE_COMPILE_CACHE: cacheDir,
OPENCLAUDE_CONFIG_DIR: join(scratch, 'config'),
})
expectNormalVersion(runLauncher(env))
expectNormalVersion(runLauncher(env))
expect(hasFileContent(cacheDir)).toBe(true)
} finally {
rmSync(scratch, { recursive: true, force: true })
}
})
})