1import { assert, describe, it } from "@effect/vitest"
2import * as Effect from "effect/Effect"
3import { spawn, spawnSync } from "node:child_process"
4import { join } from "node:path"
5
6const fixture = join(__dirname, "fixtures", "node-terminal.ts")
7
8const runFixture = (mode: string, input: string) =>
9 spawnSync(process.execPath, [fixture, mode], {
10 encoding: "utf8",
11 input,
12 timeout: 2_000
13 })
14
15const 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
22const 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.
46describe("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})