cmux/tests/test_shell_histfile_ghostty_zdotdir_regression.py
Lawrence Chen 9817d131f8
Release v1.23.0 (#31)
* Rename cmuxterm to cmux across entire codebase

- Rename GitHub repos: manaflow-ai/cmuxterm -> manaflow-ai/cmux,
  manaflow-ai/homebrew-cmuxterm -> manaflow-ai/homebrew-cmux
- Rename bundle IDs: com.cmuxterm.app -> com.cmux.app
- Rename CLI: CLI/cmuxterm.swift -> CLI/cmux.swift
- Rename homebrew submodule: homebrew-cmuxterm -> homebrew-cmux
- Update all socket paths: /tmp/cmuxterm*.sock -> /tmp/cmux*.sock
- Update all GitHub URLs, DMG names, Sparkle URLs
- Update all source files, scripts, tests, docs, CI workflows

* Bump version to 1.23.0
2026-02-09 15:30:43 -08:00

96 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""
Regression: GhosttyKit already injects zsh shell integration by setting ZDOTDIR
to Ghostty's own integration directory (and optionally preserving a user-set
ZDOTDIR in GHOSTTY_ZSH_ZDOTDIR).
cmux also injects its own zsh integration by setting ZDOTDIR to
Resources/shell-integration. If cmux incorrectly treats Ghostty's injected
ZDOTDIR as the "user" ZDOTDIR, zsh history will be isolated to the integration
directory rather than the user's HOME/ZDOTDIR, breaking cross-terminal history
and therefore zsh-autosuggestions.
This test simulates that stacked injection scenario and asserts HISTFILE ends
up at $HOME/.zsh_history (not inside Ghostty's integration directory).
"""
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
def _run_zsh_print_histfile(env: dict[str, str]) -> tuple[int, str]:
# A PTY is not required for this regression: we only need /etc/zshrc to run
# and set HISTFILE based on the restored ZDOTDIR/HOME.
result = subprocess.run(
["zsh", "-ic", 'print -r -- "$HISTFILE"'],
env=env,
capture_output=True,
text=True,
timeout=8,
)
return (result.returncode, (result.stdout or "") + (result.stderr or ""))
def main() -> int:
root = Path(__file__).resolve().parents[1]
cmux_wrapper_dir = root / "Resources" / "shell-integration"
ghostty_zsh_dir = root / "ghostty" / "src" / "shell-integration" / "zsh"
if not (cmux_wrapper_dir / ".zshenv").exists():
print(f"SKIP: missing cmux wrapper .zshenv at {cmux_wrapper_dir}")
return 0
if not (ghostty_zsh_dir / ".zshenv").exists():
print(f"SKIP: missing Ghostty zsh .zshenv at {ghostty_zsh_dir}")
return 0
base = Path("/tmp") / f"cmux_histfile_ghostty_stack_{os.getpid()}"
try:
shutil.rmtree(base, ignore_errors=True)
base.mkdir(parents=True, exist_ok=True)
home = base / "home"
home.mkdir(parents=True, exist_ok=True)
env = dict(os.environ)
env["HOME"] = str(home)
env.pop("HISTFILE", None)
# Keep this test focused and deterministic: don't run Ghostty's heavy zsh
# integration when executing under a PTY in CI/agent runs.
env.pop("GHOSTTY_RESOURCES_DIR", None)
env.pop("GHOSTTY_SHELL_FEATURES", None)
env.pop("GHOSTTY_BIN_DIR", None)
# Simulate the buggy situation: cmux stores Ghostty's injected ZDOTDIR
# as the "original" ZDOTDIR, then sets ZDOTDIR to its own wrapper.
env["CMUX_ORIGINAL_ZDOTDIR"] = str(ghostty_zsh_dir)
env["ZDOTDIR"] = str(cmux_wrapper_dir)
env["CMUX_SHELL_INTEGRATION"] = "0"
rc, out = _run_zsh_print_histfile(env)
if rc != 0:
print(f"FAIL: zsh exited non-zero rc={rc}")
return 1
lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
if not lines:
print("FAIL: no output captured from zsh")
return 1
seen = lines[-1]
expected = str(home / ".zsh_history")
if seen != expected:
print(f"FAIL: HISTFILE={seen!r}, expected {expected!r}")
print(f" cmux_wrapper_dir={cmux_wrapper_dir}")
print(f" ghostty_zsh_dir={ghostty_zsh_dir}")
return 1
print("PASS: HISTFILE resolves to user home history (not Ghostty integration dir)")
return 0
finally:
shutil.rmtree(base, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())