2026-04-04 15:51:54 +09:00
/// <reference types="bun-types" />
type CiTestPlan = {
isolatedTestTargets : string [ ]
isolatedModuleMockFiles : string [ ]
sharedTestFiles : string [ ]
}
2026-05-15 11:41:35 +09:00
type CiTestPhase = "all" | "isolated" | "shared"
type CiTestRunOptions = {
phase : CiTestPhase
shardCount : number
shardIndex : number
}
type CiTestTargetSelection = {
isolatedTestTargets : string [ ]
sharedTestFiles : string [ ]
}
2026-04-04 16:12:22 +09:00
const TEST_ROOTS = [ "bin" , "script" , "src" ] as const
2026-04-04 15:51:54 +09:00
const MODULE_MOCK_PATTERN = "mock.module("
2026-04-28 10:48:38 +09:00
const ALWAYS_ISOLATED_TEST_FILES = [
"src/features/team-mode/team-mailbox/ack.test.ts" ,
"src/features/team-mode/team-mailbox/send.test.ts" ,
"src/features/team-mode/team-runtime/shutdown.test.ts" ,
"src/features/team-mode/team-runtime/status.test.ts" ,
"src/features/team-mode/team-state-store/resume.test.ts" ,
"src/features/team-mode/team-state-store/store.test.ts" ,
2026-05-06 11:11:12 +09:00
"src/features/boulder-state/storage.test.ts" ,
2026-04-28 10:48:38 +09:00
"src/hooks/anthropic-context-window-limit-recovery/aggressive-truncation-strategy.test.ts" ,
2026-05-06 11:11:12 +09:00
"src/hooks/session-notification-input-needed.test.ts" ,
"src/hooks/session-notification-sender.test.ts" ,
"src/hooks/session-notification.test.ts" ,
2026-04-28 10:48:38 +09:00
"src/openclaw/__tests__/reply-listener-discord.test.ts" ,
2026-05-06 11:11:12 +09:00
"src/tools/background-task/create-background-output.blocking.test.ts" ,
"src/tools/background-task/tools.test.ts" ,
2026-05-07 13:01:33 +09:00
"src/tools/interactive-bash/tmux-path-resolver.test.ts" ,
2026-05-06 11:11:12 +09:00
"src/tools/task/task-list.test.ts" ,
2026-04-28 10:48:38 +09:00
] as const
2026-04-04 15:51:54 +09:00
async function collectTestFiles ( rootDirectory : string ) : Promise < string [ ] > {
const testFiles : string [ ] = [ ]
for ( const testRoot of TEST_ROOTS ) {
const glob = new Bun . Glob ( "**/*.test.ts" )
for await ( const testFile of glob . scan ( { cwd : ` ${ rootDirectory } / ${ testRoot } ` } ) ) {
testFiles . push ( ` ${ testRoot } / ${ testFile } ` )
}
}
return testFiles . sort ( ( left , right ) = > left . localeCompare ( right ) )
}
async function usesModuleMock ( rootDirectory : string , testFile : string ) : Promise < boolean > {
const testContents = await Bun . file ( ` ${ rootDirectory } / ${ testFile } ` ) . text ( )
return testContents . includes ( MODULE_MOCK_PATTERN )
}
function toIsolatedTarget ( testFile : string ) : string {
2026-04-09 12:09:09 +09:00
return testFile
2026-04-04 15:51:54 +09:00
}
function isCoveredByTarget ( testFile : string , isolatedTarget : string ) : boolean {
return testFile === isolatedTarget || testFile . startsWith ( ` ${ isolatedTarget } / ` )
}
function collapseNestedTargets ( isolatedTargets : string [ ] ) : string [ ] {
return isolatedTargets . filter ( ( isolatedTarget ) = > {
return ! isolatedTargets . some ( ( otherTarget ) = > {
return otherTarget !== isolatedTarget && isolatedTarget . startsWith ( ` ${ otherTarget } / ` )
} )
} )
}
2026-05-15 11:41:35 +09:00
function readFlagValue ( args : string [ ] , flagName : string ) : string | null {
const prefix = ` ${ flagName } = `
const flag = args . find ( ( arg ) = > arg . startsWith ( prefix ) )
return flag ? . slice ( prefix . length ) ? ? null
}
function parsePhase ( rawPhase : string | null ) : CiTestPhase {
if ( rawPhase === null ) {
return "all"
}
if ( rawPhase === "all" || rawPhase === "isolated" || rawPhase === "shared" ) {
return rawPhase
}
throw new Error ( ` Invalid --phase value: ${ rawPhase } . Expected all, isolated, or shared. ` )
}
function parsePositiveIntegerFlag ( args : string [ ] , flagName : string , defaultValue : number ) : number {
const rawValue = readFlagValue ( args , flagName )
if ( rawValue === null ) {
return defaultValue
}
const parsedValue = Number ( rawValue )
if ( ! Number . isInteger ( parsedValue ) || parsedValue < 1 ) {
throw new Error ( ` Invalid ${ flagName } value: ${ rawValue } . Expected a positive integer. ` )
}
return parsedValue
}
function parseNonNegativeIntegerFlag ( args : string [ ] , flagName : string , defaultValue : number ) : number {
const rawValue = readFlagValue ( args , flagName )
if ( rawValue === null ) {
return defaultValue
}
const parsedValue = Number ( rawValue )
if ( ! Number . isInteger ( parsedValue ) || parsedValue < 0 ) {
throw new Error ( ` Invalid ${ flagName } value: ${ rawValue } . Expected a non-negative integer. ` )
}
return parsedValue
}
function parseCiTestRunOptions ( args : string [ ] ) : CiTestRunOptions {
const phase = parsePhase ( readFlagValue ( args , "--phase" ) )
const shardCount = parsePositiveIntegerFlag ( args , "--shard-count" , 1 )
const shardIndex = parseNonNegativeIntegerFlag ( args , "--shard-index" , 0 )
if ( shardIndex >= shardCount ) {
throw new Error ( ` Invalid --shard-index value: ${ shardIndex } . Expected a value less than --shard-count ${ shardCount } . ` )
}
if ( shardCount > 1 && phase !== "isolated" ) {
throw new Error ( "Test sharding is only supported with --phase=isolated." )
}
return { phase , shardCount , shardIndex }
}
function selectShard ( testTargets : string [ ] , shardCount : number , shardIndex : number ) : string [ ] {
if ( shardCount === 1 ) {
return testTargets
}
return testTargets . filter ( ( _ , index ) = > index % shardCount === shardIndex )
}
export function selectCiTestTargets ( ciTestPlan : CiTestPlan , options : CiTestRunOptions ) : CiTestTargetSelection {
const isolatedTestTargets = options . phase === "shared"
? [ ]
: selectShard ( ciTestPlan . isolatedTestTargets , options . shardCount , options . shardIndex )
const sharedTestFiles = options . phase === "isolated" ? [ ] : ciTestPlan . sharedTestFiles
return { isolatedTestTargets , sharedTestFiles }
}
2026-04-04 15:51:54 +09:00
export async function createCiTestPlan ( rootDirectory : string = process . cwd ( ) ) : Promise < CiTestPlan > {
const allTestFiles = await collectTestFiles ( rootDirectory )
const isolatedModuleMockFiles : string [ ] = [ ]
for ( const testFile of allTestFiles ) {
if ( await usesModuleMock ( rootDirectory , testFile ) ) {
isolatedModuleMockFiles . push ( testFile )
}
}
2026-04-09 12:09:09 +09:00
const isolatedTestFiles = Array . from (
new Set ( [ . . . isolatedModuleMockFiles , . . . ALWAYS_ISOLATED_TEST_FILES . filter ( ( testFile ) = > allTestFiles . includes ( testFile ) ) ] ) ,
)
2026-04-04 15:51:54 +09:00
const isolatedTestTargets = collapseNestedTargets (
2026-04-09 12:09:09 +09:00
isolatedTestFiles . map ( ( testFile ) = > toIsolatedTarget ( testFile ) ) . sort ( ( left , right ) = >
2026-04-04 15:51:54 +09:00
left . localeCompare ( right ) ,
) ,
)
const sharedTestFiles = allTestFiles . filter ( ( testFile ) = > {
return ! isolatedTestTargets . some ( ( isolatedTarget ) = > isCoveredByTarget ( testFile , isolatedTarget ) )
} )
return {
isolatedTestTargets ,
isolatedModuleMockFiles ,
sharedTestFiles ,
}
}
async function runBunTest ( testFiles : string [ ] , label : string ) : Promise < void > {
if ( testFiles . length === 0 ) {
return
}
console . log ( ` ::group:: ${ label } ` )
2026-05-15 11:41:35 +09:00
const args = testFiles . map ( ( testFile ) = > {
if ( testFile . includes ( "/" ) && ! testFile . endsWith ( ".test.ts" ) ) {
return [ testFile , "!_auc-*/**/*.test.ts" ]
2026-04-04 17:53:24 +09:00
}
2026-05-15 11:41:35 +09:00
return testFile
2026-04-04 17:53:24 +09:00
} ) . flat ( )
2026-05-15 11:41:35 +09:00
2026-04-04 17:53:24 +09:00
const command = [ "bun" , "test" , . . . args ]
2026-04-04 15:51:54 +09:00
const spawnedProcess = Bun . spawn ( command , {
cwd : process.cwd ( ) ,
stdin : "inherit" ,
stdout : "inherit" ,
stderr : "inherit" ,
} )
const exitCode = await spawnedProcess . exited
console . log ( "::endgroup::" )
if ( exitCode !== 0 ) {
throw new Error ( ` Command failed: ${ command . join ( " " ) } ` )
}
}
async function main ( ) : Promise < void > {
2026-05-15 11:41:35 +09:00
const options = parseCiTestRunOptions ( process . argv . slice ( 2 ) )
2026-04-04 15:51:54 +09:00
const ciTestPlan = await createCiTestPlan ( )
2026-05-15 11:41:35 +09:00
const selectedTargets = selectCiTestTargets ( ciTestPlan , options )
2026-04-04 15:51:54 +09:00
console . log (
` Detected ${ ciTestPlan . isolatedModuleMockFiles . length } mock.module() test files, ${ ciTestPlan . isolatedTestTargets . length } isolated targets, and ${ ciTestPlan . sharedTestFiles . length } shared test files. ` ,
)
2026-05-15 11:41:35 +09:00
if ( options . phase === "isolated" && options . shardCount > 1 ) {
console . log (
` Running isolated test shard ${ options . shardIndex + 1 } / ${ options . shardCount } with ${ selectedTargets . isolatedTestTargets . length } targets. ` ,
)
}
for ( const isolatedTestTarget of selectedTargets . isolatedTestTargets ) {
2026-04-04 15:51:54 +09:00
await runBunTest ( [ isolatedTestTarget ] , ` Isolated ${ isolatedTestTarget } ` )
}
2026-05-15 11:41:35 +09:00
await runBunTest ( selectedTargets . sharedTestFiles , "Shared Bun test suite" )
2026-04-04 15:51:54 +09:00
}
export const moduleMockPattern = MODULE_MOCK_PATTERN
export const testRoots = TEST_ROOTS
if ( process . argv . includes ( "--print-plan" ) ) {
const ciTestPlan = await createCiTestPlan ( )
console . log ( JSON . stringify ( ciTestPlan , null , 2 ) )
} else if ( import . meta . main ) {
try {
await main ( )
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error )
console . error ( message )
process . exit ( 1 )
}
}