Merge branch 'main' into css-improvements

This commit is contained in:
Adrià Arrufat
2026-03-16 10:25:35 +09:00
21 changed files with 249 additions and 81 deletions

View File

@@ -295,7 +295,7 @@ pub const Client = struct {
}
var cdp = &self.mode.cdp;
var last_message = timestamp(.monotonic);
var last_message = milliTimestamp(.monotonic);
var ms_remaining = self.ws.timeout_ms;
while (true) {
@@ -304,7 +304,7 @@ pub const Client = struct {
if (self.readSocket() == false) {
return;
}
last_message = timestamp(.monotonic);
last_message = milliTimestamp(.monotonic);
ms_remaining = self.ws.timeout_ms;
},
.no_page => {
@@ -319,16 +319,18 @@ pub const Client = struct {
if (self.readSocket() == false) {
return;
}
last_message = timestamp(.monotonic);
last_message = milliTimestamp(.monotonic);
ms_remaining = self.ws.timeout_ms;
},
.done => {
const elapsed = timestamp(.monotonic) - last_message;
if (elapsed > ms_remaining) {
const now = milliTimestamp(.monotonic);
const elapsed = now - last_message;
if (elapsed >= ms_remaining) {
log.info(.app, "CDP timeout", .{});
return;
}
ms_remaining -= @intCast(elapsed);
last_message = now;
},
}
}
@@ -501,6 +503,7 @@ fn buildJSONVersionResponse(
}
pub const timestamp = @import("datetime.zig").timestamp;
pub const milliTimestamp = @import("datetime.zig").milliTimestamp;
const testing = std.testing;
test "server: buildJSONVersionResponse" {

View File

@@ -1091,7 +1091,6 @@ pub fn iframeAddedCallback(self: *Page, iframe: *IFrame) !void {
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
self._pending_loads -= 1;
iframe._window = null;
page_frame.deinit(true);
return error.IFrameLoadError;
};

View File

@@ -548,7 +548,9 @@ fn processQueuedNavigation(self: *Session) !void {
continue;
}
try self.processFrameNavigation(page, qn);
self.processFrameNavigation(page, qn) catch |err| {
log.warn(.page, "frame navigation", .{ .url = qn.url, .err = err });
};
}
// Clear the queue after first pass
@@ -588,7 +590,8 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v
errdefer iframe._window = null;
if (page._parent_notified) {
const parent_notified = page._parent_notified;
if (parent_notified) {
// we already notified the parent that we had loaded
parent._pending_loads += 1;
}
@@ -598,7 +601,19 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v
page.* = undefined;
try Page.init(page, frame_id, self, parent);
errdefer page.deinit(true);
errdefer {
for (parent.frames.items, 0..) |frame, i| {
if (frame == page) {
parent.frames_sorted = false;
_ = parent.frames.swapRemove(i);
break;
}
}
if (parent_notified) {
parent._pending_loads -= 1;
}
page.deinit(true);
}
page.iframe = iframe;
iframe._window = page.window;

View File

@@ -197,18 +197,20 @@ pub fn trackTemp(self: *Context, global: v8.Global) !void {
}
pub fn weakRef(self: *Context, obj: anytype) void {
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn safeWeakRef(self: *Context, obj: anytype) void {
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
@@ -216,11 +218,12 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void {
return;
};
v8.v8__Global__ClearWeak(&fc.global);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn strongRef(self: *Context, obj: anytype) void {
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);

View File

@@ -63,6 +63,20 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
};
}
const RejectError = union(enum) {
generic: []const u8,
type_error: []const u8,
};
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
const handle = switch (err) {
.type_error => |str| self.local.isolate.createTypeError(str),
.generic => |str| self.local.isolate.createError(str),
};
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
};
}
fn _reject(self: PromiseResolver, value: anytype) !void {
const local = self.local;
const js_val = try local.zigValueToJs(value, .{});

View File

@@ -11,9 +11,9 @@
}
{
// Empty XML is a parse error (no root element)
const parser = new DOMParser();
testing.expectError('Error', () => parser.parseFromString('', 'text/xml'));
let d = parser.parseFromString('', 'text/xml');
testing.expectEqual('<parsererror>error</parsererror>', new XMLSerializer().serializeToString(d));
}
}
</script>

View File

