perf(termux): speed up non-tui cli startup
This commit is contained in:
@@ -275,6 +275,91 @@ def _is_termux_startup_environment(env: dict[str, str] | None = None) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_git_revision_fingerprint(repo_root: Path) -> str | None:
|
||||||
|
"""Return a cheap checkout fingerprint without spawning git."""
|
||||||
|
git_dir = repo_root / ".git"
|
||||||
|
try:
|
||||||
|
if git_dir.is_file():
|
||||||
|
for line in git_dir.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
key, _, value = line.partition(":")
|
||||||
|
if key.strip() == "gitdir" and value.strip():
|
||||||
|
git_dir = (repo_root / value.strip()).resolve()
|
||||||
|
break
|
||||||
|
head_file = git_dir / "HEAD"
|
||||||
|
head = head_file.read_text(encoding="utf-8", errors="replace").strip()
|
||||||
|
if head.startswith("ref:"):
|
||||||
|
ref = head.split(":", 1)[1].strip()
|
||||||
|
ref_file = git_dir / ref
|
||||||
|
if ref_file.exists():
|
||||||
|
return f"git:{ref}:{ref_file.read_text(encoding='utf-8', errors='replace').strip()}"
|
||||||
|
return f"git:HEAD:{head}"
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _termux_bundled_skills_fingerprint() -> str:
|
||||||
|
"""Cheap invalidation key for Termux bundled-skill startup sync."""
|
||||||
|
git_fp = _read_git_revision_fingerprint(PROJECT_ROOT)
|
||||||
|
if git_fp:
|
||||||
|
return git_fp
|
||||||
|
skills_dir = PROJECT_ROOT / "skills"
|
||||||
|
try:
|
||||||
|
stat = skills_dir.stat()
|
||||||
|
return f"skills:{__version__}:{__release_date__}:{stat.st_mtime_ns}:{stat.st_size}"
|
||||||
|
except OSError:
|
||||||
|
return f"skills:{__version__}:{__release_date__}:missing"
|
||||||
|
|
||||||
|
|
||||||
|
def _termux_bundled_skills_stamp_path() -> Path:
|
||||||
|
return get_hermes_home() / "skills" / ".termux_bundled_sync_stamp"
|
||||||
|
|
||||||
|
|
||||||
|
def _termux_bundled_skills_sync_needed() -> bool:
|
||||||
|
if not _is_termux_startup_environment():
|
||||||
|
return True
|
||||||
|
if os.environ.get("HERMES_TERMUX_FORCE_SKILLS_SYNC") == "1":
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
stamp = _termux_bundled_skills_stamp_path()
|
||||||
|
return stamp.read_text(encoding="utf-8").strip() != _termux_bundled_skills_fingerprint()
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_termux_bundled_skills_synced() -> None:
|
||||||
|
if not _is_termux_startup_environment():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
stamp = _termux_bundled_skills_stamp_path()
|
||||||
|
stamp.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
stamp.write_text(_termux_bundled_skills_fingerprint() + "\n", encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_bundled_skills_for_startup() -> bool:
|
||||||
|
"""Sync bundled skills, but skip unchanged Termux checkouts cheaply.
|
||||||
|
|
||||||
|
Hashing every bundled skill is safe but expensive on older Android
|
||||||
|
storage. The git/ref stamp keeps post-update correctness: a changed
|
||||||
|
checkout revision forces one real sync, then later starts skip it.
|
||||||
|
"""
|
||||||
|
if _is_termux_startup_environment() and not _termux_bundled_skills_sync_needed():
|
||||||
|
return False
|
||||||
|
|
||||||
|
from tools.skills_sync import sync_skills
|
||||||
|
|
||||||
|
sync_skills(quiet=True)
|
||||||
|
_mark_termux_bundled_skills_synced()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _termux_should_prefetch_update_check() -> bool:
|
||||||
|
if not _is_termux_startup_environment():
|
||||||
|
return True
|
||||||
|
return os.environ.get("HERMES_TERMUX_PREFETCH_UPDATES") == "1"
|
||||||
|
|
||||||
|
|
||||||
def _relative_time(ts) -> str:
|
def _relative_time(ts) -> str:
|
||||||
"""Format a timestamp as relative time (e.g., '2h ago', 'yesterday')."""
|
"""Format a timestamp as relative time (e.g., '2h ago', 'yesterday')."""
|
||||||
if not ts:
|
if not ts:
|
||||||
@@ -1523,19 +1608,20 @@ def cmd_chat(args):
|
|||||||
print("You can run 'hermes setup' at any time to configure.")
|
print("You can run 'hermes setup' at any time to configure.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Start update check in background (runs while other init happens)
|
# Start update check in background (runs while other init happens).
|
||||||
try:
|
# On Termux this imports rich/prompt_toolkit in the foreground and then
|
||||||
from hermes_cli.banner import prefetch_update_check
|
# competes for CPU on single-core devices, so keep it opt-in there.
|
||||||
|
if _termux_should_prefetch_update_check():
|
||||||
|
try:
|
||||||
|
from hermes_cli.banner import prefetch_update_check
|
||||||
|
|
||||||
prefetch_update_check()
|
prefetch_update_check()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Sync bundled skills on every CLI launch (fast -- skips unchanged skills)
|
# Sync bundled skills on every CLI launch (fast -- skips unchanged skills)
|
||||||
try:
|
try:
|
||||||
from tools.skills_sync import sync_skills
|
_sync_bundled_skills_for_startup()
|
||||||
|
|
||||||
sync_skills(quiet=True)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -5971,8 +6057,7 @@ def cmd_import(args):
|
|||||||
run_import(args)
|
run_import(args)
|
||||||
|
|
||||||
|
|
||||||
def cmd_version(args):
|
def _print_version_info(*, check_updates: bool = True) -> None:
|
||||||
"""Show version."""
|
|
||||||
print(f"Hermes Agent v{__version__} ({__release_date__})")
|
print(f"Hermes Agent v{__version__} ({__release_date__})")
|
||||||
print(f"Project: {PROJECT_ROOT}")
|
print(f"Project: {PROJECT_ROOT}")
|
||||||
|
|
||||||
@@ -5992,6 +6077,9 @@ def cmd_version(args):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
print("OpenAI SDK: Not installed")
|
print("OpenAI SDK: Not installed")
|
||||||
|
|
||||||
|
if not check_updates:
|
||||||
|
return
|
||||||
|
|
||||||
# Show update status (synchronous — acceptable since user asked for version info)
|
# Show update status (synchronous — acceptable since user asked for version info)
|
||||||
try:
|
try:
|
||||||
from hermes_cli.banner import check_for_updates
|
from hermes_cli.banner import check_for_updates
|
||||||
@@ -6010,6 +6098,11 @@ def cmd_version(args):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_version(args):
|
||||||
|
"""Show version."""
|
||||||
|
_print_version_info(check_updates=True)
|
||||||
|
|
||||||
|
|
||||||
def cmd_uninstall(args):
|
def cmd_uninstall(args):
|
||||||
"""Uninstall Hermes Agent."""
|
"""Uninstall Hermes Agent."""
|
||||||
_require_tty("uninstall")
|
_require_tty("uninstall")
|
||||||
@@ -10515,6 +10608,137 @@ def _plugin_cli_discovery_needed() -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
_AGENT_COMMANDS = {None, "chat", "acp", "rl"}
|
||||||
|
_AGENT_SUBCOMMANDS = {
|
||||||
|
"cron": ("cron_command", {"run", "tick"}),
|
||||||
|
"gateway": ("gateway_command", {"run"}),
|
||||||
|
"mcp": ("mcp_action", {"serve"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_agent_startup(args) -> None:
|
||||||
|
"""Discover plugins/MCP/hooks for commands that can run an agent turn."""
|
||||||
|
_sub_attr, _sub_set = _AGENT_SUBCOMMANDS.get(args.command, (None, None))
|
||||||
|
if not (
|
||||||
|
args.command in _AGENT_COMMANDS
|
||||||
|
or (_sub_attr and getattr(args, _sub_attr, None) in _sub_set)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
_accept_hooks = bool(getattr(args, "accept_hooks", False))
|
||||||
|
try:
|
||||||
|
from hermes_cli.plugins import discover_plugins
|
||||||
|
|
||||||
|
discover_plugins()
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"plugin discovery failed at CLI startup",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# MCP tool discovery — no event loop running in CLI/TUI startup,
|
||||||
|
# so inline is safe. Moved here from model_tools.py module scope
|
||||||
|
# to avoid freezing the gateway's event loop on its first message
|
||||||
|
# via the same lazy import path (#16856).
|
||||||
|
from tools.mcp_tool import discover_mcp_tools
|
||||||
|
|
||||||
|
discover_mcp_tools()
|
||||||
|
except Exception:
|
||||||
|
logger.debug(
|
||||||
|
"MCP tool discovery failed at CLI startup",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
from hermes_cli.config import load_config
|
||||||
|
from agent.shell_hooks import register_from_config
|
||||||
|
|
||||||
|
register_from_config(load_config(), accept_hooks=_accept_hooks)
|
||||||
|
except Exception:
|
||||||
|
logger.debug(
|
||||||
|
"shell-hook registration failed at CLI startup",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_chat_arg_defaults(args) -> None:
|
||||||
|
for attr, default in [
|
||||||
|
("query", None),
|
||||||
|
("model", None),
|
||||||
|
("provider", None),
|
||||||
|
("toolsets", None),
|
||||||
|
("verbose", False),
|
||||||
|
("resume", None),
|
||||||
|
("continue_last", None),
|
||||||
|
("worktree", False),
|
||||||
|
]:
|
||||||
|
if not hasattr(args, attr):
|
||||||
|
setattr(args, attr, default)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_termux_fast_version_argv(argv: list[str]) -> bool:
|
||||||
|
return argv in (["--version"], ["-V"], ["version"])
|
||||||
|
|
||||||
|
|
||||||
|
def _try_termux_fast_cli_launch() -> bool:
|
||||||
|
"""Run obvious Termux non-TUI chat/oneshot/version paths on a light parser."""
|
||||||
|
if not _is_termux_startup_environment():
|
||||||
|
return False
|
||||||
|
if os.environ.get("HERMES_TERMUX_DISABLE_FAST_CLI") == "1":
|
||||||
|
return False
|
||||||
|
|
||||||
|
argv = sys.argv[1:]
|
||||||
|
if "-h" in argv or "--help" in argv:
|
||||||
|
return False
|
||||||
|
if os.environ.get("HERMES_TUI") == "1" or "--tui" in argv:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if _is_termux_fast_version_argv(argv):
|
||||||
|
_print_version_info(check_updates=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
first = _first_positional_argv()
|
||||||
|
has_oneshot = any(
|
||||||
|
arg == "-z" or arg == "--oneshot" or arg.startswith("--oneshot=")
|
||||||
|
for arg in argv
|
||||||
|
)
|
||||||
|
if not has_oneshot and first not in {None, "chat"}:
|
||||||
|
return False
|
||||||
|
|
||||||
|
from hermes_cli._parser import build_top_level_parser
|
||||||
|
|
||||||
|
parser, _subparsers, chat_parser = build_top_level_parser()
|
||||||
|
chat_parser.set_defaults(func=cmd_chat)
|
||||||
|
args = parser.parse_args(_coalesce_session_name_args(argv))
|
||||||
|
|
||||||
|
if getattr(args, "version", False):
|
||||||
|
_print_version_info(check_updates=False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if getattr(args, "oneshot", None):
|
||||||
|
_prepare_agent_startup(args)
|
||||||
|
from hermes_cli.oneshot import run_oneshot
|
||||||
|
|
||||||
|
sys.exit(
|
||||||
|
run_oneshot(
|
||||||
|
args.oneshot,
|
||||||
|
model=getattr(args, "model", None),
|
||||||
|
provider=getattr(args, "provider", None),
|
||||||
|
toolsets=getattr(args, "toolsets", None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (args.resume or args.continue_last) and args.command is None:
|
||||||
|
args.command = "chat"
|
||||||
|
|
||||||
|
if args.command in {None, "chat"}:
|
||||||
|
_set_chat_arg_defaults(args)
|
||||||
|
_prepare_agent_startup(args)
|
||||||
|
cmd_chat(args)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _try_termux_fast_tui_launch() -> bool:
|
def _try_termux_fast_tui_launch() -> bool:
|
||||||
"""Launch obvious Termux TUI invocations before building every subparser.
|
"""Launch obvious Termux TUI invocations before building every subparser.
|
||||||
|
|
||||||
@@ -10575,6 +10799,8 @@ def main():
|
|||||||
|
|
||||||
if _try_termux_fast_tui_launch():
|
if _try_termux_fast_tui_launch():
|
||||||
return
|
return
|
||||||
|
if _try_termux_fast_cli_launch():
|
||||||
|
return
|
||||||
|
|
||||||
from hermes_cli._parser import build_top_level_parser
|
from hermes_cli._parser import build_top_level_parser
|
||||||
|
|
||||||
@@ -13373,51 +13599,7 @@ Examples:
|
|||||||
# so introspection/management commands (hermes hooks list, cron
|
# so introspection/management commands (hermes hooks list, cron
|
||||||
# list, gateway status, mcp add, ...) don't pay discovery cost or
|
# list, gateway status, mcp add, ...) don't pay discovery cost or
|
||||||
# trigger consent prompts for hooks the user is still inspecting.
|
# trigger consent prompts for hooks the user is still inspecting.
|
||||||
# Groups with mixed admin/CRUD vs. agent-running entries narrow via
|
_prepare_agent_startup(args)
|
||||||
# the nested subcommand (dest varies by parser).
|
|
||||||
_AGENT_COMMANDS = {None, "chat", "acp", "rl"}
|
|
||||||
_AGENT_SUBCOMMANDS = {
|
|
||||||
"cron": ("cron_command", {"run", "tick"}),
|
|
||||||
"gateway": ("gateway_command", {"run"}),
|
|
||||||
"mcp": ("mcp_action", {"serve"}),
|
|
||||||
}
|
|
||||||
_sub_attr, _sub_set = _AGENT_SUBCOMMANDS.get(args.command, (None, None))
|
|
||||||
if args.command in _AGENT_COMMANDS or (
|
|
||||||
_sub_attr and getattr(args, _sub_attr, None) in _sub_set
|
|
||||||
):
|
|
||||||
_accept_hooks = bool(getattr(args, "accept_hooks", False))
|
|
||||||
try:
|
|
||||||
from hermes_cli.plugins import discover_plugins
|
|
||||||
|
|
||||||
discover_plugins()
|
|
||||||
except Exception:
|
|
||||||
logger.warning(
|
|
||||||
"plugin discovery failed at CLI startup",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
# MCP tool discovery — no event loop running in CLI/TUI startup,
|
|
||||||
# so inline is safe. Moved here from model_tools.py module scope
|
|
||||||
# to avoid freezing the gateway's event loop on its first message
|
|
||||||
# via the same lazy import path (#16856).
|
|
||||||
from tools.mcp_tool import discover_mcp_tools
|
|
||||||
|
|
||||||
discover_mcp_tools()
|
|
||||||
except Exception:
|
|
||||||
logger.debug(
|
|
||||||
"MCP tool discovery failed at CLI startup",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
from hermes_cli.config import load_config
|
|
||||||
from agent.shell_hooks import register_from_config
|
|
||||||
|
|
||||||
register_from_config(load_config(), accept_hooks=_accept_hooks)
|
|
||||||
except Exception:
|
|
||||||
logger.debug(
|
|
||||||
"shell-hook registration failed at CLI startup",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handle top-level --oneshot / -z: single-shot mode, stdout = final
|
# Handle top-level --oneshot / -z: single-shot mode, stdout = final
|
||||||
# response only, nothing else. Bypasses cli.py entirely.
|
# response only, nothing else. Bypasses cli.py entirely.
|
||||||
|
|||||||
@@ -283,6 +283,151 @@ def test_fast_tui_launch_is_termux_only(monkeypatch, main_mod):
|
|||||||
assert main_mod._try_termux_fast_tui_launch() is False
|
assert main_mod._try_termux_fast_tui_launch() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_fast_cli_launch_chat_uses_light_parser(monkeypatch, main_mod):
|
||||||
|
captured = {}
|
||||||
|
prepared = []
|
||||||
|
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.delenv("HERMES_TUI", raising=False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys, "argv", ["hermes", "chat", "-q", "hello", "--toolsets", "web,terminal"]
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_mod, "_prepare_agent_startup", lambda args: prepared.append(args.command)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_mod,
|
||||||
|
"cmd_chat",
|
||||||
|
lambda args: captured.update(
|
||||||
|
{"query": args.query, "toolsets": args.toolsets, "command": args.command}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert main_mod._try_termux_fast_cli_launch() is True
|
||||||
|
assert prepared == ["chat"]
|
||||||
|
assert captured == {
|
||||||
|
"query": "hello",
|
||||||
|
"toolsets": "web,terminal",
|
||||||
|
"command": "chat",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod):
|
||||||
|
captured = {}
|
||||||
|
prepared = []
|
||||||
|
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.delenv("HERMES_TUI", raising=False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
["hermes", "-z", "hello", "--model", "gpt-test", "--provider", "openai"],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_mod, "_prepare_agent_startup", lambda args: prepared.append(args.command)
|
||||||
|
)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
sys.modules,
|
||||||
|
"hermes_cli.oneshot",
|
||||||
|
types.SimpleNamespace(
|
||||||
|
run_oneshot=lambda prompt, **kwargs: captured.update(
|
||||||
|
{"prompt": prompt, **kwargs}
|
||||||
|
)
|
||||||
|
or 17
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc:
|
||||||
|
main_mod._try_termux_fast_cli_launch()
|
||||||
|
|
||||||
|
assert exc.value.code == 17
|
||||||
|
assert prepared == [None]
|
||||||
|
assert captured == {
|
||||||
|
"prompt": "hello",
|
||||||
|
"model": "gpt-test",
|
||||||
|
"provider": "openai",
|
||||||
|
"toolsets": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_fast_cli_launch_version_skips_update_check(monkeypatch, main_mod):
|
||||||
|
captured = []
|
||||||
|
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.delenv("HERMES_TUI", raising=False)
|
||||||
|
monkeypatch.setattr(sys, "argv", ["hermes", "version"])
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_mod, "_print_version_info", lambda *, check_updates: captured.append(check_updates)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert main_mod._try_termux_fast_cli_launch() is True
|
||||||
|
assert captured == [False]
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_fast_cli_launch_skips_help(monkeypatch, main_mod):
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.delenv("HERMES_TUI", raising=False)
|
||||||
|
monkeypatch.setattr(sys, "argv", ["hermes", "chat", "--help"])
|
||||||
|
|
||||||
|
assert main_mod._try_termux_fast_cli_launch() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_fast_cli_launch_can_be_disabled(monkeypatch, main_mod):
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.setenv("HERMES_TERMUX_DISABLE_FAST_CLI", "1")
|
||||||
|
monkeypatch.delenv("HERMES_TUI", raising=False)
|
||||||
|
monkeypatch.setattr(sys, "argv", ["hermes", "version"])
|
||||||
|
|
||||||
|
assert main_mod._try_termux_fast_cli_launch() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_bundled_skills_stamp_controls_sync(monkeypatch, tmp_path, main_mod):
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.setattr(main_mod, "get_hermes_home", lambda: tmp_path)
|
||||||
|
monkeypatch.setattr(main_mod, "_termux_bundled_skills_fingerprint", lambda: "fp1")
|
||||||
|
|
||||||
|
assert main_mod._termux_bundled_skills_sync_needed() is True
|
||||||
|
main_mod._mark_termux_bundled_skills_synced()
|
||||||
|
assert main_mod._termux_bundled_skills_sync_needed() is False
|
||||||
|
|
||||||
|
monkeypatch.setenv("HERMES_TERMUX_FORCE_SKILLS_SYNC", "1")
|
||||||
|
assert main_mod._termux_bundled_skills_sync_needed() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_skips_bundled_skill_sync_when_stamp_fresh(monkeypatch, tmp_path, main_mod):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.setattr(main_mod, "get_hermes_home", lambda: tmp_path)
|
||||||
|
monkeypatch.setattr(main_mod, "_termux_bundled_skills_fingerprint", lambda: "fp1")
|
||||||
|
main_mod._mark_termux_bundled_skills_synced()
|
||||||
|
monkeypatch.setitem(
|
||||||
|
sys.modules,
|
||||||
|
"tools.skills_sync",
|
||||||
|
types.SimpleNamespace(sync_skills=lambda quiet: calls.append(quiet)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert main_mod._sync_bundled_skills_for_startup() is False
|
||||||
|
assert calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_termux_forced_bundled_skill_sync_runs(monkeypatch, tmp_path, main_mod):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||||
|
monkeypatch.setenv("HERMES_TERMUX_FORCE_SKILLS_SYNC", "1")
|
||||||
|
monkeypatch.setattr(main_mod, "get_hermes_home", lambda: tmp_path)
|
||||||
|
monkeypatch.setattr(main_mod, "_termux_bundled_skills_fingerprint", lambda: "fp1")
|
||||||
|
monkeypatch.setitem(
|
||||||
|
sys.modules,
|
||||||
|
"tools.skills_sync",
|
||||||
|
types.SimpleNamespace(sync_skills=lambda quiet: calls.append(quiet)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert main_mod._sync_bundled_skills_for_startup() is True
|
||||||
|
assert calls == [True]
|
||||||
|
|
||||||
|
|
||||||
def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
|
def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user