Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

feat(tests): introduce pexpect for TUI integration testing#295

Closed
Xiang-Pan wants to merge 1 commit intoasheshgoplani:mainfrom
Xiang-Pan:feature/pexpect-tui-tests
Closed

feat(tests): introduce pexpect for TUI integration testing#295
Xiang-Pan wants to merge 1 commit intoasheshgoplani:mainfrom
Xiang-Pan:feature/pexpect-tui-tests

Conversation

@Xiang-Pan
Copy link
Contributor

Summary

  • Adds a proof-of-concept pexpect-based test suite (tests/tui/) that launches the real agent-deck binary in a pseudo-terminal and validates TUI behavior through keyboard interaction and screen output assertions
  • Fills the testing gap between unit-level Bubble Tea component tests (which call Go functions directly) and the existing repterm-based E2E tests (which only exercise CLI/JSON commands, not the interactive TUI)
  • Adds make test-tui target for running the suite

Motivation

The current test suite has excellent coverage at two levels:

Level Tool What it tests
Unit Go testing + testify Individual component state, View() output, message handling
E2E repterm (TypeScript) CLI commands (add --quick --json, session start) over SSH

Missing layer: Nobody launches the binary, renders the Bubble Tea TUI in a real terminal, presses keys, and verifies what appears on screen. Bugs in initialization, terminal capability negotiation, ANSI rendering, and cross-component keyboard routing are invisible to the existing tests.

pexpect is the standard tool for this class of testing — it spawns a process in a PTY, sends keystrokes, and pattern-matches on terminal output. It is mature (20+ years), well-documented, and widely used for CLI/TUI testing in projects like GDB, IPython, and Fish shell.

What's included (proof-of-concept)

tests/tui/
├── conftest.py          # Fixtures: auto-build binary, spawn_deck() factory
├── requirements.txt     # pexpect + pytest
└── test_basic_tui.py    # 7 smoke tests

Tests:

  1. --help prints usage and exits 0
  2. version exits 0
  3. TUI renders initial home screen (matches "Agent Deck" / "No sessions" / help bar)
  4. ? key opens help overlay
  5. q key exits cleanly
  6. n key opens new-session dialog
  7. Escape closes the dialog

Fixtures (conftest.py):

  • Builds binary via make build once per pytest session
  • spawn_deck() factory spawns the binary with AGENTDECK_PROFILE=_tui_test isolation (same safety pattern used by Go tests) and a throwaway $HOME
  • Auto-kills child processes on teardown

Usage

make test-tui          # build + install deps + run
# or manually:
cd tests/tui && pip install -r requirements.txt && pytest -v .

Alternatives considered

Option Pros Cons
pexpect (this PR) Mature, PTY-native, pattern matching, cross-platform (Linux/macOS) Python dependency, no Windows
goexpect (Go) Same language Less mature, awkward API for TUI testing
vhs (Charm) Built for Bubble Tea demos Designed for recording, not assertions; YAML-based
Extend repterm (TS) Reuse existing E2E infra repterm is oriented toward command output, not interactive TUI
teatest (Go, Charm) First-party Bubble Tea testing Only tests the tea.Model in-process, doesn't exercise real terminal rendering

Test plan

  • Run make test-tui on Linux — all 7 tests pass
  • Run on macOS — pexpect supports both platforms
  • Verify tests are isolated (no side effects on real sessions via AGENTDECK_PROFILE=_tui_test)
  • Discuss: should test-tui be added to make ci / lefthook pre-push?
  • Discuss: scope of future tests (settings panel, MCP dialog, search, session lifecycle)

🤖 Generated with Claude Code

Add a proof-of-concept test suite using Python's pexpect to drive the
real agent-deck binary in a pseudo-terminal. This fills the gap between
unit-level Bubble Tea tests and the existing repterm-based E2E tests by
exercising the actual TUI rendering and keyboard interaction flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 6, 2026 06:03
@Xiang-Pan Xiang-Pan closed this Mar 6, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a proof-of-concept Python/pexpect integration test suite that exercises the real agent-deck binary in a pseudo-terminal to validate interactive TUI behavior (bridging the gap between in-process Bubble Tea unit tests and CLI-oriented E2E tests).

Changes:

  • Added tests/tui/ pytest + pexpect smoke tests for basic TUI rendering and key interactions.
  • Added pytest fixtures to build the binary and spawn agent-deck in a PTY with isolated HOME/profile.
  • Added make test-tui target to run the TUI test suite.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
tests/tui/test_basic_tui.py Adds pexpect-driven smoke tests for help/version and basic TUI interactions.
tests/tui/conftest.py Adds session build + PTY spawn fixtures with environment isolation.
tests/tui/requirements.txt Pins pexpect and pytest dependencies for the suite.
Makefile Adds a test-tui target to install deps and run pytest.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.



class TestVersionFlag:
"""Sanity: --version exits cleanly."""
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says this is testing --version, but the test actually invokes the version subcommand. Either update the docstring/name to match the version command, or change the invocation to --version so the intent is consistent.

