diff --git a/.github/OPENCLAW_CI.md b/.github/OPENCLAW_CI.md index bd15c5777c7e..5b0174c0e6b4 100644 --- a/.github/OPENCLAW_CI.md +++ b/.github/OPENCLAW_CI.md @@ -157,8 +157,28 @@ tests. The same failure reproduces with continuous cache-file progress under Microsoft Defender. The Windows rename helper requested `FILE_TRAVERSE`, which is `FILE_EXECUTE` for a regular file, forcing synchronous executable-file scans for each cache entry. The helper now uses its existing non-executable access -rights directly. The test workloads, deadlines, idle-generation window, and -signal-exit budget are unchanged. +rights directly in [PR #130](https://github.com/openclaw/bun/pull/130), without +changing the test workloads, deadlines, idle-generation window, or signal-exit budget. + +The deferred-idle test hit its outer 30-second deadline again in Windows x64 +[run 37386790583](https://github.com/openclaw/bun/actions/runs/37386790583), with +#130 present. That run retained no progress timeline, so its runner-level cause +is unknown. On a separate Windows x64 host with Defender enabled, limiting the +exact binary's CPU budget reproduced the timeout while entries kept increasing. +An independent driver completed all 2,002 entries in 64.7 seconds; suspending the +child instead triggered the existing ten-second no-progress guard. + +Using a progress-only deadline also exposed a test-runner bug: a stale file +timer treated the unlimited entry's zero timestamp as expired and killed its +child. The process-reaping check now excludes the no-deadline sentinel, matching +the entry's timeout check. A separate regression covers the completed-deadline +and live-child sequence. + +The test now bounds module loading, pipe flushes, and child exit individually +and keeps the ten-second persistence stall guard. It has no aggregate duration +limit: slow, steadily increasing entry counts satisfy the idle-progress +contract. The 2,000-module workload, late-module handshake, expected 2,002 files, +idle-generation window, and signal-exit budget are unchanged. Keep later failures distinguishable from these causes; a green rerun does not establish a cause. The native lane has no automatic test retries or exclusion diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2335532e3e..715024970a92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -222,3 +222,7 @@ - Load CommonJS native addons through Windows namespaced paths so long installation paths work with addon activation contexts, while preserving module cache keys and embedded-addon extraction. - Resolve Windows namespaced CommonJS paths without treating the device prefix as a query, preserving Node's distinct cache keys and native-addon loading. + +- Detect deferred compile-cache test stalls by persistence progress, with bounded startup and shutdown, so slow Windows runners can complete the unchanged workload. + +- Preserve an unlimited test's child processes when a completed test's stale deadline fires. diff --git a/src/runtime/test_runner/Execution.rs b/src/runtime/test_runner/Execution.rs index 91139275776e..c85bf86d6b36 100644 --- a/src/runtime/test_runner/Execution.rs +++ b/src/runtime/test_runner/Execution.rs @@ -319,7 +319,10 @@ impl Execution { // SAFETY: arena-owned entry, alive for lifetime of BunTest let entry = unsafe { entry.as_ref() }; let now = Timespec::now_force_real_time(); - if entry.timespec.order(&now) == core::cmp::Ordering::Less { + // A stale file timer can fire while an unlimited entry is active. + if !entry.timespec.eql(&Timespec::EPOCH) + && entry.timespec.order(&now) == core::cmp::Ordering::Less + { // SAFETY: bun_vm() returns the live per-thread VM. let kill_count = global_this.bun_vm().as_mut().auto_killer.kill(); if kill_count.processes > 0 { diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 1fad55319b9b..07994f0e744c 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -554,6 +554,44 @@ it("test timeouts when expected", () => { expect(err).not.toContain("unreachable code"); }); +test("a completed deadline does not kill an unlimited test's child", async () => { + using dir = tempDir("unlimited-test-child", { + "child.test.js": ` + import { afterAll, beforeAll, expect, test } from "bun:test"; + let child; + beforeAll(() => { + child = Bun.spawn([process.execPath, "-e", "setInterval(() => {}, 1000)"], { + stdin: "ignore", + stdout: "ignore", + stderr: "inherit", + }); + }); + afterAll(() => { + child.kill(); + child.unref(); + }); + test("completed finite deadline", () => {}, 50); + test("unlimited test retains its child", async () => { + // Cross the completed test's deadline while this entry owns a live child. + await Bun.sleep(100); + expect(() => process.kill(child.pid, 0)).not.toThrow(); + }, 0); + `, + }); + await using proc = spawn({ + cmd: [bunExe(), "test", "child.test.js"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toContain("2 pass"); + expect(stderr).toContain("0 fail"); + expect(stderr).not.toContain("killed"); +}); + test("jest.setTimeout will change default timeout", () => { const path = join(tmp, "jest-setTimeout-test.test.js"); copyFileSync(join(import.meta.dir, "setTimeout-test-fixture.js"), path); @@ -735,10 +773,10 @@ test("my-test", () => { const stackLines = output.split("\n").filter(line => line.trim().startsWith("at ")); expect(stackLines.length).toBeGreaterThan(0); if (process.platform === "win32") { - expect(stackLines[0]).toContain(`