| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import * as Effect from "effect/Effect" |
| 3 | import { spawn, spawnSync } from "node:child_process" |
| 4 | import { join } from "node:path" |
| 5 | |
| 6 | const fixture = join(__dirname, "fixtures", "node-terminal.ts") |
| 7 | |
| 8 | const runFixture = (mode: string, input: string) => |
| 9 | spawnSync(process.execPath, [fixture, mode], { |
| 10 | encoding: "utf8", |
| 11 | input, |
| 12 | timeout: 2_000 |
| 13 | }) |
| 14 | |
| 15 | const assertResult = (mode: string, input: string, expected: string) => { |
| 16 | const result = runFixture(mode, input) |
| 17 | assert.isUndefined(result.error) |
| 18 | assert.strictEqual(result.status, 0, result.stderr) |
| 19 | assert.isTrue(result.stderr.includes(`RESULT ${expected}`), result.stderr) |
| 20 | } |
| 21 | |
| 22 | const assertOpenResult = (mode: string, input: string, expected: string) => |
| 23 | Effect.callback<void>((resume) => { |
| 24 | const child = spawn(process.execPath, [fixture, mode]) |
| 25 | let stderr = "" |
| 26 | child.stderr.setEncoding("utf8") |
| 27 | child.stderr.on("data", (data) => { |
| 28 | stderr += data |
| 29 | if (stderr.includes("RESULT ")) { |
| 30 | child.stdin.end() |
| 31 | } |
| 32 | }) |
| 33 | child.on("exit", (code) => { |
| 34 | resume( |
| 35 | code === 0 && stderr.includes(`RESULT ${expected}`) |
| 36 | ? Effect.void |
| 37 | : Effect.die(new Error(stderr)) |
| 38 | ) |
| 39 | }) |
| 40 | child.stdin.write(input) |
| 41 | return Effect.sync(() => child.kill()) |
| 42 | }) |
| 43 | |
| 44 | // spawnSync blocks the Vitest worker; concurrent tests share a running timeout |
| 45 | // while waiting for other fixture processes to finish. |
| 46 | describe("NodeTerminal", { concurrent: false }, () => { |
| 47 | it("does not install a readline interface until the terminal is used", () => { |
| 48 | assertResult("unused", "", "{\"dataListeners\":0}") |
| 49 | }) |
| 50 | |
| 51 | it("fails a prompt with QuitError after piped input is exhausted", () => { |
| 52 | assertResult("prompts", "y\n", "{\"first\":true,\"second\":\"QuitError\"}") |
| 53 | }) |
| 54 | |
| 55 | it("delivers buffered keypresses before ending the input queue", () => { |
| 56 | assertResult("read-input", "yn", "{\"first\":\"y\",\"second\":\"n\",\"ended\":true}") |
| 57 | }) |
| 58 | |
| 59 | it("flushes an unterminated line before failing readLine with QuitError at EOF", () => { |
| 60 | assertResult("read-line", "last line", "{\"first\":\"last line\",\"second\":\"QuitError\"}") |
| 61 | }) |
| 62 | |
| 63 | it("preserves lines buffered between sequential readLine calls", () => { |
| 64 | assertResult("read-lines", "first\nsecond\n", "{\"first\":\"first\",\"second\":\"second\"}") |
| 65 | }) |
| 66 | |
| 67 | it("fails readLine with QuitError when stdin ended before initialization", () => { |
| 68 | assertResult("read-line-after-end", "", "\"QuitError\"") |
| 69 | }) |
| 70 | |
| 71 | it.effect("disposes readline after its idle TTL", () => |
| 72 | assertOpenResult( |
| 73 | "read-line-disposed", |
| 74 | "line\n", |
| 75 | "{\"line\":\"line\",\"duringTtl\":1,\"dataListeners\":0}" |
| 76 | )) |
| 77 | }) |