@@ -1,15 +1,15 @@
<!DOCTYPE html>
<body onload="loaded()"></body>
<body onload="loadEvent = event"></body>
<script src="../testing.js"></script>
<script id=bodyOnLoad2>
let called = 0;
function loaded(e) {
called += 1;
}
// Per spec, the handler is compiled as: function(event) { loadEvent = event }
// Verify: handler fires, "event" parameter is a proper Event, and handler is a function.
let loadEvent = null;
testing.eventually(() => {
testing.expectEqual(1, called);
testing.expectEqual("function", typeof document.body.onload);
testing.expectTrue(loadEvent instanceof Event);
testing.expectEqual("load", loadEvent.type);
});
</script>

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<body onload="called++"></body>
<script src="../testing.js"></script>
<script id=bodyOnLoad3>
// Per spec, the handler is compiled as: function(event) { called++ }
// Verify: handler fires exactly once, and body.onload reflects to window.onload.
let called = 0;
testing.eventually(() => {
// The attribute handler should have fired exactly once.
testing.expectEqual(1, called);
// body.onload is a Window-reflecting handler per spec.
testing.expectEqual("function", typeof document.body.onload);
testing.expectEqual(document.body.onload, window.onload);
// Setting body.onload via property replaces the attribute handler.
let propertyCalled = false;
document.body.onload = function() { propertyCalled = true; };
testing.expectEqual(document.body.onload, window.onload);
// Setting onload to null removes the handler.
document.body.onload = null;
testing.expectEqual(null, document.body.onload);
testing.expectEqual(null, window.onload);
});
</script>

View File

@@ -86,15 +86,15 @@ pub fn parseFromString(
var parser = Parser.init(arena, doc_node, page);
parser.parseXML(html);
if (parser.err) |pe| {
return pe.err;
if (parser.err != null or doc_node.firstChild() == null) {
// Return a document with a <parsererror> element per spec.
const err_doc = try page._factory.document(XMLDocument{ ._proto = undefined });
var err_parser = Parser.init(arena, err_doc.asNode(), page);
err_parser.parseXML("<parsererror xmlns=\"http://www.mozilla.org/newlayout/xml/parsererror.xml\">error</parsererror>");
return err_doc.asDocument();
}
const first_child = doc_node.firstChild() orelse {
// Empty XML or no root element - this is a parse error.
// TODO: Return a document with a <parsererror> element per spec.
return error.JsException;
};
const first_child = doc_node.firstChild().?;
// If first node is a `ProcessingInstruction`, skip it.
if (first_child.getNodeType() == 7) {

View File

@@ -192,7 +192,8 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
self._page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(self._resolver).reject("fetch error", @errorName(err));
// fetch() must reject with a TypeError on network errors per spec
ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = @errorName(err) });
}
fn httpShutdownCallback(ctx: *anyopaque) void {

View File

@@ -228,6 +228,13 @@ pub const Writer = struct {
try w.objectField("value");
switch (value) {
.integer => |v| {
// CDP spec requires integer values to be serialized as strings.
// 20 bytes is enough for the decimal representation of a 64-bit integer.
var buf: [20]u8 = undefined;
const s = try std.fmt.bufPrint(&buf, "{d}", .{v});
try w.write(s);
},
inline else => |v| try w.write(v),
}
@@ -1212,4 +1219,25 @@ test "AXNode: writer" {
// Check childIds array exists
const child_ids = doc_node.get("childIds").?.array.items;
try testing.expect(child_ids.len > 0);
// Find the h1 node and verify its level property is serialized as a string
for (nodes) |node_val| {
const obj = node_val.object;
const role_obj = obj.get("role") orelse continue;
const role_val = role_obj.object.get("value") orelse continue;
if (!std.mem.eql(u8, role_val.string, "heading")) continue;
const props = obj.get("properties").?.array.items;
for (props) |prop| {
const prop_obj = prop.object;
const name_str = prop_obj.get("name").?.string;
if (!std.mem.eql(u8, name_str, "level")) continue;
const level_value = prop_obj.get("value").?.object;
try testing.expectEqual("integer", level_value.get("type").?.string);
// CDP spec: integer values must be serialized as strings
try testing.expectEqual("1", level_value.get("value").?.string);
return;
}
}
return error.HeadingNodeNotFound;
}

View File

@@ -168,13 +168,11 @@ pub fn CDPT(comptime TypeProvider: type) type {
if (is_startup) {
dispatchStartupCommand(&command, input.method) catch |err| {
command.sendError(-31999, @errorName(err), .{}) catch {};
return err;
command.sendError(-31999, @errorName(err), .{}) catch return err;
};
} else {
dispatchCommand(&command, input.method) catch |err| {
command.sendError(-31998, @errorName(err), .{}) catch {};
return err;
command.sendError(-31998, @errorName(err), .{}) catch return err;
};
}
}
@@ -924,18 +922,20 @@ test "cdp: invalid json" {
// method is required
try testing.expectError(error.InvalidJSON, ctx.processMessage(.{}));
try testing.expectError(error.InvalidMethod, ctx.processMessage(.{
try ctx.processMessage(.{
.method = "Target",
}));
});
try ctx.expectSentError(-31998, "InvalidMethod", .{});
try testing.expectError(error.UnknownDomain, ctx.processMessage(.{
try ctx.processMessage(.{
.method = "Unknown.domain",
}));
});
try ctx.expectSentError(-31998, "UnknownDomain", .{});
try testing.expectError(error.UnknownMethod, ctx.processMessage(.{
try ctx.processMessage(.{
.method = "Target.over9000",
}));
});
try ctx.expectSentError(-31998, "UnknownMethod", .{});
}
test "cdp: invalid sessionId" {

View File

@@ -550,11 +550,12 @@ test "cdp.dom: getSearchResults unknown search id" {
var ctx = testing.context();
defer ctx.deinit();
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{
try ctx.processMessage(.{
.id = 8,
.method = "DOM.getSearchResults",
.params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 },
}));
});
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 8 });
}
test "cdp.dom: search flow" {
@@ -604,11 +605,12 @@ test "cdp.dom: search flow" {
try ctx.expectSentResult(null, .{ .id = 16 });
// make sure the delete actually did something
try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{
try ctx.processMessage(.{
.id = 17,
.method = "DOM.getSearchResults",
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
}));
});
try ctx.expectSentError(-31998, "SearchResultNotFound", .{ .id = 17 });
}
test "cdp.dom: querySelector unknown search id" {
@@ -645,11 +647,12 @@ test "cdp.dom: querySelector Node not found" {
});
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 });
try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{
try ctx.processMessage(.{
.id = 4,
.method = "DOM.querySelector",
.params = .{ .nodeId = 1, .selector = "a" },
}));
});
try ctx.expectSentError(-31998, "NodeNotFoundForGivenId", .{ .id = 4 });
try ctx.processMessage(.{
.id = 5,

View File

@@ -19,6 +19,7 @@
const std = @import("std");
const lp = @import("lightpanda");
const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
const CdpStorage = @import("storage.zig");
@@ -117,7 +118,12 @@ fn deleteCookies(cmd: anytype) !void {
path: ?[]const u8 = null,
partitionKey: ?CdpStorage.CookiePartitionKey = null,
})) orelse return error.InvalidParams;
if (params.partitionKey != null) return error.NotImplemented;
// Silently ignore partitionKey since we don't support partitioned cookies (CHIPS).
// This allows Puppeteer's page.setCookie() to work, which sends deleteCookies
// with partitionKey as part of its cookie-setting workflow.
if (params.partitionKey != null) {
log.warn(.not_implemented, "partition key", .{ .src = "deleteCookies" });
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const cookies = &bc.session.cookie_jar.cookies;

View File

@@ -633,7 +633,7 @@ test "cdp.page: getFrameTree" {
defer ctx.deinit();
{
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } }));
try ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } });
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
}

