@@ -19,6 +19,8 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
1919const tempDirs : string [ ] = [ ] ;
2020const repoRoot = process . cwd ( ) ;
2121const fakeCrabboxBinDirs = new Map < string , string > ( ) ;
22+ const fakeGitBinDirs = new Map < string , string > ( ) ;
23+ const timingPreloads = new Map < string , string > ( ) ;
2224const GIT_COMMON_DIR_KEY = "rev-parse\u0000--git-common-dir" ;
2325const GIT_CONFIG_SPARSE_KEY = "config\u0000--bool\u0000core.sparseCheckout" ;
2426const GIT_SPARSE_LIST_KEY = "sparse-checkout\u0000list" ;
@@ -213,6 +215,38 @@ function makeSlowVersionCrabbox(helpText: string): string {
213215 return binDir ;
214216}
215217
218+ function testTimingPreload ( options : { clockScale ?: number ; spawnTimeoutMs ?: number } ) : string {
219+ const key = JSON . stringify ( options ) ;
220+ let preloadPath = timingPreloads . get ( key ) ;
221+ if ( ! preloadPath ) {
222+ const dir = mkdtempSync ( path . join ( tmpdir ( ) , "openclaw-crabbox-timing-" ) ) ;
223+ tempDirs . push ( dir ) ;
224+ preloadPath = path . join ( dir , "preload.cjs" ) ;
225+ const script : string [ ] = [ ] ;
226+ if ( options . clockScale !== undefined ) {
227+ script . push (
228+ "const realNow = Date.now.bind(Date);" ,
229+ "const startedAt = realNow();" ,
230+ `Date.now = () => startedAt + (realNow() - startedAt) * ${ options . clockScale } ;` ,
231+ ) ;
232+ }
233+ if ( options . spawnTimeoutMs !== undefined ) {
234+ script . push (
235+ 'const childProcess = require("node:child_process");' ,
236+ 'const { syncBuiltinESMExports } = require("node:module");' ,
237+ "const realSpawnSync = childProcess.spawnSync;" ,
238+ "childProcess.spawnSync = (command, args, spawnOptions) =>" ,
239+ " realSpawnSync(command, args," ,
240+ ` spawnOptions?.timeout ? { ...spawnOptions, timeout: Math.min(spawnOptions.timeout, ${ options . spawnTimeoutMs } ) } : spawnOptions);` ,
241+ "syncBuiltinESMExports();" ,
242+ ) ;
243+ }
244+ writeFileSync ( preloadPath , `${ script . join ( "\n" ) } \n` , "utf8" ) ;
245+ timingPreloads . set ( key , preloadPath ) ;
246+ }
247+ return preloadPath ;
248+ }
249+
216250function windowsNodeCmdShim ( target : string ) : string {
217251 return [
218252 "@ECHO off" ,
@@ -243,6 +277,11 @@ function shellSingleQuote(value: string): string {
243277function makeFakeGit (
244278 responses : Record < string , { status ?: number ; stdout ?: string ; stderr ?: string } > ,
245279) : string {
280+ const key = JSON . stringify ( responses ) ;
281+ const cached = fakeGitBinDirs . get ( key ) ;
282+ if ( cached ) {
283+ return cached ;
284+ }
246285 const binDir = mkdtempSync ( path . join ( tmpdir ( ) , "openclaw-fake-git-" ) ) ;
247286 tempDirs . push ( binDir ) ;
248287 const gitPath = path . join ( binDir , "git" ) ;
@@ -277,6 +316,7 @@ function makeFakeGit(
277316 ] . join ( "\n" ) ;
278317 writeFileSync ( gitPath , `${ script } \n` , "utf8" ) ;
279318 chmodSync ( gitPath , 0o755 ) ;
319+ fakeGitBinDirs . set ( key , binDir ) ;
280320 return binDir ;
281321 }
282322
@@ -299,6 +339,7 @@ function makeFakeGit(
299339 writeFileSync ( gitPath , `${ script } \n` , "utf8" ) ;
300340 writeFileSync ( `${ gitPath } .cmd` , windowsNodeCmdShim ( "git" ) , "utf8" ) ;
301341 chmodSync ( gitPath , 0o755 ) ;
342+ fakeGitBinDirs . set ( key , binDir ) ;
302343 return binDir ;
303344}
304345
@@ -311,7 +352,12 @@ function shellArgListCondition(args: string[]): string {
311352}
312353
313354function runWrapper ( helpText : string , args : string [ ] , options : WrapperOptions = { } ) {
314- return spawnSync ( process . execPath , [ "scripts/crabbox-wrapper.mjs" , ...args ] , {
355+ const nodeArgs = [
356+ ...( options . nodePreload ? [ "--require" , options . nodePreload ] : [ ] ) ,
357+ "scripts/crabbox-wrapper.mjs" ,
358+ ...args ,
359+ ] ;
360+ return spawnSync ( process . execPath , nodeArgs , {
315361 cwd : repoRoot ,
316362 encoding : "utf8" ,
317363 input : options . input ,
@@ -327,10 +373,16 @@ type WrapperOptions = {
327373 extraPathEntries ?: string [ ] ;
328374 gitResponses ?: Record < string , { status ?: number ; stdout ?: string ; stderr ?: string } > ;
329375 input ?: string ;
376+ nodePreload ?: string ;
330377} ;
331378
332379function spawnWrapper ( helpText : string , args : string [ ] , options : WrapperOptions = { } ) {
333- return spawn ( process . execPath , [ "scripts/crabbox-wrapper.mjs" , ...args ] , {
380+ const nodeArgs = [
381+ ...( options . nodePreload ? [ "--require" , options . nodePreload ] : [ ] ) ,
382+ "scripts/crabbox-wrapper.mjs" ,
383+ ...args ,
384+ ] ;
385+ return spawn ( process . execPath , nodeArgs , {
334386 cwd : repoRoot ,
335387 env : wrapperEnv ( helpText , options ) ,
336388 stdio : [ "ignore" , "pipe" , "pipe" ] ,
@@ -442,6 +494,7 @@ async function runSignalCleanupProof(sendSignals: (pid: number) => Promise<void>
442494 env : {
443495 OPENCLAW_FAKE_CRABBOX_DESCENDANT_PID_PATH : descendantPidPath ,
444496 } ,
497+ nodePreload : testTimingPreload ( { clockScale : 20 } ) ,
445498 } ,
446499 ) ;
447500
@@ -3060,6 +3113,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
30603113 const helpText = "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n" ;
30613114 const result = runWrapper ( helpText , [ "--version" ] , {
30623115 extraPathEntries : [ makeSlowVersionCrabbox ( helpText ) ] ,
3116+ nodePreload : testTimingPreload ( { spawnTimeoutMs : 25 } ) ,
30633117 } ) ;
30643118
30653119 expect ( result . error ) . toBeUndefined ( ) ;
@@ -3185,25 +3239,6 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
31853239 ) ;
31863240 } ) ;
31873241
3188- it ( "rebuilds stale remote Git metadata before sparse changed gates" , ( ) => {
3189- const result = runWrapper (
3190- "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n" ,
3191- [ "run" , "--provider" , "aws" , "--" , "corepack" , "pnpm" , "check:changed" ] ,
3192- {
3193- gitResponses : {
3194- [ GIT_CONFIG_SPARSE_KEY ] : { stdout : "true\n" } ,
3195- [ GIT_STATUS_PORCELAIN_KEY ] : { stdout : "" } ,
3196- [ GIT_MERGE_BASE_MAIN_HEAD_KEY ] : { stdout : "abc123\n" } ,
3197- } ,
3198- } ,
3199- ) ;
3200-
3201- const output = parseFakeCrabboxOutput ( result ) ;
3202- const remoteCommand = normalizeShellLineEndings ( output . args . at ( - 1 ) ?? "" ) ;
3203- expect ( result . status ) . toBe ( 0 ) ;
3204- expectChangedGateGitBootstrap ( remoteCommand ) ;
3205- } ) ;
3206-
32073242 it ( "bootstraps Git metadata for non-sparse changed gates on remote raw syncs" , ( ) => {
32083243 const result = runWrapper (
32093244 "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n" ,
0 commit comments