Skip to content

Commit 252a9f1

Browse files
justrachclaude
andcommitted
test: add unit tests for timeout + POLLHUP detection (#148)
- Verify idle_timeout_ms is 10 minutes (600,000ms) - Verify POLLHUP detected when pipe write end is closed (client gone) - Verify open pipe does NOT trigger false HUP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8754cf1 commit 252a9f1

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

src/tests.zig

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4570,3 +4570,89 @@ test "issue-116: getGitHead returns valid SHA for git repos" {
45704570
}
45714571
}
45724572
}
4573+
4574+
test "issue-148: idle timeout is 10 minutes" {
4575+
const mcp = @import("mcp.zig");
4576+
try testing.expectEqual(@as(i64, 10 * 60 * 1000), mcp.idle_timeout_ms);
4577+
}
4578+
4579+
test "issue-148: POLLHUP detects closed pipe" {
4580+
const pipe = try std.posix.pipe();
4581+
std.posix.close(pipe[1]);
4582+
4583+
var poll_fds = [_]std.posix.pollfd{.{
4584+
.fd = pipe[0],
4585+
.events = std.posix.POLL.IN | std.posix.POLL.HUP,
4586+
.revents = 0,
4587+
}};
4588+
4589+
const result = try std.posix.poll(&poll_fds, 0);
4590+
try testing.expect(result > 0);
4591+
try testing.expect((poll_fds[0].revents & std.posix.POLL.HUP) != 0);
4592+
std.posix.close(pipe[0]);
4593+
}
4594+
4595+
test "issue-148: open pipe does not trigger HUP" {
4596+
const pipe = try std.posix.pipe();
4597+
defer std.posix.close(pipe[0]);
4598+
defer std.posix.close(pipe[1]);
4599+
4600+
var poll_fds = [_]std.posix.pollfd{.{
4601+
.fd = pipe[0],
4602+
.events = std.posix.POLL.IN | std.posix.POLL.HUP,
4603+
.revents = 0,
4604+
}};
4605+
4606+
const result = try std.posix.poll(&poll_fds, 0);
4607+
try testing.expectEqual(@as(usize, 0), result);
4608+
}
4609+
4610+
test "issue-148: codedb mcp exits when stdin is closed" {
4611+
// Integration test: spawn codedb mcp, close stdin, verify it exits
4612+
var child = std.process.Child.init(
4613+
&.{ "zig", "build", "run", "--", "--mcp" },
4614+
testing.allocator,
4615+
);
4616+
child.stdin_behavior = .Pipe;
4617+
child.stdout_behavior = .Pipe;
4618+
child.stderr_behavior = .Ignore;
4619+
4620+
try child.spawn();
4621+
4622+
// Send initialize then close stdin (simulate client crash)
4623+
const init_msg = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1\"}}}";
4624+
const header = std.fmt.comptimePrint("Content-Length: {d}\r\n\r\n", .{init_msg.len});
4625+
4626+
if (child.stdin) |stdin| {
4627+
stdin.writeAll(header) catch {};
4628+
stdin.writeAll(init_msg) catch {};
4629+
// Close stdin — simulates client disconnecting
4630+
stdin.close();
4631+
child.stdin = null;
4632+
}
4633+
4634+
// Wait up to 15 seconds for the process to exit
4635+
// (watchdog polls every 10s, so it should detect POLLHUP within ~10s)
4636+
const start = std.time.milliTimestamp();
4637+
const term = child.wait() catch {
4638+
// If wait fails, the process is stuck — test fails
4639+
try testing.expect(false);
4640+
return;
4641+
};
4642+
4643+
const elapsed = std.time.milliTimestamp() - start;
4644+
4645+
// Should have exited (not been killed by us)
4646+
switch (term) {
4647+
.Exited => |code| {
4648+
// Any exit code is fine — we just care that it exited
4649+
_ = code;
4650+
},
4651+
else => {
4652+
// Signal-killed or other — acceptable
4653+
},
4654+
}
4655+
4656+
// Should exit within 15 seconds (10s poll interval + margin)
4657+
try testing.expect(elapsed < 15_000);
4658+
}

0 commit comments

Comments
 (0)