Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion src/bun.js/VirtualMachine.zig
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,51 @@ pub fn handledPromise(this: *jsc.VirtualMachine, globalObject: *JSGlobalObject,
return Bun__emitHandledPromiseEvent(globalObject, promise);
}

/// Check if an error is a module-not-found error and start polling for it in watch mode
fn tryHandleMissingModuleInWatchMode(this: *jsc.VirtualMachine, globalObject: *JSGlobalObject, err: JSValue) void {
// Only poll in watch mode
if (this.hot_reload != .watch) return;

if (!err.isObject()) return;

const error_obj = err.uncheckedPtrCast(jsc.JSObject);

// Get the error code without triggering getters
const code_value = error_obj.getCodePropertyVMInquiry(globalObject) orelse return;

if (!code_value.isString()) return;

const code_str = code_value.toBunString(globalObject) catch return;
defer code_str.deref();

// Check if it's a module-not-found error code
const is_module_not_found = code_str.eqlComptime("ERR_MODULE_NOT_FOUND") or
code_str.eqlComptime("MODULE_NOT_FOUND");

if (!is_module_not_found) return;

// Try to extract the specifier
const specifier_value = (err.get(globalObject, "specifier") catch return) orelse return;

if (!specifier_value.isString()) return;

const specifier = specifier_value.toBunString(globalObject) catch return;
defer specifier.deref();

// Convert to a slice
const specifier_slice = specifier.toUTF8(bun.default_allocator);
defer specifier_slice.deinit();

// Start polling for this path
this.timer.missing_module_poll_timer.startPolling(this, specifier_slice.slice()) catch {
// If we can't start polling, just continue normally
return;
};

Output.prettyErrorln("<r><yellow>Waiting for module to exist:<r> {s}", .{specifier_slice.slice()});
Output.flush();
}

pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObject, err: JSValue, is_rejection: bool) bool {
if (this.isShuttingDown()) {
return true;
Expand All @@ -665,6 +710,10 @@ pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObjec
bun.api.node.process.exit(globalObject, 1);
@panic("made it past Bun__Process__exit");
}

// Try to handle missing module in watch mode before exiting
this.tryHandleMissingModuleInWatchMode(globalObject, err);

this.is_handling_uncaught_exception = true;
defer this.is_handling_uncaught_exception = false;
const handled = Bun__handleUncaughtException(globalObject, err.toError() orelse err, if (is_rejection) 1 else 0) > 0;
Expand All @@ -680,7 +729,12 @@ pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObjec
pub fn handlePendingInternalPromiseRejection(this: *jsc.VirtualMachine) void {
var promise = this.pending_internal_promise.?;
if (promise.status(this.global.vm()) == .rejected and !promise.isHandled(this.global.vm())) {
this.unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue());
const rejection_value = promise.result(this.global.vm());

// Try to handle missing module in watch mode
this.tryHandleMissingModuleInWatchMode(this.global, rejection_value);

this.unhandledRejection(this.global, rejection_value, promise.asValue());
promise.setHandled(this.global.vm());
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/bun.js/api/Timer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub const All = struct {
/// Updates the "Date" header.
date_header_timer: DateHeaderTimer = .{},

/// Polls for missing modules in watch mode
missing_module_poll_timer: MissingModulePollTimer = MissingModulePollTimer.init(),

Comment on lines +55 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add lifecycle cleanup for MissingModulePollTimer

This timer duplicates strings; without calling its deinit() on VM teardown, memory can leak. Ensure VM.deinit invokes vm.timer.missing_module_poll_timer.deinit(vm).


🏁 Script executed:

#!/bin/bash
set -e
rg -n "MissingModulePollTimer" -n src/bun.js/api/Timer.zig
rg -n "fn deinit" -n src/bun.js/api/Timer.zig

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -e
echo "=== Timer.zig (first 200 lines) ==="
sed -n '1,200p' src/bun.js/api/Timer.zig
echo "=== MissingModulePollTimer.zig (first 200 lines) ==="
sed -n '1,200p' src/bun.js/api/Timer/MissingModulePollTimer.zig
echo "=== Search for deinit in MissingModulePollTimer.zig ==="
rg -n "deinit" -n src/bun.js/api/Timer/MissingModulePollTimer.zig || true

Length of output: 11786


🏁 Script executed:

#!/bin/bash
set -e
# Find VirtualMachine.zig
fd --extension zig VirtualMachine.zig
# Display first 200 lines of its file(s)
for f in $(fd --extension zig VirtualMachine.zig); do
  echo "=== $f ==="
  sed -n '1,200p' "$f"
done
# Search for deinit or finalize
rg -n "fn deinit" -n $(fd --extension zig VirtualMachine.zig) || true
rg -n "fn finalize" -n $(fd --extension zig VirtualMachine.zig) || true

Length of output: 7515


🏁 Script executed:

#!/bin/bash
set -e
echo "=== VirtualMachine.deinit (lines 1980-2100) ==="
sed -n '1980,2100p' src/bun.js/VirtualMachine.zig
echo "=== References to missing_module_poll_timer in VirtualMachine.zig ==="
rg -n "missing_module_poll_timer" src/bun.js/VirtualMachine.zig || echo "no references"

Length of output: 3986


Invoke MissingModulePollTimer.deinit in VM teardown

Add a call to this.timer.missing_module_poll_timer.deinit(this); inside VirtualMachine.deinit to free the duplicated path string and stop the active poll timer.

🤖 Prompt for AI Agents
In src/bun.js/api/Timer.zig around lines 55-57, the MissingModulePollTimer
created at missing_module_poll_timer: MissingModulePollTimer =
MissingModulePollTimer.init() allocates a duplicated path string and starts an
active poll timer that isn't being cleaned up; add a call to
this.timer.missing_module_poll_timer.deinit(this); inside the
VirtualMachine.deinit method so the duplicated path is freed and the poll timer
is stopped as part of VM teardown.

pub fn init() @This() {
return .{
.thread_id = std.Thread.getCurrentId(),
Expand Down Expand Up @@ -602,6 +605,8 @@ pub const DateHeaderTimer = @import("./Timer/DateHeaderTimer.zig");

pub const EventLoopDelayMonitor = @import("./Timer/EventLoopDelayMonitor.zig");

pub const MissingModulePollTimer = @import("./Timer/MissingModulePollTimer.zig");

pub const internal_bindings = struct {
/// Node.js has some tests that check whether timers fire at the right time. They check this
/// with the internal binding `getLibuvNow()`, which returns an integer in milliseconds. This
Expand Down
6 changes: 6 additions & 0 deletions src/bun.js/api/Timer/EventLoopTimer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub const Tag = enum {
DateHeaderTimer,
BunTest,
EventLoopDelayMonitor,
MissingModulePollTimer,

pub fn Type(comptime T: Tag) type {
return switch (T) {
Expand All @@ -94,6 +95,7 @@ pub const Tag = enum {
.DateHeaderTimer => jsc.API.Timer.DateHeaderTimer,
.BunTest => jsc.Jest.bun_test.BunTest,
.EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor,
.MissingModulePollTimer => jsc.API.Timer.MissingModulePollTimer,
};
}
};
Expand Down Expand Up @@ -188,6 +190,10 @@ pub fn fire(self: *Self, now: *const timespec, vm: *VirtualMachine) Arm {
monitor.onFire(vm, now);
return .disarm;
},
.MissingModulePollTimer => {
const poll_timer = @as(*jsc.API.Timer.MissingModulePollTimer, @fieldParentPtr("event_loop_timer", self));
return poll_timer.onTimeout(vm);
},
inline else => |t| {
if (@FieldType(t.Type(), "event_loop_timer") != Self) {
@compileError(@typeName(t.Type()) ++ " has wrong type for 'event_loop_timer'");
Expand Down
146 changes: 146 additions & 0 deletions src/bun.js/api/Timer/MissingModulePollTimer.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/// MissingModulePollTimer manages polling for missing modules in watch mode.
///
/// When a script tries to import a file that doesn't exist, Bun's watch mode should
/// wait for the file to be created rather than exiting with an error. This timer
/// implements exponential backoff polling to check if the missing file has appeared.
///
/// Behavior:
/// - Starts at 2ms intervals, exponentially backs off to max 100ms
/// - Checks if the missing file exists on each poll
/// - Triggers a process reload when the file appears
/// - Stops polling if watch mode is disabled or VM is shutting down
const MissingModulePollTimer = @This();

event_loop_timer: jsc.API.Timer.EventLoopTimer = .{
.tag = .MissingModulePollTimer,
.next = .epoch,
},

/// The path to the missing module that we're polling for
missing_path: []const u8 = &.{},

/// Current polling interval in milliseconds
current_interval_ms: u32 = 2,

/// Minimum polling interval (2ms)
min_interval_ms: u32 = 2,

/// Maximum polling interval (100ms)
max_interval_ms: u32 = 100,

/// Whether the timer is actively polling
is_polling: bool = false,

pub fn init() MissingModulePollTimer {
return .{};
}

/// Start polling for a missing module
pub fn startPolling(this: *MissingModulePollTimer, vm: *VirtualMachine, missing_path: []const u8) !void {
// If already polling, stop the current timer first
if (this.is_polling) {
this.stopPolling(vm);
}

// Store a copy of the path
if (this.missing_path.len > 0) {
bun.default_allocator.free(this.missing_path);
}
this.missing_path = try bun.default_allocator.dupe(u8, missing_path);

// Reset interval to minimum
this.current_interval_ms = this.min_interval_ms;
this.is_polling = true;

// Schedule the first poll
this.scheduleNextPoll(vm);

log("Started polling for missing module: {s} (interval: {}ms)", .{ this.missing_path, this.current_interval_ms });
}

/// Stop polling for the missing module
pub fn stopPolling(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
if (!this.is_polling) return;

if (this.event_loop_timer.state == .ACTIVE) {
vm.timer.remove(&this.event_loop_timer);
}

this.is_polling = false;
this.current_interval_ms = this.min_interval_ms;

log("Stopped polling for missing module: {s}", .{this.missing_path});
}

Comment on lines +61 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Free duplicated path on stop to avoid leaks

missing_path is duped in startPolling but never freed on stopPolling(). Free it and reset to empty.

 pub fn stopPolling(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
     if (!this.is_polling) return;
 
     if (this.event_loop_timer.state == .ACTIVE) {
         vm.timer.remove(&this.event_loop_timer);
     }
 
     this.is_polling = false;
     this.current_interval_ms = this.min_interval_ms;
 
-    log("Stopped polling for missing module: {s}", .{this.missing_path});
+    log("Stopped polling for missing module: {s}", .{this.missing_path});
+    if (this.missing_path.len > 0) {
+        bun.default_allocator.free(this.missing_path);
+        this.missing_path = &.{};
+    }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/bun.js/api/Timer/MissingModulePollTimer.zig around lines 61-74,
stopPolling currently never frees the duplicated missing_path from startPolling;
call the same allocator that was used to dup missing_path to free that buffer
(guarding against empty/null), then reset missing_path to an empty value (e.g.,
null/empty string) to avoid a dangling pointer and prevent future double-free;
perform the free before clearing is_polling and log after cleanup.

/// Schedule the next poll with the current interval
fn scheduleNextPoll(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
this.event_loop_timer.next = bun.timespec.msFromNow(@intCast(this.current_interval_ms));
vm.timer.insert(&this.event_loop_timer);
}

/// Timer callback that checks if the missing file exists
pub fn onTimeout(this: *MissingModulePollTimer, vm: *VirtualMachine) jsc.API.Timer.EventLoopTimer.Arm {
this.event_loop_timer.state = .FIRED;

if (!this.is_polling) {
return .disarm;
}

// Check if the file exists
const file_exists = this.checkFileExists();

if (file_exists) {
log("Missing module found: {s}. Triggering reload.", .{this.missing_path});

// Stop polling
this.is_polling = false;

// Trigger a hot reload by calling reload directly
const HotReloader = jsc.hot_reloader.HotReloader;
var task = HotReloader.Task.initEmpty(undefined);
vm.reload(&task);

return .disarm;
}

// File still doesn't exist, increase interval with exponential backoff
this.current_interval_ms = @min(this.current_interval_ms * 2, this.max_interval_ms);

log("Missing module not found yet: {s}. Next poll in {}ms", .{ this.missing_path, this.current_interval_ms });

// Schedule next poll
this.scheduleNextPoll(vm);

return .disarm;
}
Comment on lines +81 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Free duplicated path when file is found

When the module appears, release missing_path to prevent accumulation across reload cycles.

     if (file_exists) {
         log("Missing module found: {s}. Triggering reload.", .{this.missing_path});
 
         // Stop polling
         this.is_polling = false;
+        if (this.missing_path.len > 0) {
+            bun.default_allocator.free(this.missing_path);
+            this.missing_path = &.{};
+        }
 
         // Trigger a hot reload by calling reload directly
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Timer callback that checks if the missing file exists
pub fn onTimeout(this: *MissingModulePollTimer, vm: *VirtualMachine) jsc.API.Timer.EventLoopTimer.Arm {
this.event_loop_timer.state = .FIRED;
if (!this.is_polling) {
return .disarm;
}
// Check if the file exists
const file_exists = this.checkFileExists();
if (file_exists) {
log("Missing module found: {s}. Triggering reload.", .{this.missing_path});
// Stop polling
this.is_polling = false;
// Trigger a hot reload by calling reload directly
const HotReloader = jsc.hot_reloader.HotReloader;
var task = HotReloader.Task.initEmpty(undefined);
vm.reload(&task);
return .disarm;
}
// File still doesn't exist, increase interval with exponential backoff
this.current_interval_ms = @min(this.current_interval_ms * 2, this.max_interval_ms);
log("Missing module not found yet: {s}. Next poll in {}ms", .{ this.missing_path, this.current_interval_ms });
// Schedule next poll
this.scheduleNextPoll(vm);
return .disarm;
}
/// Timer callback that checks if the missing file exists
pub fn onTimeout(this: *MissingModulePollTimer, vm: *VirtualMachine) jsc.API.Timer.EventLoopTimer.Arm {
this.event_loop_timer.state = .FIRED;
if (!this.is_polling) {
return .disarm;
}
// Check if the file exists
const file_exists = this.checkFileExists();
if (file_exists) {
log("Missing module found: {s}. Triggering reload.", .{this.missing_path});
// Stop polling
this.is_polling = false;
if (this.missing_path.len > 0) {
bun.default_allocator.free(this.missing_path);
this.missing_path = &.{};
}
// Trigger a hot reload by calling reload directly
const HotReloader = jsc.hot_reloader.HotReloader;
var task = HotReloader.Task.initEmpty(undefined);
vm.reload(&task);
return .disarm;
}
// File still doesn't exist, increase interval with exponential backoff
this.current_interval_ms = @min(this.current_interval_ms * 2, this.max_interval_ms);
log("Missing module not found yet: {s}. Next poll in {}ms", .{ this.missing_path, this.current_interval_ms });
// Schedule next poll
this.scheduleNextPoll(vm);
return .disarm;
}
🤖 Prompt for AI Agents
In src/bun.js/api/Timer/MissingModulePollTimer.zig around lines 81 to 115, when
the missing file is found the duplicated path stored in this.missing_path is
never released; free the allocated memory for this.missing_path using the
appropriate allocator/free routine (e.g., call the timer/VM allocator's free
function) before or immediately after setting this.is_polling = false, then set
this.missing_path to null (or an empty/zero value) to avoid double-free on
subsequent reload cycles.


/// Check if the file exists
fn checkFileExists(this: *MissingModulePollTimer) bool {
if (this.missing_path.len == 0) return false;

// Use stat to check if the file exists
const stat_result = std.fs.cwd().statFile(this.missing_path) catch return false;
return stat_result.kind == .file;
}

/// Cleanup timer resources
pub fn deinit(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
if (this.event_loop_timer.state == .ACTIVE) {
vm.timer.remove(&this.event_loop_timer);
}

if (this.missing_path.len > 0) {
bun.default_allocator.free(this.missing_path);
this.missing_path = &.{};
}

this.is_polling = false;
}

const log = bun.Output.scoped(.MissingModulePollTimer, .hidden);

const bun = @import("bun");
const std = @import("std");

const jsc = bun.jsc;
const VirtualMachine = jsc.VirtualMachine;
50 changes: 50 additions & 0 deletions test/cli/watch/watch-missing-external.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from "bun:test";
import { writeFileSync } from "fs";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";

test("watch mode should poll for missing module and reload when it appears", async () => {
using dir = tempDir("watch-missing-external", {
"entry.js": `
console.log("Starting...");
require("./missing.js");
console.log("This should not print on first run");
`,
});

const proc = Bun.spawn({
cmd: [bunExe(), "--watch", "entry.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

// Wait a bit for the process to start and hit the error
await new Promise(resolve => setTimeout(resolve, 500));

// Now write the missing file from outside the process
writeFileSync(join(String(dir), "missing.js"), "console.log('Module loaded!'); process.exit(0);");

const timeout = setTimeout(() => {
proc.kill();
}, 5000);

try {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

clearTimeout(timeout);

console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);

Comment on lines +38 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove debug logging in tests

Drop console logs to reduce CI noise; keep assertions only.

🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-external.test.ts around lines 38 to 41, remove
the three console.log debug statements (console.log("STDOUT:", stdout);
console.log("STDERR:", stderr); console.log("Exit code:", exitCode);) so the
test does not emit noisy output in CI; leave the existing assertions intact and
only assert against stdout/stderr/exitCode as needed.

// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
expect(stdout).toContain("Module loaded!");
} catch (err) {
clearTimeout(timeout);
proc.kill();
throw err;
}
}, 10000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid explicit test timeout

Let the runner manage timeouts; remove the 10000 override.

-}, 10000);
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}, 10000);
});
🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-external.test.ts around line 50, the test has an
explicit timeout override "}, 10000);" which should be removed so the test
runner manages timeouts; delete the trailing numeric timeout argument from the
test closure (remove ", 10000") leaving the test definition without an explicit
timeout.

56 changes: 56 additions & 0 deletions test/cli/watch/watch-missing-module-simple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";

test("watch mode should detect missing module error", async () => {
using dir = tempDir("watch-missing-simple", {
"entry.js": `
console.log("Starting...");
try {
require("./missing.js");
} catch (err) {
console.log("ERROR:", err.code);
console.log("Caught error for missing module");
const fs = require("fs");
const path = require("path");
// Write the missing file after a short delay
setTimeout(() => {
console.log("Writing missing.js");
fs.writeFileSync(path.join(__dirname, "missing.js"), "console.log('Module loaded!'); process.exit(0);");
}, 100);
// Re-throw so watch mode sees it
throw err;
}
`,
});

const proc = Bun.spawn({
cmd: [bunExe(), "--watch", "entry.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const timeout = setTimeout(() => {
proc.kill();
}, 5000);

try {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

clearTimeout(timeout);

console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);
Comment on lines +45 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove debug logging in tests

These prints are unnecessary for assertions and bloat output.

🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-module-simple.test.ts around lines 45 to 47, the
three console.log debug statements (printing STDOUT, STDERR, and Exit code)
should be removed because they are unnecessary for assertions and clutter test
output; delete these console.log calls and rely on existing assertions or add
focused assertions if visibility into outputs is needed.


// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
Comment on lines +36 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating the fallback timeout behavior.

The test captures stdout/stderr but only logs them without assertions. While the current assertion on exitCode is sufficient for the happy path, consider:

  1. Adding assertions on stdout/stderr to validate that the error was caught and the polling message appeared.
  2. Testing the fallback case where the file is never created (though this would require a separate test with a shorter timeout).

Example:

     const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

     clearTimeout(timeout);

-    console.log("STDOUT:", stdout);
-    console.log("STDERR:", stderr);
-    console.log("Exit code:", exitCode);
+    // Validate that watch mode detected the error and started polling
+    expect(stderr).toContain("Waiting for module to exist:");

     // The test passes if the process exits with code 0
     expect(exitCode).toBe(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const timeout = setTimeout(() => {
proc.kill();
}, 5000);
try {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
clearTimeout(timeout);
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);
// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
const timeout = setTimeout(() => {
proc.kill();
}, 5000);
try {
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
clearTimeout(timeout);
// Validate that watch mode detected the error and started polling
expect(stderr).toContain("Waiting for module to exist:");
// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-module-simple.test.ts around lines 36 to 50, the
test only logs stdout/stderr and asserts exitCode; add assertions that stdout
and/or stderr contain the expected polling/fallback messages (e.g., the error
caught and the "waiting" or "polling" text) to validate normal fallback
behavior, and add a separate short-timeout test case that simulates the file
never being created to assert the fallback timeout path (assert that the polling
message repeats or that a specific timeout message appears) — implement these
assertions and the additional short timeout test to ensure both happy and
fallback paths are validated.

} catch (err) {
clearTimeout(timeout);
proc.kill();
throw err;
}
}, 10000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid explicit test timeout

Rely on default runner timeout.

-}, 10000);
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}, 10000);
});
🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-module-simple.test.ts around line 56 there is an
explicit test timeout passed (", 10000"); remove the explicit timeout argument
from the test call so the test relies on the test runner's default timeout
(i.e., delete the ", 10000" token and keep the test function invocation
unchanged otherwise) and ensure no other hard-coded timeouts remain in this
file.

Loading