vibe/tests/cli/smoke_binary.py
Laure Hugo e607ccbb00
v2.18.0 (#843)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Cyprien <courtot.c@gmail.com>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Jean Burellier <sheplu@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com>
Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: josephine-delas <57808586+josephine-delas@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-25 16:08:45 +02:00

425 lines
14 KiB
Python

#!/usr/bin/env python3
"""Smoke tests for the built vibe binary.
Usage: python tests/cli/smoke_binary.py <binary-dir>
Tests:
1. --version exits successfully
2. Normal interactive launch starts far enough to load the main Textual app
3. --setup starts far enough to load bundled setup/Textual assets
4. Programmatic mode without an API key fails with the expected config error
5. Runtime data files are present in the bundle
6. The relocated bundle can be launched from PATH
7. (Linux) No ELF binaries require executable stack (GNU_STACK RWE)
"""
from __future__ import annotations
import os
from pathlib import Path
import platform
import shutil
import struct
import subprocess
import sys
import tempfile
import time
from typing import NoReturn
def _fail(msg: str) -> NoReturn:
print(f"FAIL: {msg}", file=sys.stderr)
sys.exit(1)
def _isolated_env(vibe_home: Path) -> dict[str, str]:
env = os.environ.copy()
env["VIBE_HOME"] = str(vibe_home)
env["VIBE_TEST_DISABLE_KEYRING"] = "1"
env["TERM"] = env.get("TERM") or "xterm-256color"
env.pop("MISTRAL_API_KEY", None)
env.pop("VIBE_MISTRAL_API_KEY", None)
return env
def test_version(binary: Path) -> None:
result = subprocess.run(
[str(binary), "--version"], capture_output=True, text=True, timeout=30
)
if result.returncode != 0:
_fail(
f"--version exited with code {result.returncode}\nstderr: {result.stderr}"
)
print(f"PASS: --version -> {result.stdout.strip()}")
def test_interactive_launch_loads_bundled_assets(binary: Path) -> None:
if platform.system() == "Windows":
# Windows does not provide the Unix pty module used to drive Textual here.
print("SKIP: interactive pty smoke test (Windows)")
return
import pty
import select
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
workdir = tmp_path / "workdir"
workdir.mkdir()
master_fd, slave_fd = pty.openpty()
env = _isolated_env(tmp_path / "home")
env["MISTRAL_API_KEY"] = "smoke-test-mock-key"
proc = subprocess.Popen(
[str(binary), "--trust", "--workdir", str(workdir)],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
env=env,
text=False,
)
os.close(slave_fd)
output = bytearray()
deadline = time.monotonic() + 30
try:
while time.monotonic() < deadline:
if proc.poll() is not None:
break
readable, _, _ = select.select([master_fd], [], [], 0.2)
if not readable:
continue
chunk = os.read(master_fd, 4096)
if not chunk:
break
output.extend(chunk)
decoded = output.decode("utf-8", errors="replace")
if "Traceback" in decoded or "StylesheetError" in decoded:
_fail(decoded)
# This pins the Textual app title used as the main-app smoke marker.
if "\x1b]0;Vibe\x07" in decoded and len(output) > 4096:
print("PASS: interactive launch loaded bundled UI assets")
return
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=5)
os.close(master_fd)
_fail("interactive launch did not render expected UI output before timeout")
def test_setup_loads_bundled_assets(binary: Path) -> None:
if platform.system() == "Windows":
# Windows does not provide the Unix pty module used to drive Textual here.
print("SKIP: --setup pty smoke test (Windows)")
return
import pty
import select
with tempfile.TemporaryDirectory() as tmp:
workdir = Path(tmp) / "workdir"
workdir.mkdir()
master_fd, slave_fd = pty.openpty()
proc = subprocess.Popen(
[str(binary), "--setup", "--workdir", str(workdir)],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
env=_isolated_env(Path(tmp) / "home"),
text=False,
)
os.close(slave_fd)
output = bytearray()
deadline = time.monotonic() + 30
try:
while time.monotonic() < deadline:
if proc.poll() is not None:
break
readable, _, _ = select.select([master_fd], [], [], 0.2)
if not readable:
continue
chunk = os.read(master_fd, 4096)
if not chunk:
break
output.extend(chunk)
if b"Traceback" in output or b"StylesheetError" in output:
_fail(output.decode("utf-8", errors="replace"))
if b"Mistral" in output or b"API" in output or b"Welcome" in output:
print("PASS: --setup loaded bundled UI assets")
return
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=5)
os.close(master_fd)
_fail("--setup did not render expected setup UI text before timeout")
def test_programmatic_missing_api_key(binary: Path) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = subprocess.run(
[str(binary), "-p", "hello"],
capture_output=True,
env=_isolated_env(Path(tmp) / ".vibe"),
text=True,
timeout=30,
)
if result.returncode == 0:
_fail("programmatic mode without API key unexpectedly succeeded")
output = f"{result.stdout}\n{result.stderr}"
if "Traceback" in output:
_fail(f"programmatic mode raised a traceback:\n{output}")
# This pins the user-facing guidance for the missing API key path.
if "Set the environment variable" not in output:
_fail(f"missing expected API key guidance:\n{output}")
print("PASS: programmatic mode reports missing API key")
def test_bundled_runtime_files(binary_dir: Path) -> None:
bundle_root = binary_dir / "_internal" / "vibe"
source_root = Path(__file__).resolve().parents[2] / "vibe"
if not bundle_root.is_dir():
_fail(f"bundled vibe package not found at {bundle_root}")
if not source_root.is_dir():
_fail(f"source vibe package not found at {source_root}")
required_exact = [
"whats_new.md",
"cli/textual_ui/app.tcss",
"setup/onboarding/onboarding.tcss",
"setup/trusted_folders/trust_folder_dialog.tcss",
]
missing_exact = [
relative
for relative in required_exact
if not (bundle_root / relative).is_file()
]
if missing_exact:
lines = ["Missing required bundled runtime file(s):"]
lines.extend(f" - vibe/{relative}" for relative in missing_exact)
_fail("\n".join(lines))
mirrored_globs = [
"**/*.tcss",
"**/*.md",
"core/tools/builtins/*.py",
"core/skills/builtins/*.py",
]
for pattern in mirrored_globs:
source_files = {
path.relative_to(source_root)
for path in source_root.glob(pattern)
if path.is_file()
}
bundled_files = {
path.relative_to(bundle_root)
for path in bundle_root.glob(pattern)
if path.is_file()
}
missing = sorted(source_files - bundled_files)
if missing:
lines = [f"Bundle is missing runtime file(s) for pattern {pattern}:"]
lines.extend(f" - vibe/{path}" for path in missing)
_fail("\n".join(lines))
print("PASS: bundled runtime files are present")
def test_installed_bundle_launches(binary_dir: Path, binary_name: str) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
install_dir = tmp_path / "install" / "vibe"
workdir = tmp_path / "workdir"
vibe_home = tmp_path / "home"
workdir.mkdir()
shutil.copytree(binary_dir, install_dir)
installed_binary = install_dir / binary_name
if not installed_binary.exists():
_fail(f"installed binary not found at {installed_binary}")
if platform.system() != "Windows":
installed_binary.chmod(0o755)
env = _isolated_env(vibe_home)
env["PATH"] = f"{install_dir}{os.pathsep}{env.get('PATH', '')}"
command = binary_name
if platform.system() == "Windows":
# subprocess on Windows does not resolve executables through a PATH
# value supplied only via env, so resolve it against that PATH first.
if (resolved := shutil.which(binary_name, path=env["PATH"])) is None:
_fail(f"installed binary not found on PATH: {binary_name}")
command = resolved
result = subprocess.run(
[command, "--version"],
capture_output=True,
cwd=workdir,
env=env,
text=True,
timeout=30,
)
if result.returncode != 0:
_fail(
"installed bundle --version exited with code "
f"{result.returncode}\nstderr: {result.stderr}"
)
if "Traceback" in f"{result.stdout}\n{result.stderr}":
_fail(f"installed bundle raised a traceback:\n{result.stdout}\n{result.stderr}")
print(f"PASS: installed bundle launches from PATH -> {result.stdout.strip()}")
_PT_GNU_STACK = 0x6474E551
_PF_X = 0x1
def _has_executable_stack(filepath: Path) -> bool | None:
try:
with filepath.open("rb") as f:
magic = f.read(4)
if magic != b"\x7fELF":
return None
ei_class = f.read(1)[0]
ei_data = f.read(1)[0]
match ei_data:
case 1:
endian = "<"
case 2:
endian = ">"
case _:
return None
if ei_class == 2:
f.seek(32)
(e_phoff,) = struct.unpack(f"{endian}Q", f.read(8))
f.seek(54)
(e_phentsize,) = struct.unpack(f"{endian}H", f.read(2))
(e_phnum,) = struct.unpack(f"{endian}H", f.read(2))
for i in range(e_phnum):
f.seek(e_phoff + i * e_phentsize)
(p_type,) = struct.unpack(f"{endian}I", f.read(4))
(p_flags,) = struct.unpack(f"{endian}I", f.read(4))
if p_type == _PT_GNU_STACK:
return bool(p_flags & _PF_X)
elif ei_class == 1:
f.seek(28)
(e_phoff,) = struct.unpack(f"{endian}I", f.read(4))
f.seek(42)
(e_phentsize,) = struct.unpack(f"{endian}H", f.read(2))
(e_phnum,) = struct.unpack(f"{endian}H", f.read(2))
for i in range(e_phnum):
off = e_phoff + i * e_phentsize
f.seek(off)
(p_type,) = struct.unpack(f"{endian}I", f.read(4))
f.seek(off + 24)
(p_flags,) = struct.unpack(f"{endian}I", f.read(4))
if p_type == _PT_GNU_STACK:
return bool(p_flags & _PF_X)
return False
except (OSError, struct.error):
return None
def test_no_executable_stack(binary_dir: Path, binary_name: str) -> None:
if platform.system() != "Linux":
print("SKIP: executable stack check (not Linux)")
return
internal_dir = binary_dir / "_internal"
if not internal_dir.exists():
_fail(f"_internal directory not found at {internal_dir}")
violations: list[Path] = []
checked = 0
candidates = [binary_dir / binary_name, *internal_dir.rglob("*")]
for filepath in candidates:
if not filepath.is_file():
continue
result = _has_executable_stack(filepath)
if result is None:
continue
checked += 1
if result:
violations.append(filepath)
if violations:
lines = [
f"Found {len(violations)} ELF file(s) with executable stack "
f"(GNU_STACK RWE) out of {checked} checked.",
"",
"These will fail on SELinux-enforcing or hardened kernels:",
]
for violation in violations:
lines.append(f" - {violation.relative_to(binary_dir)}")
lines.append("")
lines.append("Fix: run 'patchelf --clear-execstack' on these files.")
_fail("\n".join(lines))
print(f"PASS: no executable stack in {checked} ELF files")
def main() -> None:
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <binary-dir>")
sys.exit(1)
binary_dir = Path(sys.argv[1])
binary_name = "vibe.exe" if platform.system() == "Windows" else "vibe"
binary = binary_dir / binary_name
if not binary.exists():
_fail(f"binary not found at {binary}")
if platform.system() != "Windows":
binary.chmod(0o755)
print(f"Testing binary: {binary}\n")
test_version(binary)
test_bundled_runtime_files(binary_dir)
test_installed_bundle_launches(binary_dir, binary_name)
test_no_executable_stack(binary_dir, binary_name)
test_interactive_launch_loads_bundled_assets(binary)
test_setup_loads_bundled_assets(binary)
test_programmatic_missing_api_key(binary)
print("\nAll smoke tests passed!")
if __name__ == "__main__":
main()