summaryrefslogtreecommitdiffstats
path: root/tests/test_mcp.cpp
diff options
context:
space:
mode:
authorEthan Werner <mail@ethanwerner.com>2026-10-10 20:55:33 +0000
committerEthan Werner <mail@ethanwerner.com>2026-10-10 20:56:50 +0000
commitf8423ebea413605b42de23f5dd06c5479d3fa1dd (patch)
tree2612a5eb026c59e7aae01f10a1cf62f2ee43042c /tests/test_mcp.cpp
initial commitHEADmaster
Diffstat (limited to 'tests/test_mcp.cpp')
-rw-r--r--tests/test_mcp.cpp579
1 files changed, 579 insertions, 0 deletions
diff --git a/tests/test_mcp.cpp b/tests/test_mcp.cpp
new file mode 100644
index 0000000..ae112ba
--- /dev/null
+++ b/tests/test_mcp.cpp
@@ -0,0 +1,579 @@
+#include "mcp.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <cjson/cJSON.h>
+
+static int failures = 0;
+static int checks = 0;
+
+#define CHECK(cond, msg) \
+ do { \
+ checks++; \
+ if (!(cond)) { \
+ failures++; \
+ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \
+ } \
+ } while (0)
+
+#define CHECK_EQ(a, b, msg) \
+ do { \
+ checks++; \
+ if (strcmp((a) ? (a) : "(null)", (b)) != 0) { \
+ failures++; \
+ fprintf(stderr, "FAIL %s:%d: %s (got `%s`, want `%s`)\n", \
+ __FILE__, __LINE__, (msg), (a) ? (a) : "(null)", (b)); \
+ } \
+ } while (0)
+
+#define CHECK_CONT(hay, needle, msg) \
+ do { \
+ checks++; \
+ if (!hay || !strstr(hay, needle)) { \
+ failures++; \
+ fprintf(stderr, \
+ "FAIL %s:%d: %s (got `%s`, want substring `%s`)\n", \
+ __FILE__, __LINE__, (msg), hay ? hay : "(null)", \
+ (needle)); \
+ } \
+ } while (0)
+
+static void mem_free(mcp_memory *m)
+{
+ sl_free(&m->responses);
+ sl_free(&m->sent);
+}
+
+/* helpers: push a queued JSON-RPC response / read the nth sent message */
+
+static void push_resp(mcp_memory *m, unsigned long id, const char *result)
+{
+ char *line = xstrfmt("{\"jsonrpc\":\"2.0\",\"id\":%lu,\"result\":%s}", id,
+ result);
+ sl_push(&m->responses, line);
+}
+
+static cJSON *sent_msg(mcp_memory *m, size_t i)
+{
+ if (i >= m->sent.len)
+ return NULL;
+ return cJSON_Parse(m->sent.items[i]);
+}
+
+static void test_parses_stdio_config(void)
+{
+ mcp_server_config cfg;
+ CHECK(mcp_server_config_from_json(
+ "{\"command\":\"node\",\"args\":[\"server.js\",\"--fast\"],"
+ "\"env\":{\"API_KEY\":\"k\"}}",
+ &cfg) == 1,
+ "stdio config parses");
+ CHECK_EQ(cfg.command, "node", "command");
+ CHECK(cfg.arg_count == 2, "args count");
+ CHECK(cfg.arg_count == 2 && strcmp(cfg.args[0], "server.js") == 0,
+ "arg 0");
+ CHECK(cfg.arg_count == 2 && strcmp(cfg.args[1], "--fast") == 0, "arg 1");
+ CHECK(cfg.env_count == 1 && strcmp(cfg.env[0].key, "API_KEY") == 0,
+ "env key");
+ CHECK(cfg.env_count == 1 && strcmp(cfg.env[0].val, "k") == 0, "env value");
+ CHECK(cfg.url == NULL, "no url");
+ mcp_server_config_free(&cfg);
+}
+
+static void test_parses_http_config(void)
+{
+ mcp_server_config cfg;
+ CHECK(mcp_server_config_from_json(
+ "{\"url\":\"https://example.com/mcp\","
+ "\"headers\":{\"Authorization\":\"Bearer t\"}}",
+ &cfg) == 1,
+ "http config parses");
+ CHECK_EQ(cfg.url, "https://example.com/mcp", "url");
+ CHECK(cfg.header_count == 1 &&
+ strcmp(cfg.headers[0].key, "Authorization") == 0,
+ "header key");
+ CHECK(cfg.header_count == 1 &&
+ strcmp(cfg.headers[0].val, "Bearer t") == 0,
+ "header value");
+ CHECK(cfg.command == NULL, "no command");
+ mcp_server_config_free(&cfg);
+}
+
+static void test_rejects_no_transport(void)
+{
+ mcp_server_config cfg;
+ CHECK(mcp_server_config_from_json("{\"foo\":\"bar\"}", &cfg) == 0,
+ "object without transport");
+ mcp_server_config_free(&cfg);
+ CHECK(mcp_server_config_from_json("42", &cfg) == 0, "non-object");
+ mcp_server_config_free(&cfg);
+ CHECK(mcp_server_config_from_json(NULL, &cfg) == 0, "null");
+ mcp_server_config_free(&cfg);
+}
+
+static void test_handshake_discovery_call(void)
+{
+ mcp_memory mem;
+ mcp_memory_init(&mem);
+ push_resp(&mem, 1,
+ "{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"serverInfo\":{\"name\":\"fake\",\"version\":\"0\"}}");
+ push_resp(&mem, 2,
+ "{\"tools\":[{\"name\":\"add\",\"description\":\"Add two "
+ "integers\",\"inputSchema\":{\"type\":\"object\",\"properties\":"
+ "{\"a\":{\"type\":\"integer\"},\"b\":{\"type\":\"integer\"}},"
+ "\"required\":[\"a\",\"b\"]}}]}");
+ push_resp(&mem, 3, "{\"content\":[{\"type\":\"text\",\"text\":\"7\"}]}");
+
+ char *err = NULL;
+ mcp_client *c = mcp_connect_memory(&mem, &err);
+ CHECK(c != NULL, "connect memory");
+
+ CHECK(mcp_initialize(c, &err) == 1, "handshake");
+ CHECK(err == NULL, "no handshake error");
+
+ mcp_tool *tools = NULL;
+ size_t n = 0;
+ err = NULL;
+ CHECK(mcp_list_tools(c, &tools, &n, &err) == 1, "tools/list");
+ CHECK(n == 1, "one tool");
+ if (n == 1) {
+ CHECK_EQ(tools[0].name, "add", "tool name");
+ CHECK_EQ(tools[0].description, "Add two integers", "tool description");
+ CHECK_CONT(tools[0].input_schema, "\"type\":\"object\"", "schema type");
+ CHECK_CONT(tools[0].input_schema, "\"required\"", "schema required");
+ }
+
+ err = NULL;
+ char *out = mcp_call_tool(c, "add", "{\"a\":3,\"b\":4}", &err);
+ CHECK(out != NULL, "tools/call");
+ CHECK_EQ(out, "7", "call result");
+ free(out);
+
+ /* wire order and ids must be exactly the MCP handshake */
+ CHECK(mem.sent.len == 4, "four messages sent");
+ const char *want[] = {"initialize", "notifications/initialized",
+ "tools/list", "tools/call"};
+ for (size_t i = 0; i < 4 && i < mem.sent.len; i++) {
+ cJSON *m = sent_msg(&mem, i);
+ cJSON *method = m ? cJSON_GetObjectItemCaseSensitive(m, "method") : NULL;
+ CHECK(cJSON_IsString(method) && strcmp(method->valuestring, want[i]) == 0,
+ "method order");
+ cJSON_Delete(m);
+ }
+ {
+ cJSON *m0 = sent_msg(&mem, 0);
+ cJSON *m2 = sent_msg(&mem, 2);
+ cJSON *m3 = sent_msg(&mem, 3);
+ CHECK(m0 && (unsigned long long)cJSON_GetObjectItemCaseSensitive(m0, "id")->valuedouble == 1,
+ "initialize id 1");
+ CHECK(m2 && (unsigned long long)cJSON_GetObjectItemCaseSensitive(m2, "id")->valuedouble == 2,
+ "tools/list id 2");
+ CHECK(m3 && (unsigned long long)cJSON_GetObjectItemCaseSensitive(m3, "id")->valuedouble == 3,
+ "tools/call id 3");
+ if (m3) {
+ cJSON *params = cJSON_GetObjectItemCaseSensitive(m3, "params");
+ cJSON *name = params ? cJSON_GetObjectItemCaseSensitive(params, "name") : NULL;
+ CHECK(cJSON_IsString(name) && strcmp(name->valuestring, "add") == 0,
+ "call params name");
+ cJSON *args = params ? cJSON_GetObjectItemCaseSensitive(params, "arguments") : NULL;
+ CHECK(args && (long long)cJSON_GetObjectItemCaseSensitive(args, "a")->valuedouble == 3,
+ "call params a=3");
+ }
+ cJSON_Delete(m0);
+ cJSON_Delete(m2);
+ cJSON_Delete(m3);
+ }
+ mcp_tools_free(tools, n);
+ mcp_client_free(c);
+ mem_free(&mem);
+}
+
+static void test_server_error_is_surfaced(void)
+{
+ mcp_memory mem;
+ mcp_memory_init(&mem);
+ sl_push(&mem.responses,
+ xstrdup("{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32601,"
+ "\"message\":\"Method not found\"}}"));
+ char *err = NULL;
+ mcp_client *c = mcp_connect_memory(&mem, &err);
+ err = NULL;
+ char *res = mcp_request(c, "initialize", "{}", &err);
+ CHECK(res == NULL, "request failed");
+ CHECK(err && strstr(err, "server error -32601") != NULL, "error code");
+ CHECK(err && strstr(err, "Method not found") != NULL, "error message");
+ free(err);
+ mcp_client_free(c);
+ mem_free(&mem);
+}
+
+static void test_notification_lines_are_skipped(void)
+{
+ mcp_memory mem;
+ mcp_memory_init(&mem);
+ sl_push(&mem.responses, xstrdup(
+ "{\"jsonrpc\":\"2.0\",\"method\":"
+ "\"notifications/message\",\"params\":{}}"));
+ push_resp(&mem, 1, "{\"ok\":true}");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_memory(&mem, &err);
+ err = NULL;
+ char *res = mcp_request(c, "ping", "{}", &err);
+ CHECK(res != NULL, "response past the notification");
+ CHECK_CONT(res, "\"ok\":true", "result ok");
+ free(res);
+ mcp_client_free(c);
+ mem_free(&mem);
+}
+
+static void test_tool_invoke_roundtrip(void)
+{
+ mcp_memory mem;
+ mcp_memory_init(&mem);
+ push_resp(&mem, 1,
+ "{\"tools\":[{\"name\":\"echo\",\"description\":\"echo\","
+ "\"inputSchema\":{\"type\":\"object\"}}]}");
+ push_resp(&mem, 2, "{\"content\":[{\"type\":\"text\",\"text\":\"pong\"}]}");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_memory(&mem, &err);
+
+ mcp_tool *tools = NULL;
+ size_t n = 0;
+ err = NULL;
+ CHECK(mcp_list_tools(c, &tools, &n, &err) == 1, "tools");
+ CHECK(n == 1, "one tool");
+ if (n == 1) {
+ CHECK_EQ(tools[0].name, "echo", "tool name");
+ err = NULL;
+ char *out = mcp_tool_invoke(&tools[0], "{\"msg\":\"hi\"}", &err);
+ CHECK(out != NULL, "execute");
+ CHECK_CONT(out, "pong", "server output");
+ free(out);
+ }
+ mcp_tools_free(tools, n);
+ mcp_client_free(c);
+ mem_free(&mem);
+}
+
+static void test_tool_invoke_wraps_errors(void)
+{
+ mcp_memory mem;
+ mcp_memory_init(&mem);
+ push_resp(&mem, 1,
+ "{\"tools\":[{\"name\":\"boom\",\"description\":\"\",\"inputSchema\":{\"type\":\"object\"}}]}");
+ push_resp(&mem, 2,
+ "{\"isError\":true,\"content\":[{\"type\":\"text\",\"text\":\"kaboom\"}]}");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_memory(&mem, &err);
+ mcp_tool *tools = NULL;
+ size_t n = 0;
+ err = NULL;
+ CHECK(mcp_list_tools(c, &tools, &n, &err) == 1, "tools");
+ if (n == 1) {
+ err = NULL;
+ char *out = mcp_tool_invoke(&tools[0], "{}", &err);
+ CHECK(out == NULL, "isError surfaces as failure");
+ CHECK(err && strstr(err, "MCP tool `boom` failed") != NULL,
+ "wrapped error");
+ CHECK(err && strstr(err, "kaboom") != NULL, "server text included");
+ free(err);
+ }
+ mcp_tools_free(tools, n);
+ mcp_client_free(c);
+ mem_free(&mem);
+}
+
+static void test_extract_json_plain_and_sse(void)
+{
+ char *err = NULL;
+ char *v = mcp_extract_json(
+ "application/json",
+ "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"a\":1}}", &err);
+ CHECK(v != NULL, "plain parses");
+ CHECK_CONT(v, "\"a\":1", "plain result");
+ free(v);
+
+ const char *sse = "event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":2,"
+ "\"result\":{\"b\":2}}\n\n";
+ err = NULL;
+ v = mcp_extract_json("text/event-stream", sse, &err);
+ CHECK(v != NULL, "sse parses");
+ CHECK_CONT(v, "\"b\":2", "sse result");
+ free(v);
+
+ err = NULL;
+ v = mcp_extract_json("text/event-stream", "event: message\n\n", &err);
+ CHECK(v == NULL, "sse without data line fails");
+ CHECK(err && strstr(err, "data:") != NULL, "sse error mentions data:");
+ free(err);
+}
+
+static void test_connect_all_collects_warnings(void)
+{
+ const char *names[] = {"good", "bad"};
+ const char *configs[] = {
+ "{\"command\":\"/definitely/not/a/real/binary-llsh\"}",
+ "{\"foo\":\"bar\"}",
+ };
+ strlist warnings;
+ sl_init(&warnings);
+ mcp_registry reg;
+ mcp_connect_all(names, configs, 2, &reg, &warnings);
+ CHECK(reg.tool_count == 0, "no tools from unreachable servers");
+ CHECK(warnings.len == 2, "one warning per failed server");
+ if (warnings.len == 2) {
+ CHECK_CONT(warnings.items[0], "`good`", "good warning");
+ CHECK_CONT(warnings.items[1], "`bad`", "bad warning");
+ CHECK_CONT(warnings.items[1], "no `command` or `url`", "bad reason");
+ }
+ for (size_t i = 0; i < warnings.len; i++)
+ free(warnings.items[i]);
+ free(warnings.items);
+ mcp_registry_free(&reg);
+}
+
+/* ------------------------------------------------------------------ */
+/* read_line_fd (via mcp_read_line) -- buffered stdio reads */
+/* ------------------------------------------------------------------ */
+
+/* Spawn `sh -c cmd` with stdout piped; returns the read end and the pid. */
+static int spawn_sh(const char *cmd, pid_t *out_pid)
+{
+ int pfd[2];
+ if (pipe(pfd) != 0)
+ return -1;
+ pid_t pid = fork();
+ if (pid < 0) {
+ close(pfd[0]);
+ close(pfd[1]);
+ return -1;
+ }
+ if (pid == 0) {
+ dup2(pfd[1], STDOUT_FILENO);
+ close(pfd[0]);
+ close(pfd[1]);
+ int dn = open("/dev/null", O_WRONLY);
+ if (dn >= 0) {
+ dup2(dn, STDERR_FILENO);
+ close(dn);
+ }
+ setpgid(0, 0);
+ execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
+ _exit(127);
+ }
+ close(pfd[1]);
+ if (out_pid)
+ *out_pid = pid;
+ return pfd[0];
+}
+
+static void kill_sh(pid_t pid)
+{
+ if (pid <= 0)
+ return;
+ kill(-pid, SIGKILL);
+ int st;
+ waitpid(pid, &st, 0);
+}
+
+/* A burst of lines arriving in one read(): the first line must come back
+ * alone, with the rest carried for the following calls. */
+static void test_read_line_burst(void)
+{
+ pid_t pid = 0;
+ int fd = spawn_sh("printf 'a\\nb\\nc\\n'", &pid);
+ CHECK(fd >= 0, "server spawned");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_fds(-1, fd, &err);
+ CHECK(c != NULL, "fd client created");
+ char *l = mcp_read_line(c, "t", &err);
+ CHECK(l && strcmp(l, "a") == 0, "first line of the burst");
+ free(l);
+ err = NULL;
+ l = mcp_read_line(c, "t", &err);
+ CHECK(l && strcmp(l, "b") == 0, "second line of the burst");
+ free(l);
+ err = NULL;
+ l = mcp_read_line(c, "t", &err);
+ CHECK(l && strcmp(l, "c") == 0, "third line of the burst");
+ free(l);
+ err = NULL;
+ l = mcp_read_line(c, "t", &err);
+ CHECK(l == NULL, "EOF after the burst");
+ CHECK(err && strstr(err, "server closed") != NULL, "EOF error message");
+ free(err);
+ mcp_client_free(c);
+ kill_sh(pid);
+}
+
+/* A line split across two read() calls must be reassembled from the carry
+ * buffer. */
+static void test_read_line_partial_across_reads(void)
+{
+ pid_t pid = 0;
+ int fd = spawn_sh("printf 'hel'; sleep 0.5; printf 'lo\\nworld\\n'", &pid);
+ CHECK(fd >= 0, "server spawned");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_fds(-1, fd, &err);
+ char *l = mcp_read_line(c, "t", &err);
+ CHECK(l && strcmp(l, "hello") == 0, "line assembled across two reads");
+ free(l);
+ err = NULL;
+ l = mcp_read_line(c, "t", &err);
+ CHECK(l && strcmp(l, "world") == 0, "following line intact");
+ free(l);
+ mcp_client_free(c);
+ kill_sh(pid);
+}
+
+/* Timeout with bytes in flight: the partial line is what we got. */
+static void test_read_line_timeout_partial(void)
+{
+ setenv("LLSH_MCP_TIMEOUT", "1", 1);
+ pid_t pid = 0;
+ int fd = spawn_sh("printf 'partial'; sleep 5", &pid);
+ CHECK(fd >= 0, "server spawned");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_fds(-1, fd, &err);
+ char *l = mcp_read_line(c, "t", &err);
+ CHECK(l && strcmp(l, "partial") == 0, "partial line returned on timeout");
+ free(l);
+ mcp_client_free(c);
+ kill_sh(pid);
+ unsetenv("LLSH_MCP_TIMEOUT");
+}
+
+/* Timeout with a silent server: an error, not a hang or a fake line. */
+static void test_read_line_timeout_empty(void)
+{
+ setenv("LLSH_MCP_TIMEOUT", "1", 1);
+ pid_t pid = 0;
+ int fd = spawn_sh("sleep 5", &pid);
+ CHECK(fd >= 0, "server spawned");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_fds(-1, fd, &err);
+ char *l = mcp_read_line(c, "t", &err);
+ CHECK(l == NULL, "no line when the server is silent");
+ CHECK(err && strstr(err, "timed out") != NULL, "timeout error message");
+ free(err);
+ mcp_client_free(c);
+ kill_sh(pid);
+ unsetenv("LLSH_MCP_TIMEOUT");
+}
+
+static void test_stdio_line_size_limit(void)
+{
+ setenv("LLSH_MCP_TIMEOUT", "3", 1);
+ pid_t pid = 0;
+ int fd = spawn_sh("head -c 4194305 /dev/zero", &pid);
+ CHECK(fd >= 0, "large-response server spawned");
+ char *err = NULL;
+ mcp_client *c = mcp_connect_fds(-1, fd, &err);
+ char *line = mcp_read_line(c, "large", &err);
+ CHECK(line == NULL, "oversized MCP response line rejected");
+ CHECK(err && strstr(err, "4 MiB limit"), "size-limit error returned");
+ free(line);
+ free(err);
+ mcp_client_free(c);
+ kill_sh(pid);
+ unsetenv("LLSH_MCP_TIMEOUT");
+}
+
+/* ------------------------------------------------------------------ */
+/* mcp_connect_stdio -- real fork/pipe wiring */
+/* ------------------------------------------------------------------ */
+
+/* The child must be able to write to stdout and the parent to read it
+ * back: the pipe ends were historically swapped, which made every stdio
+ * server fail. A child that answers on its own (and stays alive so the
+ * parent's write to its stdin does not hit EPIPE) exercises both ends. */
+static void test_connect_stdio_roundtrip(void)
+{
+ mcp_server_config cfg;
+ memset(&cfg, 0, sizeof cfg);
+ cfg.command = xstrdup("/bin/sh");
+ cfg.args = (char **)xmalloc(2 * sizeof *cfg.args);
+ cfg.args[0] = xstrdup("-c");
+ cfg.args[1] =
+ xstrdup("printf '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"pong\"}"
+ "\\n'; sleep 1");
+ cfg.arg_count = 2;
+
+ char *err = NULL;
+ mcp_client *c = mcp_connect_stdio(&cfg, &err);
+ CHECK(c != NULL, "stdio client spawned");
+ if (!c) {
+ if (err)
+ free(err);
+ mcp_server_config_free(&cfg);
+ return;
+ }
+ char *res = mcp_request(c, "ping", "{}", &err);
+ CHECK(res != NULL, "request round-trips to the child's stdout");
+ if (res)
+ CHECK(strstr(res, "pong") != NULL, "child response read back");
+ free(res);
+ if (err)
+ free(err);
+ mcp_client_free(c);
+ mcp_server_config_free(&cfg);
+}
+
+static void test_connect_stdio_sends_newline_terminated_request(void)
+{
+ setenv("LLSH_MCP_TIMEOUT", "2", 1);
+ mcp_server_config cfg;
+ memset(&cfg, 0, sizeof cfg);
+ cfg.command = xstrdup("/bin/sh");
+ cfg.args = (char **)xmalloc(2 * sizeof *cfg.args);
+ cfg.args[0] = xstrdup("-c");
+ cfg.args[1] = xstrdup(
+ "IFS= read -r req || exit 2; printf '{\"jsonrpc\":\"2.0\","
+ "\"id\":1,\"result\":\"newline-ok\"}\\n'; sleep 1");
+ cfg.arg_count = 2;
+ char *err = NULL;
+ mcp_client *c = mcp_connect_stdio(&cfg, &err);
+ CHECK(c != NULL, "stdio client spawned");
+ char *res = c ? mcp_request(c, "ping", "{}", &err) : NULL;
+ CHECK(res && strstr(res, "newline-ok"),
+ "server receives complete newline-framed request");
+ free(res);
+ free(err);
+ mcp_client_free(c);
+ mcp_server_config_free(&cfg);
+ unsetenv("LLSH_MCP_TIMEOUT");
+}
+
+int main(void)
+{
+ /* Writes to a pipe whose server already died (e.g. a failed exec) must
+ * surface as EPIPE, not kill the process with SIGPIPE. */
+ signal(SIGPIPE, SIG_IGN);
+ test_parses_stdio_config();
+ test_parses_http_config();
+ test_rejects_no_transport();
+ test_handshake_discovery_call();
+ test_server_error_is_surfaced();
+ test_notification_lines_are_skipped();
+ test_tool_invoke_roundtrip();
+ test_tool_invoke_wraps_errors();
+ test_extract_json_plain_and_sse();
+ test_connect_all_collects_warnings();
+ test_read_line_burst();
+ test_read_line_partial_across_reads();
+ test_read_line_timeout_partial();
+ test_read_line_timeout_empty();
+ test_stdio_line_size_limit();
+ test_connect_stdio_roundtrip();
+ test_connect_stdio_sends_newline_terminated_request();
+ printf("%d checks, %d failures\n", checks, failures);
+ return failures ? 1 : 0;
+}