View File

@@ -128,7 +128,14 @@ pub const CdpCookie = struct {
};
pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
// Silently ignore partitionKey since we don't support partitioned cookies (CHIPS).
// This allows Puppeteer's page.setCookie() to work, which may send cookies with
// partitionKey as part of its cookie-setting workflow.
if (param.partitionKey != null) {
log.warn(.not_implemented, "partition key", .{ .src = "setCdpCookie" });
}
// Still reject unsupported features
if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null) {
return error.NotImplemented;
}

View File

@@ -556,7 +556,7 @@ test "cdp.target: disposeBrowserContext" {
defer ctx.deinit();
{
try testing.expectError(error.InvalidParams, ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" }));
try ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" });
try ctx.expectSentError(-31998, "InvalidParams", .{ .id = 7 });
}
@@ -609,7 +609,7 @@ test "cdp.target: createTarget" {
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
try testing.expectError(error.UnknownBrowserContextId, ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } });
try ctx.expectSentError(-31998, "UnknownBrowserContextId", .{ .id = 10 });
}
@@ -626,13 +626,13 @@ test "cdp.target: closeTarget" {
defer ctx.deinit();
{
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } });
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
}
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } });
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
}
@@ -640,7 +640,7 @@ test "cdp.target: closeTarget" {
_ = try bc.session.createPage();
bc.target_id = "TID-000000000A".*;
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } });
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
}
@@ -657,13 +657,13 @@ test "cdp.target: attachToTarget" {
defer ctx.deinit();
{
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } });
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
}
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } });
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
}
@@ -671,7 +671,7 @@ test "cdp.target: attachToTarget" {
_ = try bc.session.createPage();
bc.target_id = "TID-000000000B".*;
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } });
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
}
@@ -701,13 +701,13 @@ test "cdp.target: getTargetInfo" {
}
{
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "X" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "X" } });
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
}
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } });
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
}
@@ -715,7 +715,7 @@ test "cdp.target: getTargetInfo" {
_ = try bc.session.createPage();
bc.target_id = "TID-000000000C".*;
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } });
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
}

