File size: 6,497 Bytes
b5b9c2e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | """Tests for parent→subparser flag propagation.
When flags like --yolo, -w, -s exist on both the parent parser and the 'chat'
subparser, placing the flag BEFORE the subcommand (e.g. 'hermes --yolo chat')
must not silently drop the flag value.
Regression test for: argparse subparser default=False overwriting parent's
parsed True when the same argument is defined on both parsers.
Fix: chat subparser uses default=argparse.SUPPRESS for all duplicated flags,
so the subparser only sets the attribute when the user explicitly provides it.
"""
import argparse
import os
import sys
from unittest.mock import patch
import pytest
def _build_parser():
"""Build the hermes argument parser from the real code.
We import the real main() and extract the parser it builds.
Since main() is a large function that does much more than parse args,
we replicate just the parser structure here to avoid side effects.
"""
parser = argparse.ArgumentParser(prog="hermes")
parser.add_argument("--resume", "-r", metavar="SESSION", default=None)
parser.add_argument(
"--continue", "-c", dest="continue_last", nargs="?",
const=True, default=None, metavar="SESSION_NAME",
)
parser.add_argument("--worktree", "-w", action="store_true", default=False)
parser.add_argument("--skills", "-s", action="append", default=None)
parser.add_argument("--yolo", action="store_true", default=False)
parser.add_argument("--pass-session-id", action="store_true", default=False)
subparsers = parser.add_subparsers(dest="command")
chat = subparsers.add_parser("chat")
# These MUST use argparse.SUPPRESS to avoid overwriting parent values
chat.add_argument("--yolo", action="store_true",
default=argparse.SUPPRESS)
chat.add_argument("--worktree", "-w", action="store_true",
default=argparse.SUPPRESS)
chat.add_argument("--skills", "-s", action="append",
default=argparse.SUPPRESS)
chat.add_argument("--pass-session-id", action="store_true",
default=argparse.SUPPRESS)
chat.add_argument("--resume", "-r", metavar="SESSION_ID",
default=argparse.SUPPRESS)
chat.add_argument(
"--continue", "-c", dest="continue_last", nargs="?",
const=True, default=argparse.SUPPRESS, metavar="SESSION_NAME",
)
return parser
class TestFlagBeforeSubcommand:
"""Flags placed before 'chat' must propagate through."""
def test_yolo_before_chat(self):
parser = _build_parser()
args = parser.parse_args(["--yolo", "chat"])
assert getattr(args, "yolo", False) is True
def test_worktree_before_chat(self):
parser = _build_parser()
args = parser.parse_args(["-w", "chat"])
assert getattr(args, "worktree", False) is True
def test_skills_before_chat(self):
parser = _build_parser()
args = parser.parse_args(["-s", "myskill", "chat"])
assert getattr(args, "skills", None) == ["myskill"]
def test_pass_session_id_before_chat(self):
parser = _build_parser()
args = parser.parse_args(["--pass-session-id", "chat"])
assert getattr(args, "pass_session_id", False) is True
def test_resume_before_chat(self):
parser = _build_parser()
args = parser.parse_args(["-r", "abc123", "chat"])
assert getattr(args, "resume", None) == "abc123"
class TestFlagAfterSubcommand:
"""Flags placed after 'chat' must still work."""
def test_yolo_after_chat(self):
parser = _build_parser()
args = parser.parse_args(["chat", "--yolo"])
assert getattr(args, "yolo", False) is True
def test_worktree_after_chat(self):
parser = _build_parser()
args = parser.parse_args(["chat", "-w"])
assert getattr(args, "worktree", False) is True
def test_skills_after_chat(self):
parser = _build_parser()
args = parser.parse_args(["chat", "-s", "myskill"])
assert getattr(args, "skills", None) == ["myskill"]
def test_resume_after_chat(self):
parser = _build_parser()
args = parser.parse_args(["chat", "-r", "abc123"])
assert getattr(args, "resume", None) == "abc123"
class TestNoSubcommandDefaults:
"""When no subcommand is given, flags must work and defaults must hold."""
def test_yolo_no_subcommand(self):
parser = _build_parser()
args = parser.parse_args(["--yolo"])
assert args.yolo is True
assert args.command is None
def test_defaults_no_flags(self):
parser = _build_parser()
args = parser.parse_args([])
assert getattr(args, "yolo", False) is False
assert getattr(args, "worktree", False) is False
assert getattr(args, "skills", None) is None
assert getattr(args, "resume", None) is None
def test_defaults_chat_no_flags(self):
parser = _build_parser()
args = parser.parse_args(["chat"])
# With SUPPRESS, these fall through to parent defaults
assert getattr(args, "yolo", False) is False
assert getattr(args, "worktree", False) is False
assert getattr(args, "skills", None) is None
class TestYoloEnvVar:
"""Verify --yolo sets HERMES_YOLO_MODE regardless of flag position.
This tests the actual cmd_chat logic pattern (getattr → os.environ).
"""
@pytest.fixture(autouse=True)
def _clean_env(self):
os.environ.pop("HERMES_YOLO_MODE", None)
yield
os.environ.pop("HERMES_YOLO_MODE", None)
def _simulate_cmd_chat_yolo_check(self, args):
"""Replicate the exact check from cmd_chat in main.py."""
if getattr(args, "yolo", False):
os.environ["HERMES_YOLO_MODE"] = "1"
def test_yolo_before_chat_sets_env(self):
parser = _build_parser()
args = parser.parse_args(["--yolo", "chat"])
self._simulate_cmd_chat_yolo_check(args)
assert os.environ.get("HERMES_YOLO_MODE") == "1"
def test_yolo_after_chat_sets_env(self):
parser = _build_parser()
args = parser.parse_args(["chat", "--yolo"])
self._simulate_cmd_chat_yolo_check(args)
assert os.environ.get("HERMES_YOLO_MODE") == "1"
def test_no_yolo_no_env(self):
parser = _build_parser()
args = parser.parse_args(["chat"])
self._simulate_cmd_chat_yolo_check(args)
assert os.environ.get("HERMES_YOLO_MODE") is None
|