Suggested change
"""Sanity: --version exits cleanly."""
"""Sanity: 'version' subcommand exits cleanly."""

Copilot uses AI. Check for mistakes.

# Run pexpect-based TUI tests (requires Python 3 + pexpect)
test-tui: build
@cd tests/tui && pip install -q -r requirements.txt && pytest -v .
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test-tui target uses pip/pytest directly, which can pick up the wrong Python (or install into the global environment). Prefer python3 -m pip ... and python3 -m pytest ... (optionally with --user or a venv) to ensure consistent interpreter selection and avoid polluting system site-packages.

Suggested change
@cd tests/tui && pip install -q -r requirements.txt && pytest -v .
@cd tests/tui && python3 -m pip install -q -r requirements.txt && python3 -m pytest -v .

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
import shutil
import subprocess
import tempfile
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shutil and tempfile are imported but never used in this file. Removing unused imports will keep the fixture module clean and avoids failing stricter linters.

Suggested change
import shutil
import subprocess
import tempfile
import subprocess

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +23
def _ensure_binary():
"""Build the binary if it doesn't exist or is stale."""
if not os.path.isfile(BINARY_PATH):
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _ensure_binary() docstring claims it rebuilds when the binary is "stale", but the implementation only checks for existence. Either update the docstring or add a staleness check (e.g., compare mtimes vs. relevant Go sources) so the behavior matches the comment.

Suggested change
def _ensure_binary():
"""Build the binary if it doesn't exist or is stale."""
if not os.path.isfile(BINARY_PATH):
def _binary_is_stale() -> bool:
"""Return True if the agent-deck binary is missing or older than any Go sources."""
if not os.path.isfile(BINARY_PATH):
return True
binary_mtime = os.path.getmtime(BINARY_PATH)
for root, _dirs, files in os.walk(PROJECT_ROOT):
# Skip the build output directory when scanning for source files.
if os.path.commonpath([root, BUILD_DIR]) == BUILD_DIR:
continue
for filename in files:
if filename.endswith(".go"):
source_path = os.path.join(root, filename)
if os.path.getmtime(source_path) > binary_mtime:
return True
return False
def _ensure_binary():
"""Build the binary if it doesn't exist or is stale."""
if _binary_is_stale():

Copilot uses AI. Check for mistakes.
# Isolate test runs from the user's real profile / data.
env["AGENTDECK_PROFILE"] = "_tui_test"
# Provide a throwaway home so config files don't collide.
env["HOME"] = str(tmp_path)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spawn_deck() launches the TUI without disabling update checks. By default, agent-deck does a GitHub release check before starting the TUI (up to a 10s HTTP timeout on a cold cache), but the tests only wait 5s for the first render—this can make the suite flaky or hang behind an update prompt. Consider writing a minimal ~/.agent-deck/config.toml into the temporary HOME to set [updates] check_enabled=false (and set check_interval_hours to a non-zero value so it isn’t defaulted back to true) to make launches deterministic/offline-safe.

Suggested change
env["HOME"] = str(tmp_path)
env["HOME"] = str(tmp_path)
# Disable online update checks for deterministic, offline-safe test runs.
# This writes ~/.agent-deck/config.toml inside the temporary HOME.
config_dir = tmp_path / ".agent-deck"
config_dir.mkdir(parents=True, exist_ok=True)
config_path = config_dir / "config.toml"
if not config_path.exists():
config_path.write_text(
"[updates]\n"
"check_enabled = false\n"
"check_interval_hours = 24\n",
encoding="utf-8",
)

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +55
env = os.environ.copy()
# Isolate test runs from the user's real profile / data.
env["AGENTDECK_PROFILE"] = "_tui_test"
# Provide a throwaway home so config files don't collide.
env["HOME"] = str(tmp_path)
if env_overrides:
env.update(env_overrides)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid false failures when running the test suite inside a tmux session, consider unsetting TMUX in the spawned process environment. The binary blocks launching the TUI when it detects it’s running inside an agentdeck_* tmux session, which can cause these tests to exit early on developers’ machines.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +60
def _factory(extra_args: str = "", env_overrides: dict | None = None):
env = os.environ.copy()
# Isolate test runs from the user's real profile / data.
env["AGENTDECK_PROFILE"] = "_tui_test"
# Provide a throwaway home so config files don't collide.
env["HOME"] = str(tmp_path)
if env_overrides:
env.update(env_overrides)

cmd = f"{BINARY_PATH} {extra_args}".strip()
child = pexpect.spawn(
cmd,
encoding="utf-8",
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building the command via string concatenation (f"{BINARY_PATH} {extra_args}") makes argument quoting brittle (e.g., paths with spaces) and makes it harder to extend the helper. Prefer accepting extra_args as a list[str]/tuple[str, ...] (or *args) and passing them to pexpect.spawn as separate argv elements so quoting is handled correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +10
import pytest


Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest is imported but not used in this test module. Removing the unused import keeps the file tidy and avoids tripping linting tools.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants