char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Sommcp: add streaming fetch on conversation-local and remote environmentse5ec380

main
5.6 KiB161 linesraw
1import { assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { until } from "@berry/util";
4import { BOOTSTRAP, Connection, RemoteEnvironment } from "./connection.ts";
5
6async function fixture(handler: (request: Request) => Response | Promise<Response>) {
7  const server = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, handler);
8  const connection = await Connection.open("fetch-test", [
9    Deno.execPath(),
10    "eval",
11    "--quiet",
12    BOOTSTRAP,
13  ]);
14  return {
15    env: new RemoteEnvironment("fetch-test", connection, "/"),
16    url: `http://127.0.0.1:${server.addr.port}`,
17    connection,
18    async [Symbol.asyncDispose]() {
19      await connection.close();
20      await server.shutdown();
21    },
22  };
23}
24
25Deno.test("remote fetch originates at the worker, preserving binary requests, metadata, and incremental bodies", async () => {
26  const release = Promise.withResolvers<void>();
27  const encoder = new TextEncoder();
28  await using f = await fixture(async (request) => {
29    const path = new URL(request.url).pathname;
30    if (path === "/redirect") {
31      return new Response(null, { status: 302, headers: { location: "/stream" } });
32    }
33    if (path === "/stream") {
34      return new Response(
35        new ReadableStream({
36          start(controller) {
37            controller.enqueue(encoder.encode("first"));
38          },
39          async pull(controller) {
40            await release.promise;
41            controller.enqueue(encoder.encode("second"));
42            controller.close();
43          },
44        }),
45      );
46    }
47    return new Response(await request.arrayBuffer(), {
48      status: 201,
49      statusText: "Created",
50      headers: { "x-seen": request.headers.get("x-secret") ?? "" },
51    });
52  });
53  const fetchHere = globalThis.fetch;
54  globalThis.fetch = () => Promise.reject(new Error("fetch must run on the worker"));
55  try {
56    const echoed = await f.env.fetch(f.url + "/echo", {
57      method: "POST",
58      headers: new Headers({ "x-secret": "from-remote" }),
59      body: new Uint8Array([0, 255, 10]),
60      signal: AbortSignal.timeout(5000),
61    });
62    assertEquals(echoed.status, 201);
63    assertEquals(echoed.statusText, "Created");
64    assertEquals(echoed.headers.get("x-seen"), "from-remote");
65    assertEquals(new Uint8Array(await echoed.arrayBuffer()), new Uint8Array([0, 255, 10]));
66
67    const response = await f.env.fetch(f.url + "/redirect", { signal: AbortSignal.timeout(5000) });
68    assertEquals(response.redirected, true);
69    assertEquals(response.url, f.url + "/stream");
70    const reader = response.body!.getReader();
71    assertEquals(new TextDecoder().decode((await reader.read()).value), "first");
72    release.resolve();
73    assertEquals(new TextDecoder().decode((await reader.read()).value), "second");
74    assertEquals((await reader.read()).done, true);
75  } finally {
76    release.resolve();
77    globalThis.fetch = fetchHere;
78  }
79});
80
81Deno.test("remote fetch aborts before headers and stops streaming bodies on abort, cancel, or environment cleanup", async () => {
82  const entered = Promise.withResolvers<void>();
83  const release = Promise.withResolvers<void>();
84  let cancelled = 0;
85  await using f = await fixture(async (request) => {
86    if (new URL(request.url).pathname === "/headers") {
87      entered.resolve();
88      await release.promise;
89      return new Response("late");
90    }
91    return new Response(
92      new ReadableStream({
93        start(controller) {
94          controller.enqueue(new TextEncoder().encode("first"));
95        },
96        cancel() {
97          cancelled++;
98        },
99      }),
100    );
101  });
102  try {
103    const beforeHeaders = new AbortController();
104    const pending = f.env.fetch(f.url + "/headers", { signal: beforeHeaders.signal });
105    const rejected = assertRejects(() => pending);
106    await entered.promise;
107    beforeHeaders.abort();
108    await rejected;
109    release.resolve();
110
111    const abort = new AbortController();
112    const response = await f.env.fetch(f.url + "/body", { signal: abort.signal });
113    const reader = response.body!.getReader();
114    await reader.read();
115    const reading = assertRejects(() => reader.read());
116    abort.abort();
117    await reading;
118    await until(() => cancelled === 1);
119
120    const discard = await f.env.fetch(f.url + "/body");
121    await discard.body!.cancel();
122    await until(() => cancelled === 2);
123
124    const orphan = await f.env.fetch(f.url + "/body");
125    const orphanReader = orphan.body!.getReader();
126    await orphanReader.read();
127    const lost = assertRejects(() => orphanReader.read());
128    await f.env.cleanup(context);
129    await lost;
130    await until(() => cancelled === 3);
131  } finally {
132    release.resolve();
133  }
134});
135
136Deno.test("losing the worker fails a streaming response rather than silently ending it", async () => {
137  await using f = await fixture(() =>
138    new Response(
139      new ReadableStream({
140        start(controller) {
141          controller.enqueue(new TextEncoder().encode("first"));
142        },
143      }),
144    )
145  );
146  const response = await f.env.fetch(f.url);
147  const reader = response.body!.getReader();
148  await reader.read();
149  const reading = assertRejects(() => reader.read(), Error, "lost");
150  await f.connection.close();
151  await reading;
152});
153
154Deno.test("remote fetch handles bodyless responses and reports network failures", async () => {
155  await using f = await fixture(() => new Response(null, { status: 204 }));
156  const response = await f.env.fetch(f.url);
157  assertEquals(response.status, 204);
158  assertEquals(response.body, null);
159  const failed = await assertRejects(() => f.env.fetch("http://127.0.0.1:1"), TypeError);
160  assertStringIncludes(failed.message, "fetch-test");
161});