char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somtests: split the slowest files so that --parallel runs them side by sidebe80d8a

main
7.4 KiB198 linesraw
1import { assert, assertEquals, assertRejects } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { testExtension } from "@berry/api/testing.ts";
4import { tempDir, until } from "@berry/util";
5import mcp from "../../extensions/mcp/mod.ts";
6import { BOOTSTRAP, Connection, RemoteEnvironment } from "./connection.ts";
7
8Deno.test("MCP servers use the worker's environment and remote force-stop leaves the worker usable", async () => {
9  await using dir = await tempDir({ prefix: "remote-mcp-" });
10  await Deno.writeTextFile(
11    dir.path + "/server.ts",
12    await Deno.readTextFile(new URL("../../extensions/mcp/test-server.ts", import.meta.url)),
13  );
14  await Deno.writeTextFile(
15    dir.path + "/mcp.json",
16    JSON.stringify({
17      mcpServers: {
18        probe: {
19          command: Deno.execPath(),
20          args: ["run", "-A", "server.ts", "stubborn"],
21          env: { TOKEN: "${BERRY_MACHINE}" },
22        },
23      },
24    }),
25  );
26  const connection = await Connection.open("mcp-test", [
27    "env",
28    "BERRY_MACHINE=mcp-test",
29    Deno.execPath(),
30    "eval",
31    "--quiet",
32    BOOTSTRAP,
33  ]);
34  const env = new RemoteEnvironment("mcp-test", connection, dir.path);
35  const ext = testExtension(mcp, { env });
36  try {
37    await ext.configure({ configFile: "mcp.json" });
38    const result = await ext.call("mcp_call", { server: "probe", tool: "echo" }) as {
39      structuredContent: { token: string; cwd: string };
40    };
41    assertEquals(result.structuredContent.token, "mcp-test");
42    assertEquals(result.structuredContent.cwd, dir.path);
43    await ext.dispose();
44    assertEquals((await env.readTextFile("mcp.json", context)).ok, true);
45
46    const child = await env.spawn(["cat"], {}, context);
47    await child.stdout.cancel();
48    const writer = child.stdin.getWriter();
49    await writer.write(new TextEncoder().encode("discarded output"));
50    await writer.close();
51    assertEquals(await child.exited, 0);
52    assertEquals((await env.fileInfo("server.ts", context)).ok, true);
53
54    const abandoned = await env.spawn(["cat"], {}, context);
55    const pending = assertRejects(() => new Response(abandoned.stdout).text(), Error, "lost");
56    await connection.close();
57    await pending;
58  } finally {
59    await ext.dispose();
60    await connection.close();
61  }
62});
63
64Deno.test("HTTP MCP calls reuse a live worker but the first call after worker replacement opens a fresh session", async () => {
65  await using dir = await tempDir({ prefix: "remote-http-mcp-" });
66  let initialized = 0;
67  let called = 0;
68  let session = "";
69  const server = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, async (request) => {
70    if (request.method === "GET") return new Response(null, { status: 405 });
71    if (request.method === "DELETE") return new Response(null, { status: 204 });
72    const message = await request.json();
73    if (message.id === undefined) return new Response(null, { status: 202 });
74    if (message.method !== "initialize" && request.headers.get("Mcp-Session-Id") !== session) {
75      return new Response(null, { status: 404 });
76    }
77    const result = message.method === "initialize"
78      ? {
79        protocolVersion: message.params.protocolVersion,
80        capabilities: { tools: {} },
81        serverInfo: { name: "http", version: "1" },
82      }
83      : { content: [], structuredContent: { session, called: ++called } };
84    if (message.method === "initialize") session = String(++initialized);
85    return Response.json({ jsonrpc: "2.0", id: message.id, result }, {
86      headers: { "Mcp-Session-Id": session },
87    });
88  });
89  await Deno.writeTextFile(
90    dir.path + "/mcp.json",
91    JSON.stringify({
92      mcpServers: { http: { url: `http://127.0.0.1:${server.addr.port}/mcp` } },
93    }),
94  );
95  const open = () => Connection.open("http", [Deno.execPath(), "eval", "--quiet", BOOTSTRAP]);
96  const first = await open();
97  let second: Connection | undefined;
98  const ext = testExtension(mcp, { env: new RemoteEnvironment("http", first, dir.path) });
99  const call = (env?: RemoteEnvironment) =>
100    ext.call("mcp_call", { server: "http", tool: "echo" }, { env });
101  try {
102    await ext.configure({ configFile: "mcp.json" });
103    await call();
104    await call(new RemoteEnvironment("http", first, dir.path));
105    assertEquals(initialized, 1);
106    await first.close();
107    assert(first.lifetime.aborted);
108    second = await open();
109    const result = await call(new RemoteEnvironment("http", second, dir.path)) as {
110      structuredContent: { session: string; called: number };
111    };
112    assertEquals(result.structuredContent, { session: "2", called: 3 });
113    assertEquals(initialized, 2);
114    assertEquals(called, 3);
115  } finally {
116    await ext.dispose();
117    await first.close();
118    await second?.close();
119    await server.shutdown();
120  }
121});
122
123Deno.test({
124  name: "a stalled remote worker cannot prevent fetch cancellation or HTTP MCP disposal",
125  ignore: Deno.build.os !== "linux",
126  async fn() {
127    await using dir = await tempDir({ prefix: "stalled-mcp-" });
128    const server = Deno.serve(
129      { hostname: "127.0.0.1", port: 0, onListen() {} },
130      async (request) => {
131        if (request.method === "GET") return new Response(null, { status: 405 });
132        if (request.method === "DELETE") return new Response(null, { status: 204 });
133        const message = await request.json();
134        if (message.id === undefined) return new Response(null, { status: 202 });
135        const result = message.method === "initialize"
136          ? {
137            protocolVersion: message.params.protocolVersion,
138            capabilities: { tools: {} },
139            serverInfo: { name: "http", version: "1" },
140          }
141          : { content: [] };
142        return Response.json({ jsonrpc: "2.0", id: message.id, result }, {
143          headers: { "Mcp-Session-Id": "session" },
144        });
145      },
146    );
147    const url = `http://127.0.0.1:${server.addr.port}/mcp`;
148    await Deno.writeTextFile(
149      dir.path + "/mcp.json",
150      JSON.stringify({ mcpServers: { http: { url } } }),
151    );
152    const connection = await Connection.open("stalled", [
153      "sh",
154      "-c",
155      'echo $$ > "$1"; shift; exec "$@"',
156      "sh",
157      dir.path + "/pid",
158      Deno.execPath(),
159      "eval",
160      "--quiet",
161      BOOTSTRAP,
162    ]);
163    const pid = Number(await Deno.readTextFile(dir.path + "/pid"));
164    const env = new RemoteEnvironment("stalled", connection, dir.path);
165    const ext = testExtension(mcp, { env });
166    try {
167      await ext.configure({ configFile: "mcp.json" });
168      await ext.call("mcp_call", { server: "http", tool: "echo" });
169      Deno.kill(pid, "SIGSTOP");
170      const abort = new AbortController();
171      const reason = new Error("request canceled");
172      let rejected: unknown;
173      const fetching = env.fetch(url, { signal: abort.signal }).catch((error: unknown) =>
174        rejected = error
175      );
176      abort.abort(reason);
177      await until(() => rejected !== undefined, {
178        timeout: 1000,
179        message: "fetch abort waited for a worker reply",
180      });
181      await fetching;
182      assertEquals(rejected, reason);
183      let disposed = false;
184      const disposing = ext.dispose().then(() => disposed = true);
185      await until(() => disposed, {
186        timeout: 3000,
187        message: "HTTP MCP disposal waited for a stalled worker",
188      });
189      await disposing;
190      assert(!connection.lifetime.aborted);
191    } finally {
192      Deno.kill(pid, "SIGCONT");
193      await ext.dispose();
194      await connection.close();
195      await server.shutdown();
196    }
197  },
198});