返回 DeepSeek-Reasonix
terminal_test.go
根目录 / desktop / terminal_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "errors"
6 "io"
7 "os"
8 "path/filepath"
9 "runtime"
10 "strconv"
11 "strings"
12 "sync"
13 "testing"
14 "time"
15
16 "reasonix/internal/config"
17 )
18
19 func TestResolveTerminalStartDirUsesFilesystemTypeAndContainsSymlinks(t *testing.T) {
20 root := t.TempDir()
21 dottedDir := filepath.Join(root, "config.d")
22 if err := os.Mkdir(dottedDir, 0o755); err != nil {
23 t.Fatal(err)
24 }
25 license := filepath.Join(root, "LICENSE")
26 if err := os.WriteFile(license, []byte("test"), 0o644); err != nil {
27 t.Fatal(err)
28 }
29 canonicalRoot, err := canonicalDirectory(root)
30 if err != nil {
31 t.Fatal(err)
32 }
33 canonicalDottedDir, err := canonicalDirectory(dottedDir)
34 if err != nil {
35 t.Fatal(err)
36 }
37
38 if got, err := resolveTerminalStartDir(root, "config.d"); err != nil || got != canonicalDottedDir {
39 t.Fatalf("dotted directory = %q, %v; want %q", got, err, canonicalDottedDir)
40 }
41 if got, err := resolveTerminalStartDir(root, "LICENSE"); err != nil || got != canonicalRoot {
42 t.Fatalf("extensionless file = %q, %v; want %q", got, err, canonicalRoot)
43 }
44 if _, err := resolveTerminalStartDir(root, "../outside"); !errors.Is(err, errTerminalOutside) {
45 t.Fatalf("parent traversal error = %v, want errTerminalOutside", err)
46 }
47 if _, err := resolveTerminalStartDir(root, root); !errors.Is(err, errTerminalOutside) {
48 t.Fatalf("absolute path error = %v, want errTerminalOutside", err)
49 }
50
51 outside := t.TempDir()
52 link := filepath.Join(root, "outside-link")
53 if err := os.Symlink(outside, link); err != nil {
54 t.Skipf("symlink unavailable: %v", err)
55 }
56 if _, err := resolveTerminalStartDir(root, "outside-link"); !errors.Is(err, errTerminalOutside) {
57 t.Fatalf("escaping directory symlink error = %v, want errTerminalOutside", err)
58 }
59 }
60
61 func TestResolveTerminalCommandTrustsOnlyUserConfigPath(t *testing.T) {
62 t.Setenv("REASONIX_HOME", t.TempDir())
63 t.Setenv("REASONIX_SAFE_MODE", "")
64 root := t.TempDir()
65 projectShell := testExecutable(t, root, "project-shell")
66 projectConfig := "[tools.shell]\nprefer = \"bash\"\npath = " + strconv.Quote(projectShell) + "\n"
67 if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte(projectConfig), 0o644); err != nil {
68 t.Fatal(err)
69 }
70
71 command, err := resolveTerminalCommand(root, "default")
72 if err != nil {
73 t.Fatal(err)
74 }
75 if command.path == projectShell {
76 t.Fatal("project reasonix.toml selected the integrated terminal executable")
77 }
78
79 userShell := testExecutable(t, t.TempDir(), "user-shell")
80 userConfig := "[tools.shell]\nprefer = \"bash\"\npath = " + strconv.Quote(userShell) + "\n"
81 userConfigPath := config.UserConfigPath()
82 if err := os.MkdirAll(filepath.Dir(userConfigPath), 0o755); err != nil {
83 t.Fatal(err)
84 }
85 if err := os.WriteFile(userConfigPath, []byte(userConfig), 0o600); err != nil {
86 t.Fatal(err)
87 }
88 command, err = resolveTerminalCommand(root, "default")
89 if err != nil {
90 t.Fatal(err)
91 }
92 if command.path != userShell {
93 t.Fatalf("user-configured shell = %q, want %q", command.path, userShell)
94 }
95 if _, err := resolveTerminalCommand(root, userShell); err == nil || !strings.Contains(err.Error(), "unsupported terminal shell") {
96 t.Fatalf("renderer path override error = %v, want unsupported shell", err)
97 }
98 }
99
100 func TestTerminalEnvironmentOverridesInheritedTerminalCapabilities(t *testing.T) {
101 env := terminalEnvironment([]string{
102 "PATH=/bin",
103 "TERM=dumb",
104 "colorterm=legacy",
105 "REASONIX_TEST=value",
106 })
107 joined := strings.Join(env, "\n")
108 if strings.Count(strings.ToUpper(joined), "TERM=") != 2 {
109 t.Fatalf("terminal environment contains duplicate TERM variables: %q", env)
110 }
111 if !strings.Contains(joined, "TERM=xterm-256color") || !strings.Contains(joined, "COLORTERM=truecolor") {
112 t.Fatalf("terminal capability overrides missing: %q", env)
113 }
114 if !strings.Contains(joined, "PATH=/bin") || !strings.Contains(joined, "REASONIX_TEST=value") {
115 t.Fatalf("terminal environment dropped unrelated variables: %q", env)
116 }
117 }
118
119 func testExecutable(t *testing.T, dir, name string) string {
120 t.Helper()
121 if runtime.GOOS == "windows" {
122 name += ".exe"
123 }
124 path := filepath.Join(dir, name)
125 if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
126 t.Fatal(err)
127 }
128 return path
129 }
130
131 func TestTerminalTargetRejectsStaleAndReadOnlyTabs(t *testing.T) {
132 app := NewApp()
133 root := t.TempDir()
134 tab := &WorkspaceTab{ID: "active", Scope: "project", WorkspaceRoot: root, ReadOnly: true}
135 app.tabs[tab.ID] = tab
136 app.tabOrder = []string{tab.ID}
137 app.activeTabID = tab.ID
138
139 view, err := app.TerminalWorkspaceForTab(tab.ID)
140 if err != nil {
141 t.Fatal(err)
142 }
143 if !view.ReadOnly || view.Sessions == nil || view.Shells == nil {
144 t.Fatalf("read-only workspace view = %+v, want non-nil arrays", view)
145 }
146 if _, err := app.CreateTerminalForTab(tab.ID, ".", "default"); err == nil || !strings.Contains(err.Error(), "read-only") {
147 t.Fatalf("read-only create error = %v", err)
148 }
149 if _, err := app.TerminalWorkspaceForTab("stale"); !errors.Is(err, errTerminalStaleTab) {
150 t.Fatalf("stale tab error = %v, want errTerminalStaleTab", err)
151 }
152 }
153
154 func TestTerminalTargetScopesSessionsToTheChatTab(t *testing.T) {
155 app := NewApp()
156 root := t.TempDir()
157 app.tabs["one"] = &WorkspaceTab{ID: "one", Scope: "project", WorkspaceRoot: root}
158 app.tabs["two"] = &WorkspaceTab{ID: "two", Scope: "project", WorkspaceRoot: root}
159 app.tabOrder = []string{"one", "two"}
160 app.activeTabID = "one"
161
162 first, err := app.terminalTargetForTab("one", false)
163 if err != nil {
164 t.Fatal(err)
165 }
166 app.activeTabID = "two"
167 second, err := app.terminalTargetForTab("two", false)
168 if err != nil {
169 t.Fatal(err)
170 }
171 if first.workspaceRoot != second.workspaceRoot {
172 t.Fatalf("same project root changed: %q != %q", first.workspaceRoot, second.workspaceRoot)
173 }
174 if first.workspaceKey == second.workspaceKey {
175 t.Fatalf("terminal scope key shared across chat tabs: %q", first.workspaceKey)
176 }
177 }
178
179 func TestEmptyTerminalWorkspaceViewSerializesArrays(t *testing.T) {
180 view := emptyTerminalWorkspaceView()
181 raw, err := json.Marshal(view)
182 if err != nil {
183 t.Fatal(err)
184 }
185 text := string(raw)
186 if !strings.Contains(text, `"sessions":[]`) || !strings.Contains(text, `"shells":[]`) {
187 t.Fatalf("terminal workspace JSON = %s, want [] arrays", text)
188 }
189 }
190
191 type fakeTerminalWait struct {
192 code int
193 err error
194 }
195
196 type fakeTerminalProcess struct {
197 waitResult chan fakeTerminalWait
198 closed chan struct{}
199 closeOnce sync.Once
200
201 mu sync.Mutex
202 writes []byte
203 resizes [][2]int
204 }
205
206 func newFakeTerminalProcess() *fakeTerminalProcess {
207 return &fakeTerminalProcess{
208 waitResult: make(chan fakeTerminalWait, 1),
209 closed: make(chan struct{}),
210 }
211 }
212
213 func (p *fakeTerminalProcess) Read([]byte) (int, error) {
214 <-p.closed
215 return 0, io.EOF
216 }
217
218 func (p *fakeTerminalProcess) Write(data []byte) (int, error) {
219 p.mu.Lock()
220 p.writes = append(p.writes, data...)
221 p.mu.Unlock()
222 return len(data), nil
223 }
224
225 func (p *fakeTerminalProcess) Resize(cols, rows int) error {
226 p.mu.Lock()
227 p.resizes = append(p.resizes, [2]int{cols, rows})
228 p.mu.Unlock()
229 return nil
230 }
231
232 func (p *fakeTerminalProcess) Wait() (int, error) {
233 select {
234 case result := <-p.waitResult:
235 p.closeOnce.Do(func() { close(p.closed) })
236 return result.code, result.err
237 case <-p.closed:
238 return -1, errors.New("closed")
239 }
240 }
241
242 func (p *fakeTerminalProcess) Close() error {
243 p.closeOnce.Do(func() { close(p.closed) })
244 return nil
245 }
246
247 type drainingTerminalProcess struct {
248 waitRelease chan struct{}
249 readRelease chan struct{}
250 closed chan struct{}
251 closeOnce sync.Once
252 }
253
254 func newDrainingTerminalProcess() *drainingTerminalProcess {
255 return &drainingTerminalProcess{
256 waitRelease: make(chan struct{}),
257 readRelease: make(chan struct{}),
258 closed: make(chan struct{}),
259 }
260 }
261
262 func (p *drainingTerminalProcess) Read(data []byte) (int, error) {
263 select {
264 case <-p.readRelease:
265 return copy(data, []byte("final output")), io.EOF
266 case <-p.closed:
267 return 0, io.EOF
268 }
269 }
270
271 func (p *drainingTerminalProcess) Write(data []byte) (int, error) { return len(data), nil }
272 func (p *drainingTerminalProcess) Resize(int, int) error { return nil }
273 func (p *drainingTerminalProcess) Wait() (int, error) {
274 <-p.waitRelease
275 return 0, nil
276 }
277 func (p *drainingTerminalProcess) Close() error {
278 p.closeOnce.Do(func() { close(p.closed) })
279 return nil
280 }
281
282 func TestTerminalManagerCountsConcurrentStartsTowardLimit(t *testing.T) {
283 manager := newTerminalManager(nil)
284 entered := make(chan struct{}, maxTerminalsPerWorkspace+1)
285 release := make(chan struct{})
286 manager.start = func(terminalStartSpec) (terminalProcess, error) {
287 entered <- struct{}{}
288 <-release
289 return newFakeTerminalProcess(), nil
290 }
291
292 type result struct{ err error }
293 results := make(chan result, maxTerminalsPerWorkspace+1)
294 for i := 0; i < maxTerminalsPerWorkspace+1; i++ {
295 go func() {
296 _, err := manager.create("tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"})
297 results <- result{err: err}
298 }()
299 }
300 for i := 0; i < maxTerminalsPerWorkspace; i++ {
301 select {
302 case <-entered:
303 case <-time.After(time.Second):
304 t.Fatal("terminal starts did not reach the concurrency barrier")
305 }
306 }
307 close(release)
308
309 succeeded, limited := 0, 0
310 for i := 0; i < maxTerminalsPerWorkspace+1; i++ {
311 result := <-results
312 switch {
313 case result.err == nil:
314 succeeded++
315 case strings.Contains(result.err.Error(), "session limit"):
316 limited++
317 default:
318 t.Fatalf("unexpected create error: %v", result.err)
319 }
320 }
321 if succeeded != maxTerminalsPerWorkspace || limited != 1 {
322 t.Fatalf("create results: succeeded=%d limited=%d", succeeded, limited)
323 }
324 manager.closeAll()
325 }
326
327 func TestTerminalManagerRejectsStartThatFinishesAfterTabClose(t *testing.T) {
328 manager := newTerminalManager(nil)
329 proc := newFakeTerminalProcess()
330 entered := make(chan struct{})
331 release := make(chan struct{})
332 manager.start = func(terminalStartSpec) (terminalProcess, error) {
333 close(entered)
334 <-release
335 return proc, nil
336 }
337
338 result := make(chan error, 1)
339 go func() {
340 _, err := manager.create("closing-tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"})
341 result <- err
342 }()
343
344 select {
345 case <-entered:
346 case <-time.After(time.Second):
347 t.Fatal("terminal start did not reach the concurrency barrier")
348 }
349 manager.closeForTab("closing-tab")
350 close(release)
351
352 select {
353 case err := <-result:
354 if !errors.Is(err, errTerminalStaleTab) {
355 t.Fatalf("create error = %v, want errTerminalStaleTab", err)
356 }
357 case <-time.After(time.Second):
358 t.Fatal("terminal create did not finish after tab close")
359 }
360 select {
361 case <-proc.closed:
362 case <-time.After(time.Second):
363 t.Fatal("terminal process started for a closed tab was not closed")
364 }
365 if got := manager.list("workspace"); len(got) != 0 {
366 t.Fatalf("closed tab registered terminal sessions: %+v", got)
367 }
368 if _, err := manager.create("closing-tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"}); !errors.Is(err, errTerminalStaleTab) {
369 t.Fatalf("create after tab close error = %v, want errTerminalStaleTab", err)
370 }
371 manager.closeAll()
372 }
373
374 func TestTerminalManagerRejectsStaleStartAfterTabGateReopens(t *testing.T) {
375 manager := newTerminalManager(nil)
376 proc := newFakeTerminalProcess()
377 entered := make(chan struct{})
378 release := make(chan struct{})
379 manager.start = func(terminalStartSpec) (terminalProcess, error) {
380 close(entered)
381 <-release
382 return proc, nil
383 }
384
385 result := make(chan error, 1)
386 go func() {
387 _, err := manager.create("rebinding-tab", "old-workspace", ".", terminalCommand{path: "shell", label: "shell"})
388 result <- err
389 }()
390
391 select {
392 case <-entered:
393 case <-time.After(time.Second):
394 t.Fatal("terminal start did not reach the concurrency barrier")
395 }
396 manager.closeForTab("rebinding-tab")
397 manager.reopenForTab("rebinding-tab")
398 close(release)
399
400 select {
401 case err := <-result:
402 if !errors.Is(err, errTerminalStaleTab) {
403 t.Fatalf("create error = %v, want errTerminalStaleTab", err)
404 }
405 case <-time.After(time.Second):
406 t.Fatal("terminal create did not finish after the tab gate reopened")
407 }
408 select {
409 case <-proc.closed:
410 case <-time.After(time.Second):
411 t.Fatal("stale terminal process was not closed after the tab gate reopened")
412 }
413 if got := manager.list("old-workspace"); len(got) != 0 {
414 t.Fatalf("stale tab registered terminal sessions: %+v", got)
415 }
416 manager.closeAll()
417 }
418
419 func TestTerminalManagerDrainsFinalOutputBeforePublishingExit(t *testing.T) {
420 manager := newTerminalManager(nil)
421 proc := newDrainingTerminalProcess()
422 manager.start = func(terminalStartSpec) (terminalProcess, error) { return proc, nil }
423 view, err := manager.create("tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"})
424 if err != nil {
425 t.Fatal(err)
426 }
427 manager.mu.Lock()
428 done := manager.sessions[view.ID].done
429 manager.mu.Unlock()
430
431 close(proc.waitRelease)
432 select {
433 case <-done:
434 t.Fatal("terminal exit completed before the reader drained final output")
435 case <-time.After(50 * time.Millisecond):
436 }
437 close(proc.readRelease)
438 select {
439 case <-done:
440 case <-time.After(time.Second):
441 t.Fatal("terminal exit did not finish after output drained")
442 }
443 if got := manager.snapshot("workspace", view.ID); got != "final output" {
444 t.Fatalf("final terminal snapshot = %q, want final output", got)
445 }
446 manager.closeAll()
447 }
448
449 func TestTerminalReadOnlyTransitionClosesAndReopensTheTabGate(t *testing.T) {
450 app := NewApp()
451 root := t.TempDir()
452 app.tabs["tab"] = &WorkspaceTab{ID: "tab", Scope: "project", WorkspaceRoot: root}
453 app.tabOrder = []string{"tab"}
454 app.activeTabID = "tab"
455
456 manager := newTerminalManager(nil)
457 app.terminals = manager
458 started := make([]*fakeTerminalProcess, 0, 2)
459 manager.start = func(terminalStartSpec) (terminalProcess, error) {
460 proc := newFakeTerminalProcess()
461 started = append(started, proc)
462 return proc, nil
463 }
464
465 if _, err := manager.create("tab", "workspace", root, terminalCommand{path: "shell", label: "shell"}); err != nil {
466 t.Fatal(err)
467 }
468 app.setTabReadOnly("tab", true)
469 if !app.tabs["tab"].ReadOnly {
470 t.Fatal("tab did not enter read-only mode")
471 }
472 select {
473 case <-started[0].closed:
474 default:
475 t.Fatal("entering read-only mode did not close the terminal process")
476 }
477 if got := manager.list("workspace"); len(got) != 0 {
478 t.Fatalf("read-only tab retained terminal sessions: %+v", got)
479 }
480 if _, err := manager.create("tab", "workspace", root, terminalCommand{path: "shell", label: "shell"}); !errors.Is(err, errTerminalStaleTab) {
481 t.Fatalf("create while read-only gate is closed = %v, want errTerminalStaleTab", err)
482 }
483
484 app.setTabReadOnly("tab", false)
485 if app.tabs["tab"].ReadOnly {
486 t.Fatal("tab did not return to writable mode")
487 }
488 if _, err := manager.create("tab", "workspace", root, terminalCommand{path: "shell", label: "shell"}); err != nil {
489 t.Fatalf("create after writable transition: %v", err)
490 }
491 manager.closeAll()
492 }
493
494 func TestTerminalWorkspaceRebindClosesOldSessionsAndReopensTheTabGate(t *testing.T) {
495 t.Setenv("REASONIX_HOME", t.TempDir())
496 app := NewApp()
497 oldRoot := t.TempDir()
498 newRoot := t.TempDir()
499 tab := &WorkspaceTab{
500 ID: "tab",
501 Scope: "project",
502 WorkspaceRoot: oldRoot,
503 SessionPath: filepath.Join(oldRoot, "old-session.jsonl"),
504 }
505 app.tabs["tab"] = tab
506 app.tabOrder = []string{"tab"}
507 app.activeTabID = "tab"
508
509 manager := newTerminalManager(nil)
510 app.terminals = manager
511 started := make([]*fakeTerminalProcess, 0, 2)
512 manager.start = func(terminalStartSpec) (terminalProcess, error) {
513 proc := newFakeTerminalProcess()
514 started = append(started, proc)
515 return proc, nil
516 }
517
518 oldTarget, err := app.terminalTargetForTab("tab", false)
519 if err != nil {
520 t.Fatal(err)
521 }
522 if _, err := manager.create("tab", oldTarget.workspaceKey, oldTarget.workspaceRoot, terminalCommand{path: "shell", label: "shell"}); err != nil {
523 t.Fatal(err)
524 }
525
526 app.applySessionBindingToTab(tab, sessionBinding{
527 path: filepath.Join(newRoot, "new-session.jsonl"),
528 scope: "project",
529 workspaceRoot: newRoot,
530 })
531
532 select {
533 case <-started[0].closed:
534 default:
535 t.Fatal("workspace rebind did not close the old terminal process")
536 }
537 if got := manager.list(oldTarget.workspaceKey); len(got) != 0 {
538 t.Fatalf("workspace rebind retained old terminal sessions: %+v", got)
539 }
540 newTarget, err := app.terminalTargetForTab("tab", false)
541 if err != nil {
542 t.Fatal(err)
543 }
544 if newTarget.workspaceKey == oldTarget.workspaceKey {
545 t.Fatalf("workspace rebind retained terminal scope %q", newTarget.workspaceKey)
546 }
547 if _, err := manager.create("tab", newTarget.workspaceKey, newTarget.workspaceRoot, terminalCommand{path: "shell", label: "shell"}); err != nil {
548 t.Fatalf("create after workspace rebind: %v", err)
549 }
550 manager.closeAll()
551 }
552
553 func TestTerminalManagerCloseAndNaturalExit(t *testing.T) {
554 t.Run("close cleans up process", func(t *testing.T) {
555 manager := newTerminalManager(nil)
556 proc := newFakeTerminalProcess()
557 manager.start = func(terminalStartSpec) (terminalProcess, error) { return proc, nil }
558 view, err := manager.create("tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"})
559 if err != nil {
560 t.Fatal(err)
561 }
562 if err := manager.closeTerminal("workspace", view.ID); err != nil {
563 t.Fatal(err)
564 }
565 select {
566 case <-proc.closed:
567 default:
568 t.Fatal("terminal process was not closed")
569 }
570 if got := manager.list("workspace"); len(got) != 0 {
571 t.Fatalf("sessions after close = %+v", got)
572 }
573 })
574
575 t.Run("natural exit updates view", func(t *testing.T) {
576 manager := newTerminalManager(nil)
577 proc := newFakeTerminalProcess()
578 manager.start = func(terminalStartSpec) (terminalProcess, error) { return proc, nil }
579 view, err := manager.create("tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"})
580 if err != nil {
581 t.Fatal(err)
582 }
583 manager.mu.Lock()
584 done := manager.sessions[view.ID].done
585 manager.mu.Unlock()
586 proc.waitResult <- fakeTerminalWait{code: 7, err: errors.New("exit status 7")}
587 select {
588 case <-done:
589 case <-time.After(time.Second):
590 t.Fatal("terminal wait loop did not finish")
591 }
592 sessions := manager.list("workspace")
593 if len(sessions) != 1 || sessions[0].Running || sessions[0].ExitCode == nil || *sessions[0].ExitCode != 7 {
594 t.Fatalf("session after exit = %+v", sessions)
595 }
596 manager.closeAll()
597 })
598 }
599
600 func TestTerminalManagerClosesOnlyTheClosingTabAndBoundsOutput(t *testing.T) {
601 manager := newTerminalManager(nil)
602 procs := make([]*fakeTerminalProcess, 0, 2)
603 manager.start = func(terminalStartSpec) (terminalProcess, error) {
604 proc := newFakeTerminalProcess()
605 procs = append(procs, proc)
606 return proc, nil
607 }
608 first, err := manager.create("tab-one", "tab-one\x00workspace", ".", terminalCommand{path: "shell", label: "shell"})
609 if err != nil {
610 t.Fatal(err)
611 }
612 second, err := manager.create("tab-two", "tab-two\x00workspace", ".", terminalCommand{path: "shell", label: "shell"})
613 if err != nil {
614 t.Fatal(err)
615 }
616
617 manager.mu.Lock()
618 manager.sessions[first.ID].output = appendTerminalSnapshot(
619 []byte(strings.Repeat("x", maxTerminalSnapshotBytes)),
620 []byte("y"),
621 )
622 manager.mu.Unlock()
623 if got := manager.snapshot("tab-one\x00workspace", first.ID); len(got) != maxTerminalSnapshotBytes || !strings.HasSuffix(got, "y") {
624 t.Fatalf("bounded snapshot = len %d suffix %q", len(got), got[len(got)-1:])
625 }
626 manager.mu.Lock()
627 manager.sessions[first.ID].output = appendTerminalSnapshot(nil, []byte(strings.Repeat("z", maxTerminalSnapshotBytes+1)))
628 manager.mu.Unlock()
629 if got := manager.snapshot("tab-one\x00workspace", first.ID); len(got) != maxTerminalSnapshotBytes || !strings.HasPrefix(got, "z") {
630 t.Fatalf("large bounded snapshot = len %d prefix %q", len(got), got[:1])
631 }
632
633 manager.closeForTab("tab-one")
634 select {
635 case <-procs[0].closed:
636 case <-time.After(time.Second):
637 t.Fatal("closing tab did not close its terminal")
638 }
639 select {
640 case <-procs[1].closed:
641 t.Fatal("closing tab closed another tab's terminal")
642 default:
643 }
644 if got := manager.list("tab-one\x00workspace"); len(got) != 0 {
645 t.Fatalf("closed tab sessions = %+v", got)
646 }
647 if got := manager.list("tab-two\x00workspace"); len(got) != 1 || got[0].ID != second.ID {
648 t.Fatalf("surviving tab sessions = %+v", got)
649 }
650 manager.closeAll()
651 }
652
652 lines GO