package terminal_test import ( "context" "os" "strings" "testing" "time" "iop/apps/node/internal/terminal" ) func TestTerminalSessionWritesPrompt(t *testing.T) { opts := terminal.Options{ Command: "sh", } core, err := terminal.StartSession(context.Background(), opts) if err != nil { t.Fatalf("failed to start terminal session: %v", err) } defer core.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Write prompt "echo hello-world" (WritePrompt will append \r) if err := core.WritePrompt(ctx, "echo hello-world"); err != nil { t.Fatalf("failed to write prompt: %v", err) } // Wait for the output to propagate var tail string success := false for { select { case <-ctx.Done(): t.Fatalf("timeout waiting for output. tail was: %q", tail) case <-time.After(100 * time.Millisecond): tail = core.Snapshot().Tail if strings.Contains(tail, "hello-world") { success = true break } } if success { break } } } func TestTerminalSessionSnapshot(t *testing.T) { opts := terminal.Options{ Command: "sh", } core, err := terminal.StartSession(context.Background(), opts) if err != nil { t.Fatalf("failed to start terminal session: %v", err) } defer core.Close() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := core.WritePrompt(ctx, "echo test-snapshot"); err != nil { t.Fatalf("failed to write prompt: %v", err) } time.Sleep(200 * time.Millisecond) snap := core.Snapshot() if !strings.Contains(snap.Tail, "test-snapshot") { t.Errorf("snapshot tail does not contain test-snapshot: %q", snap.Tail) } } func TestTerminalSessionCloseIsIdempotent(t *testing.T) { opts := terminal.Options{ Command: "sh", } core, err := terminal.StartSession(context.Background(), opts) if err != nil { t.Fatalf("failed to start terminal session: %v", err) } if err := core.Close(); err != nil { t.Errorf("first close failed: %v", err) } if err := core.Close(); err != nil { t.Errorf("second close failed: %v", err) } } func TestTerminalSessionResizeValidatesBounds(t *testing.T) { opts := terminal.Options{ Command: "sh", } core, err := terminal.StartSession(context.Background(), opts) if err != nil { t.Fatalf("failed to start terminal session: %v", err) } defer core.Close() if err := core.Resize(0, 80); err == nil { t.Error("expected error when resizing with rows=0") } if err := core.Resize(80, 0); err == nil { t.Error("expected error when resizing with cols=0") } if err := core.Resize(40, 120); err != nil { t.Errorf("failed to resize with valid dimensions: %v", err) } } func TestTerminalSessionRejectsWriteAfterClose(t *testing.T) { opts := terminal.Options{ Command: "sh", } core, err := terminal.StartSession(context.Background(), opts) if err != nil { t.Fatalf("failed to start terminal session: %v", err) } if err := core.Close(); err != nil { t.Fatalf("close failed: %v", err) } ctx := context.Background() if err := core.WritePrompt(ctx, "ls"); err == nil { t.Error("expected WritePrompt to reject writing after close") } if err := core.WriteInput(ctx, []byte("ls\n")); err == nil { t.Error("expected WriteInput to reject writing after close") } } func TestTerminalSessionSignalAfterClose(t *testing.T) { opts := terminal.Options{ Command: "sh", } core, err := terminal.StartSession(context.Background(), opts) if err != nil { t.Fatalf("failed to start terminal session: %v", err) } if err := core.Close(); err != nil { t.Fatalf("close failed: %v", err) } if err := core.Signal(os.Interrupt); err == nil { t.Error("expected Signal to reject signal after close") } } func TestTerminalSessionCwd(t *testing.T) { tmpDir := t.TempDir() opts := terminal.Options{ Command: "sh", Dir: tmpDir, } core, err := terminal.StartSession(context.Background(), opts) if err != nil { t.Fatalf("failed to start terminal session: %v", err) } defer core.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := core.WritePrompt(ctx, "pwd"); err != nil { t.Fatalf("failed to write prompt: %v", err) } resolvedTmpDir, err := os.Readlink(tmpDir) if err != nil { resolvedTmpDir = tmpDir } var tail string success := false for { select { case <-ctx.Done(): t.Fatalf("timeout waiting for pwd output. tail was: %q", tail) case <-time.After(100 * time.Millisecond): tail = core.Snapshot().Tail if strings.Contains(tail, tmpDir) || strings.Contains(tail, resolvedTmpDir) { success = true break } } if success { break } } }