feat: add CLI entry point, command modules, and zsh completion
- CLI with context detection, config merging, VM blocking - Command modules: dotfiles, packages, setup, remote, dev, projects - Zsh completion with declarative command/subcommand/flag structure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,49 +1,32 @@
|
||||
"""Tests for CLI routing and command registration."""
|
||||
"""Tests for CLI."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _clean_env():
|
||||
"""Return env dict without DF_* variables that trigger enter's guard."""
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith("DF_")}
|
||||
env["FLOW_SKIP_SUDO_REFRESH"] = "1"
|
||||
return env
|
||||
|
||||
|
||||
def test_version():
|
||||
def test_version_flag():
|
||||
"""Test --version flag works."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "--version"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "0.1.0" in result.stdout
|
||||
assert "flow" in result.stdout
|
||||
|
||||
|
||||
def test_help():
|
||||
def test_help_flag():
|
||||
"""Test --help shows commands."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "enter" in result.stdout
|
||||
assert "dev" in result.stdout
|
||||
assert "dotfiles" in result.stdout
|
||||
assert "bootstrap" in result.stdout
|
||||
assert "package" in result.stdout
|
||||
assert "sync" in result.stdout
|
||||
assert "completion" in result.stdout
|
||||
|
||||
|
||||
def test_enter_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "enter", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "target" in result.stdout
|
||||
assert "--dry-run" in result.stdout
|
||||
assert "packages" in result.stdout
|
||||
assert "setup" in result.stdout
|
||||
|
||||
|
||||
def test_dotfiles_help():
|
||||
@@ -52,136 +35,14 @@ def test_dotfiles_help():
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "init" in result.stdout
|
||||
assert "link" in result.stdout
|
||||
assert "unlink" in result.stdout
|
||||
assert "undo" in result.stdout
|
||||
assert "status" in result.stdout
|
||||
assert "sync" in result.stdout
|
||||
assert "repo" in result.stdout
|
||||
|
||||
|
||||
def test_dotfiles_help_without_sudo_in_path():
|
||||
env = _clean_env()
|
||||
env["PATH"] = os.path.dirname(sys.executable)
|
||||
|
||||
def test_packages_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "dotfiles", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "dotfiles" in result.stdout
|
||||
|
||||
|
||||
def test_bootstrap_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "bootstrap", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "run" in result.stdout
|
||||
assert "list" in result.stdout
|
||||
assert "show" in result.stdout
|
||||
assert "packages" in result.stdout
|
||||
|
||||
|
||||
def test_package_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "package", "--help"],
|
||||
[sys.executable, "-m", "flow", "packages", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "install" in result.stdout
|
||||
assert "list" in result.stdout
|
||||
assert "remove" in result.stdout
|
||||
|
||||
|
||||
def test_sync_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "sync", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "check" in result.stdout
|
||||
assert "fetch" in result.stdout
|
||||
assert "summary" in result.stdout
|
||||
|
||||
|
||||
def test_dev_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "dev", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "create" in result.stdout
|
||||
assert "exec" in result.stdout
|
||||
assert "connect" in result.stdout
|
||||
assert "list" in result.stdout
|
||||
assert "stop" in result.stdout
|
||||
assert "remove" in result.stdout
|
||||
assert "respawn" in result.stdout
|
||||
|
||||
|
||||
def test_enter_dry_run():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "enter", "--dry-run", "personal@orb"],
|
||||
capture_output=True, text=True, env=_clean_env(),
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "ssh" in result.stdout
|
||||
assert "personal.orb" in result.stdout
|
||||
assert "tmux" in result.stdout
|
||||
|
||||
|
||||
def test_enter_dry_run_no_tmux():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "enter", "--dry-run", "--no-tmux", "personal@orb"],
|
||||
capture_output=True, text=True, env=_clean_env(),
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "ssh" in result.stdout
|
||||
assert "tmux" not in result.stdout
|
||||
|
||||
|
||||
def test_enter_dry_run_with_user():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "enter", "--dry-run", "root@personal@orb"],
|
||||
capture_output=True, text=True, env=_clean_env(),
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "root@personal.orb" in result.stdout
|
||||
|
||||
|
||||
def test_enter_dry_run_shows_terminfo_hint_for_ghostty():
|
||||
env = _clean_env()
|
||||
env["TERM"] = "xterm-ghostty"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "enter", "--dry-run", "personal@orb"],
|
||||
capture_output=True, text=True, env=env,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "flow will not install or modify terminfo" in result.stdout
|
||||
assert "infocmp -x xterm-ghostty | ssh" in result.stdout
|
||||
|
||||
|
||||
def test_aliases():
|
||||
"""Test that command aliases work."""
|
||||
for alias, cmd in [("dot", "dotfiles"), ("pkg", "package"), ("setup", "bootstrap")]:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", alias, "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0, f"Alias '{alias}' failed"
|
||||
|
||||
|
||||
def test_dev_remove_alias():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "flow", "dev", "rm", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -1,130 +1,43 @@
|
||||
"""Tests for flow.commands.completion dynamic suggestions."""
|
||||
"""Tests for zsh completion."""
|
||||
|
||||
from flow.commands import completion
|
||||
from flow.commands.completion import complete
|
||||
|
||||
|
||||
def test_complete_top_level():
|
||||
result = complete(["flow", ""], 1)
|
||||
assert "dotfiles" in result
|
||||
assert "packages" in result
|
||||
assert "setup" in result
|
||||
assert "remote" in result
|
||||
assert "dev" in result
|
||||
assert "projects" in result
|
||||
|
||||
|
||||
def test_complete_top_level_prefix():
|
||||
out = completion.complete(["flow", "do"], 2)
|
||||
assert "dotfiles" in out
|
||||
assert "dot" in out
|
||||
result = complete(["flow", "do"], 1)
|
||||
assert result == ["dotfiles"]
|
||||
|
||||
|
||||
def test_complete_bootstrap_profiles(monkeypatch):
|
||||
monkeypatch.setattr(completion, "_list_bootstrap_profiles", lambda: ["linux-vm", "macos-host"])
|
||||
out = completion.complete(["flow", "bootstrap", "show", "li"], 4)
|
||||
assert out == ["linux-vm"]
|
||||
def test_complete_dotfiles_subcommands():
|
||||
result = complete(["flow", "dotfiles", ""], 2)
|
||||
assert "link" in result
|
||||
assert "unlink" in result
|
||||
assert "status" in result
|
||||
|
||||
|
||||
def test_complete_bootstrap_packages_options(monkeypatch):
|
||||
monkeypatch.setattr(completion, "_list_bootstrap_profiles", lambda: ["linux-vm", "macos-host"])
|
||||
out = completion.complete(["flow", "bootstrap", "packages", "--p"], 4)
|
||||
assert out == ["--profile"]
|
||||
|
||||
out = completion.complete(["flow", "bootstrap", "packages", "--profile", "m"], 5)
|
||||
assert out == ["macos-host"]
|
||||
def test_complete_dotfiles_link_flags():
|
||||
result = complete(["flow", "dotfiles", "link", "--"], 3)
|
||||
assert "--profile" in result
|
||||
assert "--dry-run" in result
|
||||
|
||||
|
||||
def test_complete_package_install(monkeypatch):
|
||||
monkeypatch.setattr(completion, "_list_manifest_packages", lambda: ["neovim", "fzf"])
|
||||
out = completion.complete(["flow", "package", "install", "n"], 4)
|
||||
assert out == ["neovim"]
|
||||
def test_complete_unknown_command():
|
||||
result = complete(["flow", "unknown", ""], 2)
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_complete_package_remove(monkeypatch):
|
||||
monkeypatch.setattr(completion, "_list_installed_packages", lambda: ["hello", "jq"])
|
||||
out = completion.complete(["flow", "package", "remove", "h"], 4)
|
||||
assert out == ["hello"]
|
||||
|
||||
|
||||
def test_list_manifest_packages_is_consistent_for_list_and_dict_forms(monkeypatch):
|
||||
manifests = [
|
||||
{
|
||||
"packages": [
|
||||
{"name": "neovim", "type": "binary"},
|
||||
{"name": "ripgrep", "type": "pkg"},
|
||||
{"name": "fzf", "type": "binary"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"packages": {
|
||||
"neovim": {"type": "binary"},
|
||||
"ripgrep": {"type": "pkg"},
|
||||
"fzf": {"type": "binary"},
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
monkeypatch.setattr(completion, "_safe_manifest", lambda: manifests.pop(0))
|
||||
|
||||
from_list = completion._list_manifest_packages()
|
||||
from_dict = completion._list_manifest_packages()
|
||||
|
||||
assert from_list == ["fzf", "neovim"]
|
||||
assert from_dict == ["fzf", "neovim"]
|
||||
|
||||
|
||||
def test_list_manifest_packages_uses_mapping_key_when_name_missing(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
completion,
|
||||
"_safe_manifest",
|
||||
lambda: {"packages": {"bat": {"type": "binary"}, "git": {"type": "pkg"}}},
|
||||
)
|
||||
|
||||
assert completion._list_manifest_packages() == ["bat"]
|
||||
|
||||
|
||||
def test_complete_dotfiles_profile_value(monkeypatch):
|
||||
monkeypatch.setattr(completion, "_list_dotfiles_profiles", lambda: ["work", "personal"])
|
||||
out = completion.complete(["flow", "dotfiles", "link", "--profile", "w"], 5)
|
||||
assert out == ["work"]
|
||||
|
||||
|
||||
def test_complete_dotfiles_repo_subcommands():
|
||||
out = completion.complete(["flow", "dotfiles", "repo", "p"], 4)
|
||||
assert out == ["pull", "push"]
|
||||
|
||||
|
||||
def test_complete_dotfiles_top_level_includes_undo():
|
||||
out = completion.complete(["flow", "dotfiles", "u"], 3)
|
||||
assert out == ["undo", "unlink"]
|
||||
|
||||
|
||||
def test_complete_dotfiles_modules_subcommands():
|
||||
out = completion.complete(["flow", "dotfiles", "modules", "s"], 4)
|
||||
assert out == ["sync"]
|
||||
|
||||
|
||||
def test_complete_dotfiles_modules_profile_value(monkeypatch):
|
||||
monkeypatch.setattr(completion, "_list_dotfiles_profiles", lambda: ["work", "personal"])
|
||||
out = completion.complete(["flow", "dotfiles", "modules", "list", "--profile", "w"], 6)
|
||||
assert out == ["work"]
|
||||
|
||||
|
||||
def test_complete_enter_targets(monkeypatch):
|
||||
monkeypatch.setattr(completion, "_list_targets", lambda: ["personal@orb", "work@ec2"])
|
||||
out = completion.complete(["flow", "enter", "p"], 3)
|
||||
assert out == ["personal@orb"]
|
||||
|
||||
|
||||
def test_complete_dev_subcommands():
|
||||
out = completion.complete(["flow", "dev", "c"], 3)
|
||||
assert out == ["connect", "create"]
|
||||
|
||||
|
||||
def test_complete_completion_subcommands():
|
||||
out = completion.complete(["flow", "completion", "i"], 3)
|
||||
assert out == ["install-zsh"]
|
||||
|
||||
|
||||
def test_rc_snippet_is_idempotent(tmp_path):
|
||||
rc_path = tmp_path / ".zshrc"
|
||||
completion_dir = tmp_path / "completions"
|
||||
|
||||
first = completion._ensure_rc_snippet(rc_path, completion_dir)
|
||||
second = completion._ensure_rc_snippet(rc_path, completion_dir)
|
||||
|
||||
assert first is True
|
||||
assert second is False
|
||||
text = rc_path.read_text()
|
||||
assert text.count(completion.ZSH_RC_START) == 1
|
||||
assert text.count(completion.ZSH_RC_END) == 1
|
||||
def test_complete_packages_subcommands():
|
||||
result = complete(["flow", "packages", ""], 2)
|
||||
assert "install" in result
|
||||
assert "remove" in result
|
||||
assert "list" in result
|
||||
|
||||
Reference in New Issue
Block a user