Spaces:
Paused
Paused
| """Tests for the /branch (/fork) command — session branching. | |
| Verifies that: | |
| - Branching creates a new session with copied conversation history | |
| - The original session is preserved (ended with "branched" reason) | |
| - Auto-generated titles use lineage numbering | |
| - Custom branch names are used when provided | |
| - parent_session_id links are set correctly | |
| - Edge cases: empty conversation, missing session DB | |
| """ | |
| import os | |
| import uuid | |
| from datetime import datetime | |
| from pathlib import Path | |
| from unittest.mock import MagicMock, patch, PropertyMock | |
| import pytest | |
| def session_db(tmp_path): | |
| """Create a real SessionDB for testing.""" | |
| os.environ["HERMES_HOME"] = str(tmp_path / ".hermes") | |
| os.makedirs(tmp_path / ".hermes", exist_ok=True) | |
| from hermes_state import SessionDB | |
| db = SessionDB(db_path=tmp_path / ".hermes" / "test_sessions.db") | |
| yield db | |
| db.close() | |
| def cli_instance(tmp_path, session_db): | |
| """Create a minimal HermesCLI-like object for testing _handle_branch_command.""" | |
| # We'll mock the CLI enough to test the branch logic without full init | |
| from unittest.mock import MagicMock | |
| cli = MagicMock() | |
| cli._session_db = session_db | |
| cli.session_id = "20260403_120000_abc123" | |
| cli.model = "anthropic/claude-sonnet-4.6" | |
| cli.max_turns = 90 | |
| cli.reasoning_config = {"enabled": True, "effort": "medium"} | |
| cli.session_start = datetime.now() | |
| cli._pending_title = None | |
| cli._resumed = False | |
| cli.agent = None | |
| cli.conversation_history = [ | |
| {"role": "user", "content": "Hello, can you help me?"}, | |
| {"role": "assistant", "content": "Of course! How can I help?"}, | |
| {"role": "user", "content": "Write a Python function to sort a list."}, | |
| {"role": "assistant", "content": "def sort_list(lst): return sorted(lst)"}, | |
| ] | |
| # Create the original session in the DB | |
| session_db.create_session( | |
| session_id=cli.session_id, | |
| source="cli", | |
| model=cli.model, | |
| ) | |
| session_db.set_session_title(cli.session_id, "My Coding Session") | |
| return cli | |
| class TestBranchCommandCLI: | |
| """Test the /branch command logic for the CLI.""" | |
| def test_branch_creates_new_session(self, cli_instance, session_db): | |
| """Branching should create a new session in the DB.""" | |
| from cli import HermesCLI | |
| # Call the real method on the mock, using the real implementation | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| # Verify a new session was created | |
| assert cli_instance.session_id != "20260403_120000_abc123" | |
| new_session = session_db.get_session(cli_instance.session_id) | |
| assert new_session is not None | |
| def test_branch_copies_history(self, cli_instance, session_db): | |
| """Branching should copy all messages to the new session.""" | |
| from cli import HermesCLI | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| messages = session_db.get_messages_as_conversation(cli_instance.session_id) | |
| assert len(messages) == 4 # All 4 messages copied | |
| def test_branch_preserves_parent_link(self, cli_instance, session_db): | |
| """The new session should reference the original as parent.""" | |
| from cli import HermesCLI | |
| original_id = cli_instance.session_id | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| new_session = session_db.get_session(cli_instance.session_id) | |
| assert new_session["parent_session_id"] == original_id | |
| def test_branch_ends_original_session(self, cli_instance, session_db): | |
| """The original session should be marked as ended with 'branched' reason.""" | |
| from cli import HermesCLI | |
| original_id = cli_instance.session_id | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| original = session_db.get_session(original_id) | |
| assert original["end_reason"] == "branched" | |
| def test_branch_with_custom_name(self, cli_instance, session_db): | |
| """Custom branch name should be used as the title.""" | |
| from cli import HermesCLI | |
| HermesCLI._handle_branch_command(cli_instance, "/branch refactor approach") | |
| title = session_db.get_session_title(cli_instance.session_id) | |
| assert title == "refactor approach" | |
| def test_branch_auto_title_lineage(self, cli_instance, session_db): | |
| """Without a name, branch should auto-generate a title from the parent's title.""" | |
| from cli import HermesCLI | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| title = session_db.get_session_title(cli_instance.session_id) | |
| assert title == "My Coding Session #2" | |
| def test_branch_empty_conversation(self, cli_instance, session_db): | |
| """Branching with no history should show an error.""" | |
| from cli import HermesCLI | |
| cli_instance.conversation_history = [] | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| # session_id should not have changed | |
| assert cli_instance.session_id == "20260403_120000_abc123" | |
| def test_branch_no_session_db(self, cli_instance): | |
| """Branching without a session DB should show an error.""" | |
| from cli import HermesCLI | |
| cli_instance._session_db = None | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| # session_id should not have changed | |
| assert cli_instance.session_id == "20260403_120000_abc123" | |
| def test_branch_syncs_agent(self, cli_instance, session_db): | |
| """If an agent is active, branch should sync it to the new session.""" | |
| from cli import HermesCLI | |
| agent = MagicMock() | |
| agent._last_flushed_db_idx = 0 | |
| cli_instance.agent = agent | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| # Agent should have been updated | |
| assert agent.session_id == cli_instance.session_id | |
| assert agent.reset_session_state.called | |
| assert agent._last_flushed_db_idx == 4 # len(conversation_history) | |
| def test_branch_sets_resumed_flag(self, cli_instance, session_db): | |
| """Branch should set _resumed=True to prevent auto-title generation.""" | |
| from cli import HermesCLI | |
| HermesCLI._handle_branch_command(cli_instance, "/branch") | |
| assert cli_instance._resumed is True | |
| def test_fork_alias(self): | |
| """The /fork alias should resolve to 'branch'.""" | |
| from hermes_cli.commands import resolve_command | |
| result = resolve_command("fork") | |
| assert result is not None | |
| assert result.name == "branch" | |
| class TestBranchCommandDef: | |
| """Test the CommandDef registration for /branch.""" | |
| def test_branch_in_registry(self): | |
| """The branch command should be in the command registry.""" | |
| from hermes_cli.commands import COMMAND_REGISTRY | |
| names = [c.name for c in COMMAND_REGISTRY] | |
| assert "branch" in names | |
| def test_branch_has_fork_alias(self): | |
| """The branch command should have 'fork' as an alias.""" | |
| from hermes_cli.commands import COMMAND_REGISTRY | |
| branch = next(c for c in COMMAND_REGISTRY if c.name == "branch") | |
| assert "fork" in branch.aliases | |
| def test_branch_in_session_category(self): | |
| """The branch command should be in the Session category.""" | |
| from hermes_cli.commands import COMMAND_REGISTRY | |
| branch = next(c for c in COMMAND_REGISTRY if c.name == "branch") | |
| assert branch.category == "Session" | |