Files
oh-my-opencode/script/run-ci-tests.ts
T
YeonGyu-Kim 2c081e1999 fix(ci): restore isolated Bun test execution for mock.module suites
Running Linux CI in one Bun process still leaks mock.module registrations across files, so the workflows now use a CI-specific test runner that isolates mock-heavy targets before executing the remaining suite together.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-04 15:51:54 +09:00

129 lines
3.8 KiB
TypeScript

/// <reference types="bun-types" />
type CiTestPlan = {
isolatedTestTargets: string[]
isolatedModuleMockFiles: string[]
sharedTestFiles: string[]
}
const TEST_ROOTS = ["script", "src"] as const
const MODULE_MOCK_PATTERN = "mock.module("
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 {
const pathSegments = testFile.split("/")
if (pathSegments.length <= 3) {
return testFile
}
return pathSegments.slice(0, -1).join("/")
}
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}/`)
})
})
}
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)
}
}
const isolatedTestTargets = collapseNestedTargets(
Array.from(new Set(isolatedModuleMockFiles.map((testFile) => toIsolatedTarget(testFile)))).sort((left, right) =>
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}`)
const command = ["bun", "test", ...testFiles]
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> {
const ciTestPlan = await createCiTestPlan()
console.log(
`Detected ${ciTestPlan.isolatedModuleMockFiles.length} mock.module() test files, ${ciTestPlan.isolatedTestTargets.length} isolated targets, and ${ciTestPlan.sharedTestFiles.length} shared test files.`,
)
for (const isolatedTestTarget of ciTestPlan.isolatedTestTargets) {
await runBunTest([isolatedTestTarget], `Isolated ${isolatedTestTarget}`)
}
await runBunTest(ciTestPlan.sharedTestFiles, "Shared Bun test suite")
}
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)
}
}