View File

@@ -116,3 +116,24 @@ test "MCP.Server - Integration: synchronous smoke test" {
try testing.expectJson(.{ .id = 1 }, out_alloc.writer.buffered());
}
test "MCP.Server - Integration: ping request returns an empty result" {
defer testing.reset();
const allocator = testing.allocator;
const app = testing.test_app;
const input =
\\{"jsonrpc":"2.0","id":"ping-1","method":"ping"}
;
var in_reader: std.io.Reader = .fixed(input);
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);
defer out_alloc.deinit();
var server = try Self.init(allocator, app, &out_alloc.writer);
defer server.deinit();
try router.processRequests(server, &in_reader);
try testing.expectJson(.{ .id = "ping-1", .result = .{} }, out_alloc.writer.buffered());
}

View File

@@ -238,6 +238,27 @@ test "MCP.protocol - request parsing" {
try testing.expectString("1.0.0", init_params.value.clientInfo.version);
}
test "MCP.protocol - ping request parsing" {
defer testing.reset();
const raw_json =
\\{
\\ "jsonrpc": "2.0",
\\ "id": "123",
\\ "method": "ping"
\\}
;
const parsed = try std.json.parseFromSlice(Request, testing.arena_allocator, raw_json, .{ .ignore_unknown_fields = true });
defer parsed.deinit();
const req = parsed.value;
try testing.expectString("2.0", req.jsonrpc);
try testing.expectString("ping", req.method);
try testing.expect(req.id.? == .string);
try testing.expectString("123", req.id.?.string);
try testing.expectEqual(null, req.params);
}
test "MCP.protocol - response formatting" {
defer testing.reset();
const response = Response{

View File

@@ -34,6 +34,7 @@ const log = @import("../log.zig");
const Method = enum {
initialize,
ping,
@"notifications/initialized",
@"tools/list",
@"tools/call",
@@ -43,6 +44,7 @@ const Method = enum {
const method_map = std.StaticStringMap(Method).initComptime(.{
.{ "initialize", .initialize },
.{ "ping", .ping },
.{ "notifications/initialized", .@"notifications/initialized" },
.{ "tools/list", .@"tools/list" },
.{ "tools/call", .@"tools/call" },
@@ -68,6 +70,7 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8)
switch (method) {
.initialize => try handleInitialize(server, req),
.ping => try handlePing(server, req),
.@"notifications/initialized" => {},
.@"tools/list" => try tools.handleList(server, arena, req),
.@"tools/call" => try tools.handleCall(server, arena, req),
@@ -92,6 +95,11 @@ fn handleInitialize(server: *Server, req: protocol.Request) !void {
try server.sendResult(req.id.?, result);
}
fn handlePing(server: *Server, req: protocol.Request) !void {
const id = req.id orelse return;
try server.sendResult(id, .{});
}
const testing = @import("../testing.zig");
test "MCP.router - handleMessage - synchronous unit tests" {
@@ -116,22 +124,29 @@ test "MCP.router - handleMessage - synchronous unit tests" {
, out_alloc.writer.buffered());
out_alloc.writer.end = 0;
// 2. Tools list
// 2. Ping
try handleMessage(server, aa,
\\{"jsonrpc":"2.0","id":2,"method":"tools/list"}
\\{"jsonrpc":"2.0","id":2,"method":"ping"}
);
try testing.expectJson(.{ .id = 2 }, out_alloc.writer.buffered());
try testing.expectJson(.{ .id = 2, .result = .{} }, out_alloc.writer.buffered());
out_alloc.writer.end = 0;
// 3. Tools list
try handleMessage(server, aa,
\\{"jsonrpc":"2.0","id":3,"method":"tools/list"}
);
try testing.expectJson(.{ .id = 3 }, out_alloc.writer.buffered());
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"name\":\"goto\"") != null);
out_alloc.writer.end = 0;
// 3. Method not found
// 4. Method not found
try handleMessage(server, aa,
\\{"jsonrpc":"2.0","id":3,"method":"unknown_method"}
\\{"jsonrpc":"2.0","id":4,"method":"unknown_method"}
);
try testing.expectJson(.{ .id = 3, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered());
try testing.expectJson(.{ .id = 4, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered());
out_alloc.writer.end = 0;
// 4. Parse error
// 5. Parse error
{
const filter: testing.LogFilter = .init(.mcp);
defer filter.deinit();