char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somwindows: add terminal support via ConPTY + PowerShell0853dde

main
2.9 KiB77 linesraw
1import { assertEquals, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { until } from "@berry/util";
4import { BOOTSTRAP, Connection, RemoteEnvironment, sshCommand } from "./connection.ts";
5
6const destination = Deno.env.get("BERRY_TEST_SSH");
7
8Deno.test({
9  name: "remote terminal I/O stays inside its channel and leaves the worker usable",
10  ignore: !destination && !["linux", "windows"].includes(Deno.build.os),
11  async fn() {
12    const connection = await Connection.open(
13      "pty-test",
14      destination ? sshCommand(destination) : [Deno.execPath(), "eval", "--quiet", BOOTSTRAP],
15    );
16    const env = new RemoteEnvironment("pty-test", connection, connection.hello.home);
17    try {
18      const pty = await env.pty(
19        [
20          destination ? "deno" : Deno.execPath(),
21          "eval",
22          `
23          console.log('terminal=' + [Deno.stdin, Deno.stdout, Deno.stderr].every(s => s.isTerminal()));
24          if (!Deno.stdin.isTerminal()) Deno.exit(2);
25          Deno.stdin.setRaw(true);
26          console.log('ready');
27          const buffer = new Uint8Array(1024);
28          const decoder = new TextDecoder();
29          let input = '';
30          while (!input.includes('\\r')) {
31            const n = await Deno.stdin.read(buffer);
32            if (n === null) break;
33            input += decoder.decode(buffer.subarray(0, n), { stream: true });
34          }
35          console.log('received=' + input.trimEnd());
36          console.error('terminal-stderr');
37          console.log('size=' + JSON.stringify(Deno.consoleSize()));
38          Deno.exit(17);
39        `,
40        ],
41        { cols: 100, rows: 30 },
42        context,
43      );
44      let output = "";
45      let readError: unknown;
46      let exit: number | null | undefined;
47      const drained = (async () => {
48        const decoder = new TextDecoder();
49        for await (const chunk of pty.readable) output += decoder.decode(chunk, { stream: true });
50      })().catch((error) => readError = error);
51      void pty.exited.then((code) => exit = code);
52      const input = pty.writable.getWriter();
53      try {
54        await until(() => {
55          if (connection.lost) throw connection.lost;
56          return output.includes("ready");
57        });
58        assertStringIncludes(output, "terminal=true");
59        pty.resize(90, 33);
60        await input.write(new TextEncoder().encode("berry-pty-probe\r"));
61        await until(() => exit !== undefined);
62        assertEquals(exit, 17);
63        await drained;
64        assertEquals(readError, undefined);
65        assertStringIncludes(output, "received=berry-pty-probe");
66        assertStringIncludes(output, "terminal-stderr");
67        assertStringIncludes(output, 'size={"columns":90,"rows":33}');
68        assertEquals((await env.fileInfo(env.home, context)).ok, true);
69      } finally {
70        pty.kill();
71        input.releaseLock();
72      }
73    } finally {
74      await connection.close();
75    }
76  },
77});