1import { assert, describe, it } from "@effect/vitest"
2import * as Effect from "effect/Effect"
3import { spawn, spawnSync } from "node:child_process"
4import { fileURLToPath } from "node:url"
5
6const fixture = fileURLToPath(new URL("fixtures/deno-terminal.ts", import.meta.url))
7
8const runFixture = (mode: string, input: string) =>
9 spawnSync(Deno.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(Deno.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
44const assertInteropResult = Effect.callback<void>((resume) => {
45 const child = spawn(Deno.execPath(), [fixture, "interop"])
46 let stderr = ""
47 let sentTerminalInput = false
48 child.stderr.setEncoding("utf8")
49 child.stderr.on("data", (data) => {
50 stderr += data
51 if (!sentTerminalInput && stderr.includes("READY")) {
52 sentTerminalInput = true
53 child.stdin.end("terminal\n")
54 }
55 })
56 child.on("exit", (code) => {
57 resume(
58 code === 0 && stderr.includes("RESULT {\"stdio\":\"stdio\",\"terminal\":\"terminal\"}")
59 ? Effect.void
60 : Effect.die(new Error(stderr))
61 )
62 })
63 child.stdin.write("stdio\n")
64 return Effect.sync(() => child.kill())
65})
66
67// spawnSync blocks the Vitest worker; concurrent tests share a running timeout
68// while waiting for other fixture processes to finish.
69describe("DenoTerminal", { concurrent: false }, () => {
70 it("does not install a readline interface until the terminal is used", () => {
71 assertResult("unused", "", "{\"dataListeners\":0}")
72 })
73
74 it("fails a prompt with QuitError after piped input is exhausted", () => {
75 assertResult("prompts", "y\n", "{\"first\":true,\"second\":\"QuitError\"}")
76 })
77
78 it("delivers buffered keypresses before ending the input queue", () => {
79 assertResult("read-input", "yn", "{\"first\":\"y\",\"second\":\"n\",\"ended\":true}")
80 })
81
82 it("flushes an unterminated line before failing readLine with QuitError at EOF", () => {
83 assertResult("read-line", "last line", "{\"first\":\"last line\",\"second\":\"QuitError\"}")
84 })
85
86 it("preserves lines buffered between sequential readLine calls", () => {
87 assertResult("read-lines", "first\nsecond\n", "{\"first\":\"first\",\"second\":\"second\"}")
88 })
89
90 it("fails readLine with QuitError when stdin ended before initialization", () => {
91 assertResult("read-line-after-end", "", "\"QuitError\"")
92 })
93
94 it.effect("disposes readline after its idle TTL", () =>
95 assertOpenResult(
96 "read-line-disposed",
97 "line\n",
98 "{\"line\":\"line\",\"duringTtl\":1,\"dataListeners\":0}"
99 ))
100
101 it.effect("interoperates with DenoStdio on stdin", () => assertInteropResult)
102})