mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
* 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.
205 lines
6.5 KiB
TypeScript
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 })
|
|
}
|
|
})
|
|
})
|