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
4.1 KiB117 linesraw
1import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
3import { join } from "node:path";
4import { tempDir, until } from "@berry/util";
5import { localEnvironment } from "./environment.ts";
6
7Deno.test({
8  name:
9    "pseudo-terminals preserve argv, Unicode, cwd and environment, and support input and resizing",
10  ignore: !["linux", "windows"].includes(Deno.build.os),
11  async fn() {
12    await using root = await tempDir();
13    const cwd = join(root.path, "terminal 🐈");
14    await Deno.mkdir(cwd);
15    const env = localEnvironment(cwd);
16    const args = ["", 'a "quote"', "C:\\trailing\\", "héllo 🐈"];
17    const script = `
18      console.log('args=' + JSON.stringify(Deno.args));
19      console.log('cwd=' + Deno.cwd());
20      console.log('env=' + Deno.env.get('BERRY_PTY_TEST'));
21      console.log('tty=' + Deno.stdin.isTerminal() + ',' + Deno.stdout.isTerminal());
22      if (Deno.build.os === 'windows') {
23        const kernel = Deno.dlopen('kernel32.dll', {
24          SetConsoleCP: { parameters: ['u32'], result: 'i32' },
25        });
26        // ReadFile uses the application's console code page, not ConPTY's UTF-8 transport
27        if (!kernel.symbols.SetConsoleCP(65001)) throw new Error('SetConsoleCP failed');
28        kernel.close();
29      }
30      Deno.stdin.setRaw(true);
31      console.log('ready');
32      const input = new Uint8Array(1024);
33      const decoder = new TextDecoder();
34      let typed = '';
35      while (!typed.includes('\\r')) {
36        const count = await Deno.stdin.read(input);
37        if (count === null) break;
38        typed += decoder.decode(input.subarray(0, count), { stream: true });
39      }
40      console.log('input=' + typed.trimEnd());
41      console.log('size=' + JSON.stringify(Deno.consoleSize()));
42      Deno.exit(7);
43    `;
44    const pty = await env.pty([Deno.execPath(), "eval", script, ...args], {
45      cols: 160,
46      rows: 30,
47      env: { BERRY_PTY_TEST: "value 🐈" },
48    }, BACKGROUND_CONTEXT);
49    let output = "";
50    const reading = (async () => {
51      const decoder = new TextDecoder();
52      for await (const chunk of pty.readable) output += decoder.decode(chunk, { stream: true });
53      output += decoder.decode();
54    })();
55    const input = pty.writable.getWriter();
56    try {
57      await until(() => output.includes("ready"));
58      assertStringIncludes(output, "args=" + JSON.stringify(args));
59      assertStringIncludes(output, "cwd=" + cwd);
60      assertStringIncludes(output, "env=value 🐈");
61      assertStringIncludes(output, "tty=true,true");
62      pty.resize(140, 40);
63      await input.write(new TextEncoder().encode("typed héllo\r"));
64      assertEquals(await pty.exited, 7);
65      await reading;
66      assertStringIncludes(output, "input=typed héllo");
67      assertStringIncludes(output, 'size={"columns":140,"rows":40}');
68    } finally {
69      pty.kill();
70      await reading;
71      await pty.exited;
72      input.releaseLock();
73    }
74  },
75});
76
77Deno.test({
78  name: "ConPTY closes on abort and output cancellation, and can start again after a failed spawn",
79  ignore: Deno.build.os !== "windows",
80  async fn() {
81    const env = localEnvironment(Deno.cwd());
82    await assertRejects(() =>
83      env.pty(["berry-nonexistent-program.exe"], {
84        cols: 80,
85        rows: 24,
86      }, BACKGROUND_CONTEXT)
87    );
88
89    const controller = new AbortController();
90    const pty = await env.pty([Deno.execPath(), "eval", "setInterval(() => {}, 1000)"], {
91      cols: 80,
92      rows: 24,
93    }, withAbortSignal(controller.signal, BACKGROUND_CONTEXT));
94    const drained = pty.readable.pipeTo(new WritableStream());
95    controller.abort();
96    assertEquals(await pty.exited, null);
97    await drained;
98
99    const noisy = await env.pty(
100      [
101        Deno.execPath(),
102        "eval",
103        `
104      const block = new TextEncoder().encode('x'.repeat(65536));
105      while (true) await Deno.stdout.write(block);
106    `,
107      ],
108      { cols: 80, rows: 24 },
109      BACKGROUND_CONTEXT,
110    );
111    const reader = noisy.readable.getReader();
112    assert(!(await reader.read()).done);
113    await reader.cancel();
114    await noisy.exited;
115    noisy.kill();
116  },
117});