saeeol 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/provider/provider-schemas.ts +1 -1
- package/src/server/routes/instance/httpapi/groups/ltm.ts +93 -0
- package/src/server/routes/instance/httpapi/handlers/ltm.ts +118 -0
- package/src/server/routes/instance/httpapi/handlers/provider.ts +4 -3
- package/src/server/routes/instance/index.ts +96 -1
- package/src/server/routes/instance/provider.ts +4 -3
- package/test/smoke/tui-walkthrough-driver.ts +234 -0
- package/test/smoke/tui-walkthrough-input.test.ts +285 -0
- package/test/smoke/tui-walkthrough-leader.test.ts +175 -0
- package/test/smoke/tui-walkthrough-scroll.test.ts +177 -0
- package/test/smoke/tui-walkthrough-slash.test.ts +302 -0
- package/test/smoke/tui-walkthrough-system.test.ts +208 -0
- package/test/smoke/.tui-debug-output.txt +0 -1
- package/test/smoke/.tui-debug-plain.txt +0 -1
- package/test/smoke/.tui-walkthrough-report.txt +0 -122
- package/test/smoke/tui-walkthrough.test.ts +0 -520
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tui-walkthrough.test.ts — SAEEOL TUI 전체 워크스루
|
|
3
|
-
*
|
|
4
|
-
* bun-pty로 TUI를 띄우고 모든 화면/단축키를 자동으로 눌러서:
|
|
5
|
-
* 1. 에러 없이 렌더링되는지
|
|
6
|
-
* 2. 각 다이얼로그가 열리고 닫히는지
|
|
7
|
-
* 3. 슬래시 명령이 동작하는지
|
|
8
|
-
* 4. 화면 전환이 정상인지
|
|
9
|
-
* 5. 세션 생성 → 메시지 흐름이 되는지
|
|
10
|
-
*
|
|
11
|
-
* API 호출/비용 발생 없음 — LLM 엔드포인트는 죽은 서버로 설정.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, expect, test, beforeAll, afterAll } from "bun:test"
|
|
15
|
-
import { spawn as PtySpawn, type IPty } from "bun-pty"
|
|
16
|
-
import { tmpdir, disposeAllInstances } from "../fixture/fixture"
|
|
17
|
-
import * as Log from "@saeeol/core/util/log"
|
|
18
|
-
import { Flag } from "@saeeol/core/flag/flag"
|
|
19
|
-
import path from "path"
|
|
20
|
-
|
|
21
|
-
void Log.init({ print: false })
|
|
22
|
-
|
|
23
|
-
const BIN = path.resolve(import.meta.dir, "../../bin/saeeol.cjs")
|
|
24
|
-
|
|
25
|
-
// ── 유틸리티 ──
|
|
26
|
-
|
|
27
|
-
function stripAnsi(s: string): string {
|
|
28
|
-
return s
|
|
29
|
-
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
|
30
|
-
.replace(/\x1b\].*?\x07/g, "")
|
|
31
|
-
.replace(/\x1b\[.*?m/g, "")
|
|
32
|
-
.replace(/[\x00-\x09\x0b\x0c\x0e-\x1f]/g, "")
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Known issues — 이 패턴은 무시 (해결되면 제거)
|
|
36
|
-
const KNOWN_ISSUES = [
|
|
37
|
-
/export named 'jsx' not found in module.*@opentui\/solid\/jsx-runtime/i,
|
|
38
|
-
/e=Export named 'jsx' not found.*exception/i,
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
function hasError(plain: string): string[] {
|
|
42
|
-
return plain
|
|
43
|
-
.split("\n")
|
|
44
|
-
.filter((l) => {
|
|
45
|
-
const t = l.trim()
|
|
46
|
-
if (!t) return false
|
|
47
|
-
// known issue면 무조건 스킵
|
|
48
|
-
if (KNOWN_ISSUES.some((re) => re.test(t))) return false
|
|
49
|
-
return (
|
|
50
|
-
/unhandled|TypeError|ReferenceError|SyntaxError|Cannot find module|Error:/i.test(t) ||
|
|
51
|
-
/exception/i.test(t) ||
|
|
52
|
-
/\be=\b.*not found/i.test(t) ||
|
|
53
|
-
/crashed|fatal|panic/i.test(t)
|
|
54
|
-
)
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ── TUI 드라이버 ──
|
|
59
|
-
|
|
60
|
-
class TuiDriver {
|
|
61
|
-
pty: IPty
|
|
62
|
-
output = ""
|
|
63
|
-
snapshots: Array<{ name: string; raw: string; plain: string }> = []
|
|
64
|
-
|
|
65
|
-
constructor(cwd: string) {
|
|
66
|
-
this.pty = PtySpawn("node", [BIN, "tui"], {
|
|
67
|
-
name: "xterm-256color",
|
|
68
|
-
cols: 120,
|
|
69
|
-
rows: 30,
|
|
70
|
-
cwd,
|
|
71
|
-
env: {
|
|
72
|
-
...process.env,
|
|
73
|
-
TERM: "xterm-256color",
|
|
74
|
-
FORCE_COLOR: "1",
|
|
75
|
-
SAEEOL_LLM_PROVIDER: "custom",
|
|
76
|
-
SAEEOL_LLM_BASE_URL: "http://localhost:1",
|
|
77
|
-
SAEEOL_LLM_API_KEY: "test",
|
|
78
|
-
SAEEOL_LLM_MODEL: "test",
|
|
79
|
-
},
|
|
80
|
-
})
|
|
81
|
-
this.pty.onData((data) => {
|
|
82
|
-
this.output += data
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
write(s: string) {
|
|
87
|
-
this.pty.write(s)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async wait(ms: number) {
|
|
91
|
-
await Bun.sleep(ms)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
snapshot(name: string) {
|
|
95
|
-
const raw = this.output
|
|
96
|
-
const plain = stripAnsi(this.output)
|
|
97
|
-
this.snapshots.push({ name, raw, plain })
|
|
98
|
-
return { raw, plain }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
errors(): string[] {
|
|
102
|
-
return hasError(stripAnsi(this.output))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async kill() {
|
|
106
|
-
try {
|
|
107
|
-
this.pty.kill()
|
|
108
|
-
} catch {}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** 특정 텍스트가 화면에 나타날 때까지 대기 */
|
|
112
|
-
async waitFor(text: string, timeoutMs = 10000): Promise<boolean> {
|
|
113
|
-
const start = Date.now()
|
|
114
|
-
while (Date.now() - start < timeoutMs) {
|
|
115
|
-
if (stripAnsi(this.output).includes(text)) return true
|
|
116
|
-
await Bun.sleep(200)
|
|
117
|
-
}
|
|
118
|
-
return false
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** leader 키 (ctrl+x) + 후속 키 */
|
|
122
|
-
async leader(key: string, delay = 100) {
|
|
123
|
-
this.write("\x18") // ctrl+x
|
|
124
|
-
await this.wait(delay)
|
|
125
|
-
this.write(key)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** 에스케이프로 다이얼로그 닫기 */
|
|
129
|
-
async dismiss(delay = 500) {
|
|
130
|
-
this.write("\x1b") // escape
|
|
131
|
-
await this.wait(delay)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── 테스트 ──
|
|
136
|
-
|
|
137
|
-
describe("TUI walkthrough: full interaction", () => {
|
|
138
|
-
let driver: TuiDriver
|
|
139
|
-
let tmp: Awaited<ReturnType<typeof tmpdir>>
|
|
140
|
-
|
|
141
|
-
beforeAll(async () => {
|
|
142
|
-
tmp = await tmpdir({ git: true, config: {} })
|
|
143
|
-
Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = false
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
afterAll(async () => {
|
|
147
|
-
await driver?.kill()
|
|
148
|
-
await disposeAllInstances()
|
|
149
|
-
Flag.SAEEOL_EXPERIMENTAL_HTTPAPI = true
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
// ── 1. 부트스트랩 ──
|
|
153
|
-
|
|
154
|
-
test("boot: TUI starts without errors", async () => {
|
|
155
|
-
driver = new TuiDriver(tmp.path)
|
|
156
|
-
|
|
157
|
-
// 서버 시작 + 초기 렌더링 대기
|
|
158
|
-
await driver.wait(6000)
|
|
159
|
-
driver.snapshot("boot")
|
|
160
|
-
|
|
161
|
-
// 에러 없음
|
|
162
|
-
expect(driver.errors()).toHaveLength(0)
|
|
163
|
-
|
|
164
|
-
// alternate screen 진입
|
|
165
|
-
expect(driver.output).toMatch(/\x1b\[\?9001h|\x1b\[\?1004h/)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// ── 2. Home 화면 ──
|
|
169
|
-
|
|
170
|
-
test("home: renders content", async () => {
|
|
171
|
-
const { plain } = driver.snapshot("home")
|
|
172
|
-
expect(plain.length).toBeGreaterThan(0)
|
|
173
|
-
expect(driver.errors()).toHaveLength(0)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
// ── 3. 커맨드 팔레트 (ctrl+p) ──
|
|
177
|
-
|
|
178
|
-
test("command palette: ctrl+p opens, esc closes", async () => {
|
|
179
|
-
driver.write("\x10") // ctrl+p
|
|
180
|
-
await driver.wait(1000)
|
|
181
|
-
driver.snapshot("command-palette-open")
|
|
182
|
-
expect(driver.errors()).toHaveLength(0)
|
|
183
|
-
|
|
184
|
-
await driver.dismiss()
|
|
185
|
-
driver.snapshot("command-palette-closed")
|
|
186
|
-
expect(driver.errors()).toHaveLength(0)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
// ── 4. 세션 리스트 (leader+l) ──
|
|
190
|
-
|
|
191
|
-
test("session list: leader+l opens dialog", async () => {
|
|
192
|
-
await driver.leader("l")
|
|
193
|
-
await driver.wait(1500)
|
|
194
|
-
driver.snapshot("session-list")
|
|
195
|
-
expect(driver.errors()).toHaveLength(0)
|
|
196
|
-
|
|
197
|
-
await driver.dismiss()
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
// ── 5. 모델 선택 (leader+m) ──
|
|
201
|
-
|
|
202
|
-
test("model selector: leader+m opens dialog", async () => {
|
|
203
|
-
await driver.leader("m")
|
|
204
|
-
await driver.wait(1500)
|
|
205
|
-
driver.snapshot("model-selector")
|
|
206
|
-
expect(driver.errors()).toHaveLength(0)
|
|
207
|
-
|
|
208
|
-
await driver.dismiss()
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
// ── 6. 에이전트 선택 (leader+a) ──
|
|
212
|
-
|
|
213
|
-
test("agent selector: leader+a opens dialog", async () => {
|
|
214
|
-
await driver.leader("a")
|
|
215
|
-
await driver.wait(1500)
|
|
216
|
-
driver.snapshot("agent-selector")
|
|
217
|
-
expect(driver.errors()).toHaveLength(0)
|
|
218
|
-
|
|
219
|
-
await driver.dismiss()
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// ── 7. 상태 다이얼로그 (leader+s) ──
|
|
223
|
-
|
|
224
|
-
test("status dialog: leader+s opens", async () => {
|
|
225
|
-
await driver.leader("s")
|
|
226
|
-
await driver.wait(1500)
|
|
227
|
-
driver.snapshot("status")
|
|
228
|
-
expect(driver.errors()).toHaveLength(0)
|
|
229
|
-
|
|
230
|
-
await driver.dismiss()
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
// ── 8. 테마 선택 (leader+t) ──
|
|
234
|
-
|
|
235
|
-
test("theme selector: leader+t opens", async () => {
|
|
236
|
-
await driver.leader("t")
|
|
237
|
-
await driver.wait(1500)
|
|
238
|
-
driver.snapshot("theme")
|
|
239
|
-
expect(driver.errors()).toHaveLength(0)
|
|
240
|
-
|
|
241
|
-
await driver.dismiss()
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
// ── 9. 슬래시 명령어 ──
|
|
245
|
-
|
|
246
|
-
test("slash /help: opens help dialog", async () => {
|
|
247
|
-
driver.write("/help")
|
|
248
|
-
await driver.wait(500)
|
|
249
|
-
driver.write("\r") // enter
|
|
250
|
-
await driver.wait(1500)
|
|
251
|
-
driver.snapshot("slash-help")
|
|
252
|
-
expect(driver.errors()).toHaveLength(0)
|
|
253
|
-
|
|
254
|
-
await driver.dismiss()
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
test("slash /status: opens status", async () => {
|
|
258
|
-
driver.write("/status")
|
|
259
|
-
await driver.wait(500)
|
|
260
|
-
driver.write("\r")
|
|
261
|
-
await driver.wait(1500)
|
|
262
|
-
driver.snapshot("slash-status")
|
|
263
|
-
expect(driver.errors()).toHaveLength(0)
|
|
264
|
-
|
|
265
|
-
await driver.dismiss()
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
test("slash /models: opens model dialog", async () => {
|
|
269
|
-
driver.write("/models")
|
|
270
|
-
await driver.wait(500)
|
|
271
|
-
driver.write("\r")
|
|
272
|
-
await driver.wait(1500)
|
|
273
|
-
driver.snapshot("slash-models")
|
|
274
|
-
expect(driver.errors()).toHaveLength(0)
|
|
275
|
-
|
|
276
|
-
await driver.dismiss()
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
test("slash /agents: opens agent dialog", async () => {
|
|
280
|
-
driver.write("/agents")
|
|
281
|
-
await driver.wait(500)
|
|
282
|
-
driver.write("\r")
|
|
283
|
-
await driver.wait(1500)
|
|
284
|
-
driver.snapshot("slash-agents")
|
|
285
|
-
expect(driver.errors()).toHaveLength(0)
|
|
286
|
-
|
|
287
|
-
await driver.dismiss()
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
test("slash /themes: opens theme dialog", async () => {
|
|
291
|
-
driver.write("/themes")
|
|
292
|
-
await driver.wait(500)
|
|
293
|
-
driver.write("\r")
|
|
294
|
-
await driver.wait(1500)
|
|
295
|
-
driver.snapshot("slash-themes")
|
|
296
|
-
expect(driver.errors()).toHaveLength(0)
|
|
297
|
-
|
|
298
|
-
await driver.dismiss()
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
test("slash /sessions: opens session list", async () => {
|
|
302
|
-
driver.write("/sessions")
|
|
303
|
-
await driver.wait(500)
|
|
304
|
-
driver.write("\r")
|
|
305
|
-
await driver.wait(1500)
|
|
306
|
-
driver.snapshot("slash-sessions")
|
|
307
|
-
expect(driver.errors()).toHaveLength(0)
|
|
308
|
-
|
|
309
|
-
await driver.dismiss()
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
test("slash /connect: opens provider dialog", async () => {
|
|
313
|
-
driver.write("/connect")
|
|
314
|
-
await driver.wait(500)
|
|
315
|
-
driver.write("\r")
|
|
316
|
-
await driver.wait(1500)
|
|
317
|
-
driver.snapshot("slash-connect")
|
|
318
|
-
expect(driver.errors()).toHaveLength(0)
|
|
319
|
-
|
|
320
|
-
await driver.dismiss()
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
test("slash /mcps: opens MCP dialog", async () => {
|
|
324
|
-
driver.write("/mcps")
|
|
325
|
-
await driver.wait(500)
|
|
326
|
-
driver.write("\r")
|
|
327
|
-
await driver.wait(1500)
|
|
328
|
-
driver.snapshot("slash-mcps")
|
|
329
|
-
expect(driver.errors()).toHaveLength(0)
|
|
330
|
-
|
|
331
|
-
await driver.dismiss()
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
test("slash /variants: opens variant dialog", async () => {
|
|
335
|
-
driver.write("/variants")
|
|
336
|
-
await driver.wait(500)
|
|
337
|
-
driver.write("\r")
|
|
338
|
-
await driver.wait(1500)
|
|
339
|
-
driver.snapshot("slash-variants")
|
|
340
|
-
expect(driver.errors()).toHaveLength(0)
|
|
341
|
-
|
|
342
|
-
await driver.dismiss()
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
test("slash /new: navigates home", async () => {
|
|
346
|
-
driver.write("/new")
|
|
347
|
-
await driver.wait(500)
|
|
348
|
-
driver.write("\r")
|
|
349
|
-
await driver.wait(1000)
|
|
350
|
-
driver.snapshot("slash-new")
|
|
351
|
-
expect(driver.errors()).toHaveLength(0)
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
// ── 10. 프롬프트 입력 ──
|
|
355
|
-
|
|
356
|
-
test("prompt: typing and clearing", async () => {
|
|
357
|
-
driver.write("hello world")
|
|
358
|
-
await driver.wait(500)
|
|
359
|
-
driver.snapshot("prompt-typed")
|
|
360
|
-
expect(driver.errors()).toHaveLength(0)
|
|
361
|
-
|
|
362
|
-
// ctrl+c로 클리어
|
|
363
|
-
driver.write("\x03")
|
|
364
|
-
await driver.wait(300)
|
|
365
|
-
driver.snapshot("prompt-cleared")
|
|
366
|
-
expect(driver.errors()).toHaveLength(0)
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
// ── 11. 새 세션 (leader+n) ──
|
|
370
|
-
|
|
371
|
-
test("new session: leader+n navigates home", async () => {
|
|
372
|
-
await driver.leader("n")
|
|
373
|
-
await driver.wait(1000)
|
|
374
|
-
driver.snapshot("new-session")
|
|
375
|
-
expect(driver.errors()).toHaveLength(0)
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
// ── 12. 사이드바 토글 (leader+b) ──
|
|
379
|
-
|
|
380
|
-
test("sidebar: leader+b toggles", async () => {
|
|
381
|
-
await driver.leader("b")
|
|
382
|
-
await driver.wait(800)
|
|
383
|
-
driver.snapshot("sidebar-open")
|
|
384
|
-
expect(driver.errors()).toHaveLength(0)
|
|
385
|
-
|
|
386
|
-
// 다시 토글
|
|
387
|
-
await driver.leader("b")
|
|
388
|
-
await driver.wait(500)
|
|
389
|
-
driver.snapshot("sidebar-closed")
|
|
390
|
-
expect(driver.errors()).toHaveLength(0)
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
// ── 13. 스크롤 키 ──
|
|
394
|
-
|
|
395
|
-
test("scroll: page up/down work", async () => {
|
|
396
|
-
driver.write("\x1b[5~") // page up
|
|
397
|
-
await driver.wait(300)
|
|
398
|
-
driver.write("\x1b[6~") // page down
|
|
399
|
-
await driver.wait(300)
|
|
400
|
-
driver.snapshot("scroll")
|
|
401
|
-
expect(driver.errors()).toHaveLength(0)
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
// ── 14. 모델 사이클 (f2) ──
|
|
405
|
-
|
|
406
|
-
test("model cycle: f2 cycles models", async () => {
|
|
407
|
-
// f2 키 시퀀스
|
|
408
|
-
driver.write("\x1bOQ") // f2 (xterm)
|
|
409
|
-
await driver.wait(500)
|
|
410
|
-
driver.snapshot("model-cycle-f2")
|
|
411
|
-
expect(driver.errors()).toHaveLength(0)
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
// ── 15. 에이전트 사이클 (tab) ──
|
|
415
|
-
|
|
416
|
-
test("agent cycle: tab cycles agents", async () => {
|
|
417
|
-
driver.write("\t") // tab
|
|
418
|
-
await driver.wait(500)
|
|
419
|
-
driver.snapshot("agent-cycle-tab")
|
|
420
|
-
expect(driver.errors()).toHaveLength(0)
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
// ── 16. 세션 콤팩트 (leader+c) ──
|
|
424
|
-
|
|
425
|
-
test("compact: leader+c triggers compact", async () => {
|
|
426
|
-
await driver.leader("c")
|
|
427
|
-
await driver.wait(1000)
|
|
428
|
-
driver.snapshot("session-compact")
|
|
429
|
-
expect(driver.errors()).toHaveLength(0)
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
// ── 17. 세션 타임라인 (leader+g) ──
|
|
433
|
-
|
|
434
|
-
test("timeline: leader+g opens timeline", async () => {
|
|
435
|
-
await driver.leader("g")
|
|
436
|
-
await driver.wait(1000)
|
|
437
|
-
driver.snapshot("session-timeline")
|
|
438
|
-
expect(driver.errors()).toHaveLength(0)
|
|
439
|
-
|
|
440
|
-
await driver.dismiss()
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
// ── 18. 커서 이동 키 ──
|
|
444
|
-
|
|
445
|
-
test("cursor: arrow keys and home/end", async () => {
|
|
446
|
-
// 입력창에 텍스트 입력 후 커서 이동
|
|
447
|
-
driver.write("test input for cursor movement")
|
|
448
|
-
await driver.wait(300)
|
|
449
|
-
|
|
450
|
-
driver.write("\x1b[D") // left
|
|
451
|
-
await driver.wait(100)
|
|
452
|
-
driver.write("\x1b[C") // right
|
|
453
|
-
await driver.wait(100)
|
|
454
|
-
driver.write("\x1b[H") // home
|
|
455
|
-
await driver.wait(100)
|
|
456
|
-
driver.write("\x1b[F") // end
|
|
457
|
-
await driver.wait(100)
|
|
458
|
-
|
|
459
|
-
driver.snapshot("cursor-movement")
|
|
460
|
-
expect(driver.errors()).toHaveLength(0)
|
|
461
|
-
|
|
462
|
-
// 클리어
|
|
463
|
-
driver.write("\x03")
|
|
464
|
-
await driver.wait(300)
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
// ── 19. 슬래시 명령어 — 별칭 ──
|
|
468
|
-
|
|
469
|
-
test("slash /quit: alias for /exit", async () => {
|
|
470
|
-
driver.write("/quit")
|
|
471
|
-
await driver.wait(500)
|
|
472
|
-
driver.write("\r")
|
|
473
|
-
await driver.wait(2000)
|
|
474
|
-
driver.snapshot("slash-quit")
|
|
475
|
-
|
|
476
|
-
// /quit이 /exit과 동일하게 동작하면 TUI가 종료되어야 함
|
|
477
|
-
// 종료되면 에러 없이 프로세스가 끝남
|
|
478
|
-
const exited = await Promise.race([
|
|
479
|
-
new Promise<boolean>((resolve) => {
|
|
480
|
-
driver.pty.onExit(() => resolve(true))
|
|
481
|
-
setTimeout(() => resolve(false), 3000)
|
|
482
|
-
}),
|
|
483
|
-
])
|
|
484
|
-
|
|
485
|
-
if (exited) {
|
|
486
|
-
// 정상 종료 — 드라이버를 다시 만들 필요 없음 (이후 테스트 없음)
|
|
487
|
-
expect(driver.errors()).toHaveLength(0)
|
|
488
|
-
} else {
|
|
489
|
-
// 종료 안 됨 — 에스케이프 후 계속
|
|
490
|
-
await driver.dismiss()
|
|
491
|
-
expect(driver.errors()).toHaveLength(0)
|
|
492
|
-
}
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
// ── 20. 전체 스냅샷 검증 ──
|
|
496
|
-
|
|
497
|
-
test("all snapshots: no errors in any screen", async () => {
|
|
498
|
-
const allErrors: string[] = []
|
|
499
|
-
for (const snap of driver.snapshots) {
|
|
500
|
-
const snapErrors = hasError(snap.plain)
|
|
501
|
-
if (snapErrors.length > 0) {
|
|
502
|
-
allErrors.push(
|
|
503
|
-
`[${snap.name}]: ${snapErrors.join("; ")}`,
|
|
504
|
-
)
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// 디버그: 전체 스냅샷 로그 저장
|
|
509
|
-
const reportPath = path.join(import.meta.dir, ".tui-walkthrough-report.txt")
|
|
510
|
-
const report = driver.snapshots
|
|
511
|
-
.map(
|
|
512
|
-
(s) =>
|
|
513
|
-
`=== ${s.name} (${s.raw.length} bytes) ===\n${s.plain.slice(-500)}`,
|
|
514
|
-
)
|
|
515
|
-
.join("\n\n")
|
|
516
|
-
await Bun.write(reportPath, report)
|
|
517
|
-
|
|
518
|
-
expect(allErrors).toHaveLength(0)
|
|
519
|
-
})
|
|
520
|
-
})
|