feat(system): test(seedgo): improve line coverage — architecture_check + proof standards + diagnostics (203 new tests)
Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
@@ -446,6 +446,36 @@
|
||||
"file": "tests/test_monitoring_handlers.py",
|
||||
"standard": "architecture",
|
||||
"reason": "Test file lives in tests/ by convention, not in the 3-layer apps/ structure. This is standard for all prax test files."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_log_watcher.py",
|
||||
"standard": "architecture",
|
||||
"reason": "Test file lives in tests/ by convention, not in the 3-layer apps/ structure. This is standard for all prax test files."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_log_watcher.py",
|
||||
"standard": "documentation",
|
||||
"reason": "Pre-existing test functions follow pytest naming conventions (test_*). Original 35 functions were written without docstrings per project test style."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_log_watcher.py",
|
||||
"standard": "naming",
|
||||
"reason": "Pre-existing _RealFSHandler is a private test stub with underscore prefix indicating internal use. Renaming would not improve clarity."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_monitor_module.py",
|
||||
"standard": "architecture",
|
||||
"reason": "Test file lives in tests/ by convention, not in the 3-layer apps/ structure. This is standard for all prax test files."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_operations.py",
|
||||
"standard": "architecture",
|
||||
"reason": "Test file lives in tests/ by convention, not in the 3-layer apps/ structure. This is standard for all prax test files."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_operations.py",
|
||||
"standard": "encapsulation",
|
||||
"reason": "Unit tests must import handlers directly to test them in isolation. _load_ops() imports operations handler for unit testing. Same pattern as test_logging_handlers.py."
|
||||
}
|
||||
],
|
||||
"notes": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -459,3 +459,600 @@ class TestInitializePositions:
|
||||
|
||||
assert str(log_file) in watcher.log_positions
|
||||
assert str(txt_file) not in watcher.log_positions
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADDITIONAL COVERAGE TESTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestGenerateErrorHash:
|
||||
"""Test _generate_error_hash deduplication helper."""
|
||||
|
||||
def test_returns_8_char_hex(self):
|
||||
"""Hash should be an 8-character hexadecimal string."""
|
||||
mod = _import_log_watcher()
|
||||
result = mod._generate_error_hash("mymodule", "something broke")
|
||||
assert len(result) == 8
|
||||
assert all(c in "0123456789abcdef" for c in result)
|
||||
|
||||
def test_same_input_same_hash(self):
|
||||
"""Identical module+message should produce identical hashes."""
|
||||
mod = _import_log_watcher()
|
||||
h1 = mod._generate_error_hash("mod", "error msg")
|
||||
h2 = mod._generate_error_hash("mod", "error msg")
|
||||
assert h1 == h2
|
||||
|
||||
def test_different_input_different_hash(self):
|
||||
"""Different inputs should produce different hashes."""
|
||||
mod = _import_log_watcher()
|
||||
h1 = mod._generate_error_hash("mod_a", "error one")
|
||||
h2 = mod._generate_error_hash("mod_b", "error two")
|
||||
assert h1 != h2
|
||||
|
||||
|
||||
class TestTriggerImportFallback:
|
||||
"""Test trigger import fallback when trigger module is unavailable."""
|
||||
|
||||
def test_has_trigger_flag_set(self):
|
||||
"""HAS_TRIGGER should be set based on trigger import availability."""
|
||||
mod = _import_log_watcher()
|
||||
# With our mock setup, trigger is available
|
||||
assert hasattr(mod, "HAS_TRIGGER")
|
||||
|
||||
|
||||
class TestProcessLogLine:
|
||||
"""Test _process_log_line dispatching."""
|
||||
|
||||
def test_empty_line_skipped(self):
|
||||
"""Empty/whitespace lines should be skipped entirely."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
watcher._process_log_line("PRAX", " ", "/fake/file.log")
|
||||
mock_queue.enqueue.assert_not_called()
|
||||
|
||||
def test_command_line_emits_separator(self):
|
||||
"""Command lines should emit a command separator, not a log event."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
watcher,
|
||||
"_extract_command_info",
|
||||
return_value={"command": "test cmd", "caller": None, "target": None},
|
||||
),
|
||||
patch.object(watcher, "_emit_command_separator") as mock_sep,
|
||||
patch.object(watcher, "_emit_log_event") as mock_log,
|
||||
):
|
||||
watcher._process_log_line("PRAX", "some command line", "/f.log")
|
||||
|
||||
mock_sep.assert_called_once()
|
||||
mock_log.assert_not_called()
|
||||
|
||||
def test_regular_line_emits_log_event(self):
|
||||
"""Non-command lines should emit a log event."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
with (
|
||||
patch.object(watcher, "_extract_command_info", return_value=None),
|
||||
patch.object(watcher, "_emit_log_event") as mock_log,
|
||||
):
|
||||
watcher._process_log_line("PRAX", "normal log line", "/f.log")
|
||||
|
||||
mock_log.assert_called_once()
|
||||
|
||||
|
||||
class TestReadNewContent:
|
||||
"""Test _read_new_content file tailing."""
|
||||
|
||||
def test_reads_new_content(self, tmp_path):
|
||||
"""Should read content added after last known position."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("old line\n", encoding="utf-8")
|
||||
old_size = log_file.stat().st_size
|
||||
watcher.log_positions[str(log_file)] = old_size
|
||||
|
||||
# Append new content
|
||||
with log_file.open("a", encoding="utf-8") as f:
|
||||
f.write("new line\n")
|
||||
|
||||
result = watcher._read_new_content(str(log_file))
|
||||
assert result is not None
|
||||
assert "new line" in result
|
||||
|
||||
def test_returns_none_when_no_new_content(self, tmp_path):
|
||||
"""Should return None when file hasn't grown."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("existing\n", encoding="utf-8")
|
||||
watcher.log_positions[str(log_file)] = log_file.stat().st_size
|
||||
|
||||
result = watcher._read_new_content(str(log_file))
|
||||
assert result is None
|
||||
|
||||
def test_resets_position_on_truncation(self, tmp_path):
|
||||
"""Should reset position to 0 when file is smaller than last pos."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("short\n", encoding="utf-8")
|
||||
watcher.log_positions[str(log_file)] = 99999 # Way past end
|
||||
|
||||
result = watcher._read_new_content(str(log_file))
|
||||
assert result is not None
|
||||
assert "short" in result
|
||||
|
||||
def test_returns_none_for_whitespace_only_content(self, tmp_path):
|
||||
"""Should return None when new content is only whitespace."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("initial\n", encoding="utf-8")
|
||||
old_size = log_file.stat().st_size
|
||||
watcher.log_positions[str(log_file)] = old_size
|
||||
|
||||
# Append only whitespace
|
||||
with log_file.open("a", encoding="utf-8") as f:
|
||||
f.write(" \n \n")
|
||||
|
||||
result = watcher._read_new_content(str(log_file))
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestOnModified:
|
||||
"""Test on_modified event handler."""
|
||||
|
||||
def test_ignores_directory_events(self):
|
||||
"""Should ignore directory modification events."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
event = MagicMock()
|
||||
event.is_directory = True
|
||||
|
||||
watcher.on_modified(event)
|
||||
mock_queue.enqueue.assert_not_called()
|
||||
|
||||
def test_ignores_non_log_files(self):
|
||||
"""Should ignore files that don't end with .log."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/fake/logs/system/test.txt"
|
||||
|
||||
watcher.on_modified(event)
|
||||
mock_queue.enqueue.assert_not_called()
|
||||
|
||||
def test_ignores_logs_outside_system_dir(self):
|
||||
"""Should ignore log files outside the system logs directory."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/other/dir/test.log"
|
||||
|
||||
with patch.object(mod, "get_system_logs_dir", return_value=Path("/fake/logs/system")):
|
||||
watcher.on_modified(event)
|
||||
|
||||
mock_queue.enqueue.assert_not_called()
|
||||
|
||||
def test_processes_valid_log_file(self, tmp_path):
|
||||
"""Should process a valid log file in the system logs directory."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
log_file = tmp_path / "prax.log"
|
||||
log_file.write_text("line one\n", encoding="utf-8")
|
||||
watcher.log_positions[str(log_file)] = 0
|
||||
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = str(log_file)
|
||||
|
||||
with (
|
||||
patch.object(mod, "get_system_logs_dir", return_value=tmp_path),
|
||||
patch.object(mod, "detect_branch_from_log", return_value="PRAX"),
|
||||
patch.object(watcher, "_process_log_line") as mock_process,
|
||||
):
|
||||
watcher.on_modified(event)
|
||||
|
||||
mock_process.assert_called()
|
||||
|
||||
def test_handles_read_exception(self):
|
||||
"""Should catch exceptions during log reading."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/fake/logs/system/crash.log"
|
||||
|
||||
with (
|
||||
patch.object(mod, "get_system_logs_dir", return_value=Path("/fake/logs/system")),
|
||||
patch.object(watcher, "_read_new_content", side_effect=OSError("disk error")),
|
||||
):
|
||||
# Should not raise
|
||||
watcher.on_modified(event)
|
||||
|
||||
mock_queue.enqueue.assert_not_called()
|
||||
|
||||
def test_skips_when_no_new_content(self):
|
||||
"""Should skip processing when _read_new_content returns None."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/fake/logs/system/empty.log"
|
||||
|
||||
with (
|
||||
patch.object(mod, "get_system_logs_dir", return_value=Path("/fake/logs/system")),
|
||||
patch.object(watcher, "_read_new_content", return_value=None),
|
||||
):
|
||||
watcher.on_modified(event)
|
||||
|
||||
mock_queue.enqueue.assert_not_called()
|
||||
|
||||
|
||||
class TestShouldDisplayLog:
|
||||
"""Test _should_display_log filter."""
|
||||
|
||||
def test_always_returns_true(self):
|
||||
"""Current implementation shows all log lines."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
assert watcher._should_display_log("any line") is True
|
||||
assert watcher._should_display_log("") is True
|
||||
|
||||
|
||||
class TestMatchFlowCommand:
|
||||
"""Test _match_flow_command pattern matching."""
|
||||
|
||||
def test_loaded_module_command(self):
|
||||
"""Should detect flow command from 'Loaded module' line."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
# Clear any prior flow commands to avoid dedup
|
||||
watcher.last_command_per_branch.clear()
|
||||
|
||||
result = watcher._match_flow_command("Loaded module: flow_planner")
|
||||
assert result is not None
|
||||
assert result["command"] == "flow command"
|
||||
|
||||
def test_loaded_module_suppressed_when_duplicate(self):
|
||||
"""Should suppress duplicate 'Loaded module' commands."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
watcher.last_command_per_branch["FLOW"] = "FLOW:flow command"
|
||||
|
||||
result = watcher._match_flow_command("Loaded module: flow_planner")
|
||||
assert result is None
|
||||
|
||||
def test_non_flow_line_returns_none(self):
|
||||
"""Should return None for unrecognized flow lines."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
result = watcher._match_flow_command("Just a normal log line")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestExtractCommandInfoAdditional:
|
||||
"""Additional command extraction tests for uncovered patterns."""
|
||||
|
||||
def test_seedgo_checklist_running(self):
|
||||
"""Should detect seedgo checklist commands."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "standards_checklist Running full standard check on prax"
|
||||
result = watcher._extract_command_info(line)
|
||||
assert result is not None
|
||||
assert "seedgo checklist" in result["command"]
|
||||
|
||||
def test_backup_versioned(self):
|
||||
"""Should detect backup versioned commands."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "[backup] Starting versioned backup"
|
||||
result = watcher._extract_command_info(line)
|
||||
assert result is not None
|
||||
assert "backup versioned" in result["command"]
|
||||
|
||||
def test_backup_sync(self):
|
||||
"""Should detect backup sync commands."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "[backup] Running sync operation"
|
||||
result = watcher._extract_command_info(line)
|
||||
assert result is not None
|
||||
assert "backup sync" in result["command"]
|
||||
|
||||
def test_memory_search(self):
|
||||
"""Should detect memory search commands."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "[memory] Handling search query for branch status"
|
||||
result = watcher._extract_command_info(line)
|
||||
assert result is not None
|
||||
assert "memory search" in result["command"]
|
||||
|
||||
def test_trigger_triggered(self):
|
||||
"""Should detect trigger events with 'triggered' keyword."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "[trigger] Rule triggered: error_threshold"
|
||||
result = watcher._extract_command_info(line)
|
||||
assert result is not None
|
||||
assert "trigger fire" in result["command"]
|
||||
|
||||
def test_ai_mail_send_without_target(self):
|
||||
"""Should handle ai_mail send without a parseable recipient."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "[ai_mail] Sending broadcast message"
|
||||
result = watcher._extract_command_info(line)
|
||||
assert result is not None
|
||||
assert "ai_mail send" in result["command"]
|
||||
|
||||
def test_drone_started_without_bracket_prefix(self):
|
||||
"""Should detect 'Drone started with args' without [drone] prefix."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "Drone started with args: ['audit', '@prax']"
|
||||
result = watcher._extract_command_info(line)
|
||||
assert result is not None
|
||||
assert "drone" in result["command"]
|
||||
assert "audit" in result["command"]
|
||||
|
||||
|
||||
class TestMatchExecutingCommand:
|
||||
"""Test _match_executing_command pattern matching."""
|
||||
|
||||
def test_returns_none_when_no_cmd_match(self):
|
||||
"""Should return None when command pattern doesn't match."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
result = watcher._match_executing_command("Executing something weird")
|
||||
assert result is None
|
||||
|
||||
def test_extracts_caller_and_simplifies_path(self):
|
||||
"""Should extract caller and simplify aipass paths to @branch."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "Executing command [CALLER:DRONE]: /path/to/aipass/seedgo/apps/audit.py @prax"
|
||||
result = watcher._match_executing_command(line)
|
||||
assert result is not None
|
||||
assert result["caller"] == "DRONE"
|
||||
assert result["target"] == "PRAX"
|
||||
|
||||
def test_without_caller(self):
|
||||
"""Should work without CALLER tag."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
line = "Executing command: /path/to/aipass/flow run"
|
||||
result = watcher._match_executing_command(line)
|
||||
assert result is not None
|
||||
assert result["caller"] is None
|
||||
|
||||
|
||||
class TestExtractTargetFromCmd:
|
||||
"""Test _extract_target_from_cmd static method."""
|
||||
|
||||
def test_extracts_at_target(self):
|
||||
"""Should extract target from @branch pattern."""
|
||||
mod = _import_log_watcher()
|
||||
result = mod.LogFileWatcher._extract_target_from_cmd("audit @prax")
|
||||
assert result == "PRAX"
|
||||
|
||||
def test_extracts_path_target(self):
|
||||
"""Should extract target from /aipass/branch pattern."""
|
||||
mod = _import_log_watcher()
|
||||
result = mod.LogFileWatcher._extract_target_from_cmd("/path/to/aipass/seedgo/run.py")
|
||||
assert result == "SEEDGO"
|
||||
|
||||
def test_returns_none_when_no_target(self):
|
||||
"""Should return None when no target pattern matches."""
|
||||
mod = _import_log_watcher()
|
||||
result = mod.LogFileWatcher._extract_target_from_cmd("plain command")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestEmitCommandSeparator:
|
||||
"""Test _emit_command_separator event emission."""
|
||||
|
||||
def test_dict_format(self):
|
||||
"""Should handle dict command_info format."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
watcher.last_command_per_branch.clear()
|
||||
|
||||
watcher._emit_command_separator("PRAX", {"command": "test cmd", "caller": "DRONE", "target": "FLOW"})
|
||||
mock_queue.enqueue.assert_called_once()
|
||||
|
||||
def test_tuple_format(self):
|
||||
"""Should handle legacy tuple (command, caller) format."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
watcher.last_command_per_branch.clear()
|
||||
|
||||
watcher._emit_command_separator("PRAX", ("test cmd", "DRONE"))
|
||||
mock_queue.enqueue.assert_called_once()
|
||||
|
||||
def test_string_format(self):
|
||||
"""Should handle plain string command format."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
watcher.last_command_per_branch.clear()
|
||||
|
||||
watcher._emit_command_separator("PRAX", "test cmd")
|
||||
mock_queue.enqueue.assert_called_once()
|
||||
|
||||
def test_deduplication(self):
|
||||
"""Should skip duplicate consecutive commands for same branch."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
watcher.last_command_per_branch.clear()
|
||||
|
||||
cmd_info = {"command": "same cmd", "caller": None, "target": None}
|
||||
watcher._emit_command_separator("PRAX", cmd_info)
|
||||
watcher._emit_command_separator("PRAX", cmd_info)
|
||||
|
||||
assert mock_queue.enqueue.call_count == 1
|
||||
|
||||
def test_target_stored_in_action(self):
|
||||
"""Should store target in action field when target is provided."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
watcher.last_command_per_branch.clear()
|
||||
|
||||
watcher._emit_command_separator("PRAX", {"command": "cmd", "caller": None, "target": "FLOW"})
|
||||
|
||||
enqueued_event = mock_queue.enqueue.call_args[0][0]
|
||||
assert "FLOW" in enqueued_event.action
|
||||
|
||||
|
||||
class TestEmitLogEvent:
|
||||
"""Test _emit_log_event event emission."""
|
||||
|
||||
def test_emits_log_event(self):
|
||||
"""Should create and enqueue a log monitoring event."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
watcher._emit_log_event("PRAX", "test message", "info")
|
||||
mock_queue.enqueue.assert_called_once()
|
||||
|
||||
def test_error_level_fires_trigger(self):
|
||||
"""Should fire trigger event for ERROR level logs."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
# Set up trigger mock
|
||||
mock_trigger = MagicMock()
|
||||
setattr(mod, "HAS_TRIGGER", True)
|
||||
setattr(mod, "trigger", mock_trigger)
|
||||
|
||||
watcher._emit_log_event(
|
||||
"PRAX",
|
||||
"[PRAX] 2025-01-01 | mymodule | ERROR | something broke",
|
||||
"error",
|
||||
"/fake/prax.log",
|
||||
)
|
||||
|
||||
mock_trigger.fire.assert_called_once()
|
||||
call_kwargs = mock_trigger.fire.call_args
|
||||
assert call_kwargs[0][0] == "error_detected"
|
||||
assert call_kwargs[1]["branch"] == "PRAX"
|
||||
|
||||
def test_info_level_does_not_fire_trigger(self):
|
||||
"""Should not fire trigger for non-error levels."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
mock_trigger = MagicMock()
|
||||
setattr(mod, "HAS_TRIGGER", True)
|
||||
setattr(mod, "trigger", mock_trigger)
|
||||
|
||||
watcher._emit_log_event("PRAX", "normal info", "info")
|
||||
mock_trigger.fire.assert_not_called()
|
||||
|
||||
def test_trigger_not_fired_when_unavailable(self):
|
||||
"""Should skip trigger when HAS_TRIGGER is False."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
setattr(mod, "HAS_TRIGGER", False)
|
||||
setattr(mod, "trigger", None)
|
||||
|
||||
# Should not raise
|
||||
watcher._emit_log_event("PRAX", "error msg", "error")
|
||||
mock_queue.enqueue.assert_called_once()
|
||||
|
||||
def test_error_with_no_log_file_path(self):
|
||||
"""Should use 'unknown' for log_file when path not provided."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
mock_trigger = MagicMock()
|
||||
setattr(mod, "HAS_TRIGGER", True)
|
||||
setattr(mod, "trigger", mock_trigger)
|
||||
|
||||
watcher._emit_log_event("PRAX", "error msg", "error")
|
||||
|
||||
call_kwargs = mock_trigger.fire.call_args[1]
|
||||
assert call_kwargs["log_file"] == "unknown"
|
||||
|
||||
def test_error_extracts_module_name_from_pipe_format(self):
|
||||
"""Should extract module name from pipe-delimited log lines."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, mock_queue = _make_watcher(mod)
|
||||
|
||||
mock_trigger = MagicMock()
|
||||
setattr(mod, "HAS_TRIGGER", True)
|
||||
setattr(mod, "trigger", mock_trigger)
|
||||
|
||||
watcher._emit_log_event(
|
||||
"PRAX",
|
||||
"[PRAX] 2025-01-01 | mymod.handler | ERROR | crash",
|
||||
"error",
|
||||
"/fake/log.log",
|
||||
)
|
||||
|
||||
call_kwargs = mock_trigger.fire.call_args[1]
|
||||
assert call_kwargs["module_name"] == "mymod.handler"
|
||||
|
||||
|
||||
class TestStartLogWatcherAdditional:
|
||||
"""Additional tests for start_log_watcher."""
|
||||
|
||||
def test_stops_existing_observer_before_starting(self):
|
||||
"""Should stop existing observer if already running."""
|
||||
mod = _import_log_watcher()
|
||||
mock_queue = MagicMock()
|
||||
mock_old_observer = MagicMock()
|
||||
mock_old_observer.is_alive.return_value = True
|
||||
setattr(mod, "_log_observer", mock_old_observer)
|
||||
|
||||
mock_new_observer = MagicMock()
|
||||
|
||||
with (
|
||||
patch.object(mod, "WatchdogObserver", return_value=mock_new_observer),
|
||||
patch.object(mod, "get_system_logs_dir", return_value=Path("/fake/logs")),
|
||||
patch.object(mod, "stop_log_watcher") as mock_stop,
|
||||
):
|
||||
mod.start_log_watcher(mock_queue)
|
||||
|
||||
mock_stop.assert_called_once()
|
||||
|
||||
|
||||
class TestInitializePositionsAdditional:
|
||||
"""Additional tests for initialize_positions."""
|
||||
|
||||
def test_handles_stat_exception(self, tmp_path):
|
||||
"""Should handle exceptions when getting file size."""
|
||||
mod = _import_log_watcher()
|
||||
watcher, _ = _make_watcher(mod)
|
||||
|
||||
log_file = tmp_path / "bad.log"
|
||||
log_file.write_text("content\n", encoding="utf-8")
|
||||
|
||||
with (
|
||||
patch.object(mod, "get_system_logs_dir", return_value=tmp_path),
|
||||
patch.object(Path, "stat", side_effect=OSError("stat failed")),
|
||||
):
|
||||
# Should not raise
|
||||
watcher.initialize_positions()
|
||||
|
||||
assert str(log_file) not in watcher.log_positions
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -984,3 +984,986 @@ class TestPrintStatus:
|
||||
# Should not raise
|
||||
mod.print_status(["PRAX"], verbosity=0, filters=None)
|
||||
assert mod.console.print.called
|
||||
|
||||
|
||||
# =============================================
|
||||
# BRANCH DETECTOR — ADDITIONAL COVERAGE TESTS
|
||||
# =============================================
|
||||
|
||||
# Fake home base used across external-project tests to avoid
|
||||
# hardcoded /home/ paths that trip the SEEDGO log-structure checker.
|
||||
_FAKE_HOME = Path("/fakehome/user")
|
||||
_FAKE_PROJECTS = _FAKE_HOME / "Projects"
|
||||
|
||||
|
||||
class TestFindRepoRoot:
|
||||
"""Tests for BranchDetector._find_repo_root()."""
|
||||
|
||||
def test_cached_repo_root_returned(self):
|
||||
"""When _repo_root is already set, return it without walking."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
sentinel = Path("/cached/root")
|
||||
detector._repo_root = sentinel
|
||||
result = detector._find_repo_root()
|
||||
assert result is sentinel
|
||||
|
||||
def test_walks_parents_to_find_registry(self):
|
||||
"""Should walk up from __file__ to find AIPASS_REGISTRY.json."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._repo_root = None
|
||||
|
||||
mock_parent = MagicMock(spec=Path)
|
||||
mock_registry = MagicMock(spec=Path)
|
||||
mock_registry.exists.return_value = True
|
||||
mock_parent.__truediv__ = MagicMock(return_value=mock_registry)
|
||||
|
||||
mock_resolved = MagicMock()
|
||||
mock_resolved.parent = mock_parent
|
||||
mock_resolved.parents = []
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.return_value.resolve.return_value = mock_resolved
|
||||
result = detector._find_repo_root()
|
||||
|
||||
assert result is mock_parent
|
||||
|
||||
def test_fallback_to_cwd_when_registry_missing(self):
|
||||
"""Should fall back to Path.cwd() when no AIPASS_REGISTRY.json found."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._repo_root = None
|
||||
|
||||
mock_parent = MagicMock(spec=Path)
|
||||
mock_no_registry = MagicMock(spec=Path)
|
||||
mock_no_registry.exists.return_value = False
|
||||
mock_parent.__truediv__ = MagicMock(return_value=mock_no_registry)
|
||||
mock_parent.parents = []
|
||||
|
||||
mock_resolved = MagicMock()
|
||||
mock_resolved.parent = mock_parent
|
||||
mock_resolved.parents = []
|
||||
|
||||
fake_cwd = Path("/fake/cwd")
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.return_value.resolve.return_value = mock_resolved
|
||||
mock_path_cls.cwd.return_value = fake_cwd
|
||||
result = detector._find_repo_root()
|
||||
|
||||
assert result is fake_cwd
|
||||
|
||||
|
||||
class TestRegisterBranch:
|
||||
"""Tests for BranchDetector._register_branch()."""
|
||||
|
||||
def test_registers_branch_with_name_and_path(self):
|
||||
"""Should add to branch_map and known_branches."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_resolved = MagicMock(spec=Path)
|
||||
mock_resolved.__str__ = MagicMock(return_value="/resolved/path")
|
||||
mock_path_cls.return_value.resolve.return_value = mock_resolved
|
||||
|
||||
detector._register_branch({"name": "DRONE", "path": "/some/drone"})
|
||||
|
||||
assert "DRONE" in detector.known_branches
|
||||
assert "/resolved/path" in detector.branch_map
|
||||
assert "/resolved/path/" in detector.branch_map
|
||||
|
||||
def test_skips_empty_name(self):
|
||||
"""Should skip branch entries with empty name."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
initial_count = len(detector.known_branches)
|
||||
detector._register_branch({"name": "", "path": "/some/path"})
|
||||
assert len(detector.known_branches) == initial_count
|
||||
|
||||
def test_skips_empty_path(self):
|
||||
"""Should skip branch entries with empty path."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
initial_count = len(detector.known_branches)
|
||||
detector._register_branch({"name": "EMPTY", "path": ""})
|
||||
assert len(detector.known_branches) == initial_count
|
||||
|
||||
def test_skips_missing_keys(self):
|
||||
"""Should skip branch entries with missing name or path keys."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
initial_count = len(detector.known_branches)
|
||||
detector._register_branch({})
|
||||
assert len(detector.known_branches) == initial_count
|
||||
|
||||
|
||||
class TestLoadRegistry:
|
||||
"""Tests for BranchDetector._load_registry()."""
|
||||
|
||||
def test_loads_branches_from_valid_registry(self):
|
||||
"""Should populate known_branches from valid registry file."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
detector._repo_root = Path("/fake/root")
|
||||
|
||||
registry_data = {
|
||||
"branches": [
|
||||
{"name": "PRAX", "path": "/fake/prax"},
|
||||
{"name": "FLOW", "path": "/fake/flow"},
|
||||
],
|
||||
}
|
||||
|
||||
mock_registry = MagicMock(spec=Path)
|
||||
mock_registry.exists.return_value = True
|
||||
|
||||
mock_root = MagicMock(spec=Path)
|
||||
mock_root.__truediv__ = MagicMock(return_value=mock_registry)
|
||||
|
||||
with (
|
||||
patch.object(detector, "_find_repo_root", return_value=mock_root),
|
||||
patch("builtins.open", _mopen(read_data=json.dumps(registry_data))),
|
||||
patch(f"{mod.__name__}.json.load", return_value=registry_data),
|
||||
patch(f"{mod.__name__}.Path") as mock_path_cls,
|
||||
):
|
||||
mock_path_cls.return_value.resolve.return_value = MagicMock(__str__=MagicMock(return_value="/resolved"))
|
||||
detector._load_registry()
|
||||
|
||||
assert len(detector.known_branches) >= 2
|
||||
|
||||
def test_loads_fallback_when_registry_missing(self):
|
||||
"""Should load fallback branches when registry file not found."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
detector.known_branches.clear()
|
||||
|
||||
mock_root = MagicMock(spec=Path)
|
||||
mock_registry = MagicMock(spec=Path)
|
||||
mock_registry.exists.return_value = False
|
||||
mock_root.__truediv__ = MagicMock(return_value=mock_registry)
|
||||
|
||||
with patch.object(detector, "_find_repo_root", return_value=mock_root):
|
||||
detector._load_registry()
|
||||
|
||||
assert "PRAX" in detector.known_branches
|
||||
assert "SEEDGO" in detector.known_branches
|
||||
|
||||
def test_loads_fallback_when_no_branches_key(self):
|
||||
"""Should load fallback when registry has empty branches array."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
detector.known_branches.clear()
|
||||
|
||||
mock_root = MagicMock(spec=Path)
|
||||
mock_registry = MagicMock(spec=Path)
|
||||
mock_registry.exists.return_value = True
|
||||
mock_root.__truediv__ = MagicMock(return_value=mock_registry)
|
||||
|
||||
with (
|
||||
patch.object(detector, "_find_repo_root", return_value=mock_root),
|
||||
patch("builtins.open", _mopen(read_data='{"branches": []}')),
|
||||
patch(f"{mod.__name__}.json.load", return_value={"branches": []}),
|
||||
):
|
||||
detector._load_registry()
|
||||
|
||||
assert "PRAX" in detector.known_branches
|
||||
|
||||
def test_loads_fallback_on_json_decode_error(self):
|
||||
"""Should load fallback on JSONDecodeError."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
detector.known_branches.clear()
|
||||
|
||||
mock_root = MagicMock(spec=Path)
|
||||
mock_registry = MagicMock(spec=Path)
|
||||
mock_registry.exists.return_value = True
|
||||
mock_root.__truediv__ = MagicMock(return_value=mock_registry)
|
||||
|
||||
with (
|
||||
patch.object(detector, "_find_repo_root", return_value=mock_root),
|
||||
patch("builtins.open", _mopen(read_data="{bad json")),
|
||||
patch(
|
||||
f"{mod.__name__}.json.load",
|
||||
side_effect=json.JSONDecodeError("err", "doc", 0),
|
||||
),
|
||||
):
|
||||
detector._load_registry()
|
||||
|
||||
assert "PRAX" in detector.known_branches
|
||||
|
||||
def test_loads_fallback_on_generic_exception(self):
|
||||
"""Should load fallback on any unexpected exception."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
detector.known_branches.clear()
|
||||
|
||||
mock_root = MagicMock(spec=Path)
|
||||
mock_registry = MagicMock(spec=Path)
|
||||
mock_registry.exists.return_value = True
|
||||
mock_root.__truediv__ = MagicMock(return_value=mock_registry)
|
||||
|
||||
with (
|
||||
patch.object(detector, "_find_repo_root", return_value=mock_root),
|
||||
patch("builtins.open", side_effect=PermissionError("denied")),
|
||||
):
|
||||
detector._load_registry()
|
||||
|
||||
assert "PRAX" in detector.known_branches
|
||||
|
||||
|
||||
class TestLoadFallbackBranches:
|
||||
"""Tests for BranchDetector._load_fallback_branches()."""
|
||||
|
||||
def test_populates_known_branches(self):
|
||||
"""Should populate known_branches with standard fallback set."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
detector.known_branches.clear()
|
||||
detector._load_fallback_branches()
|
||||
|
||||
expected = {
|
||||
"SEEDGO",
|
||||
"CLI",
|
||||
"FLOW",
|
||||
"PRAX",
|
||||
"DRONE",
|
||||
"BACKUP",
|
||||
"SECURITY",
|
||||
"AIPASS",
|
||||
}
|
||||
assert detector.known_branches == expected
|
||||
|
||||
|
||||
class TestResolveExternalProjectName:
|
||||
"""Tests for BranchDetector._resolve_external_project_name()."""
|
||||
|
||||
def test_returns_uppercased_name_when_no_registry(self):
|
||||
"""Should return uppercased directory name when no _REGISTRY.json found."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_home = MagicMock(spec=Path)
|
||||
mock_projects = MagicMock(spec=Path)
|
||||
mock_project_dir = MagicMock(spec=Path)
|
||||
mock_project_dir.exists.return_value = True
|
||||
mock_project_dir.iterdir.return_value = []
|
||||
mock_projects.__truediv__ = MagicMock(return_value=mock_project_dir)
|
||||
mock_home.__truediv__ = MagicMock(return_value=mock_projects)
|
||||
mock_path_cls.home.return_value = mock_home
|
||||
|
||||
result = detector._resolve_external_project_name("MyProject")
|
||||
|
||||
assert result == "MYPROJECT"
|
||||
|
||||
def test_returns_uppercased_when_dir_not_found(self):
|
||||
"""Should return uppercased name when project dir doesn't exist."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_home = MagicMock(spec=Path)
|
||||
mock_projects = MagicMock(spec=Path)
|
||||
mock_project_dir = MagicMock(spec=Path)
|
||||
mock_project_dir.exists.return_value = False
|
||||
mock_projects.__truediv__ = MagicMock(return_value=mock_project_dir)
|
||||
mock_home.__truediv__ = MagicMock(return_value=mock_projects)
|
||||
mock_path_cls.home.return_value = mock_home
|
||||
|
||||
result = detector._resolve_external_project_name("NoSuchProject")
|
||||
|
||||
assert result == "NOSUCHPROJECT"
|
||||
|
||||
def test_handles_os_error_on_iterdir(self):
|
||||
"""Should return uppercased name on OSError reading directory."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_home = MagicMock(spec=Path)
|
||||
mock_projects = MagicMock(spec=Path)
|
||||
mock_project_dir = MagicMock(spec=Path)
|
||||
mock_project_dir.exists.return_value = True
|
||||
mock_project_dir.iterdir.side_effect = OSError("permission denied")
|
||||
mock_projects.__truediv__ = MagicMock(return_value=mock_project_dir)
|
||||
mock_home.__truediv__ = MagicMock(return_value=mock_projects)
|
||||
mock_path_cls.home.return_value = mock_home
|
||||
|
||||
result = detector._resolve_external_project_name("Locked")
|
||||
|
||||
assert result == "LOCKED"
|
||||
|
||||
|
||||
class TestParseExternalProjectPath:
|
||||
"""Tests for BranchDetector._parse_external_project_path()."""
|
||||
|
||||
def test_returns_none_for_non_encoded_path(self):
|
||||
"""Should return (None, None) if path doesn't start with dash."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
project, agent = detector._parse_external_project_path("not-encoded")
|
||||
assert project is None
|
||||
assert agent is None
|
||||
|
||||
def test_returns_none_when_no_projects_boundary(self):
|
||||
"""Should return (None, None) if -projects- not found."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
project, agent = detector._parse_external_project_path("-fakehome-user-something")
|
||||
assert project is None
|
||||
assert agent is None
|
||||
|
||||
def test_parses_project_without_src(self):
|
||||
"""Should parse project name without src subdirectory."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch.object(detector, "_resolve_external_project_name", return_value="VERA-STUDIO"):
|
||||
project, agent = detector._parse_external_project_path("-fakehome-user-Projects-Vera-Studio")
|
||||
|
||||
assert project == "VERA-STUDIO"
|
||||
assert agent is None
|
||||
|
||||
def test_parses_project_with_src_agent(self):
|
||||
"""Should parse project and agent from -src- segment."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch.object(detector, "_resolve_external_project_name", return_value="AIPL"):
|
||||
project, agent = detector._parse_external_project_path("-fakehome-user-Projects-AIPL-src-polyglot")
|
||||
|
||||
assert project == "AIPL"
|
||||
assert agent == "POLYGLOT"
|
||||
|
||||
|
||||
class TestDetectFromClaudeProject:
|
||||
"""Tests for BranchDetector._detect_from_claude_project()."""
|
||||
|
||||
def test_internal_aipass_branch_via_registry(self):
|
||||
"""Should detect internal AIPass branch from Claude project path."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/" + "-fakehome-user-Projects-AIPass-src-aipass-prax/session.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
assert result is not None
|
||||
assert "AIPASS" in result
|
||||
|
||||
def test_internal_aipass_fallback_to_segment_scan(self):
|
||||
"""Should fall back to known branch name scanning for internal AIPass."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("PRAX")
|
||||
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/" + "-fakehome-user-Projects-AIPass-src-aipass-prax/something.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
assert result is not None
|
||||
assert "PRAX" in result
|
||||
|
||||
def test_internal_aipass_subagent_suffix(self):
|
||||
"""Should append SUB for subagent paths."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("PRAX")
|
||||
|
||||
path = (
|
||||
str(_FAKE_HOME)
|
||||
+ "/.claude/projects/"
|
||||
+ "-fakehome-user-Projects-AIPass-src-aipass-prax"
|
||||
+ "/subagents/abc123/session.jsonl"
|
||||
)
|
||||
result = detector._detect_from_claude_project(path)
|
||||
assert result is not None
|
||||
assert "SUB" in result
|
||||
|
||||
def test_external_project_detection(self):
|
||||
"""Should detect external project from Claude project path."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_parse_external_project_path",
|
||||
return_value=("AIPL", "POLYGLOT"),
|
||||
):
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/" + "-fakehome-user-Projects-AIPL-src-polyglot/file.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
|
||||
assert result == "AIPL/POLYGLOT"
|
||||
|
||||
def test_external_project_main_session(self):
|
||||
"""Should return project name only for main session (no agent)."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_parse_external_project_path",
|
||||
return_value=("VERA-STUDIO", None),
|
||||
):
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/" + "-fakehome-user-Projects-Vera-Studio/file.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
|
||||
assert result == "VERA-STUDIO"
|
||||
|
||||
def test_external_project_subagent(self):
|
||||
"""Should append SUB to external project subagent paths."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_parse_external_project_path",
|
||||
return_value=("VERA-STUDIO", None),
|
||||
):
|
||||
path = (
|
||||
str(_FAKE_HOME)
|
||||
+ "/.claude/projects/"
|
||||
+ "-fakehome-user-Projects-Vera-Studio"
|
||||
+ "/subagents/abc/file.jsonl"
|
||||
)
|
||||
result = detector._detect_from_claude_project(path)
|
||||
|
||||
assert result == "VERA-STUDIO SUB"
|
||||
|
||||
def test_old_fallback_segment_scan(self):
|
||||
"""Should fall back to segment scanning for unknown external paths."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("SEEDGO")
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_parse_external_project_path",
|
||||
return_value=(None, None),
|
||||
):
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/some-random-seedgo/file.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
|
||||
assert result == "SEEDGO"
|
||||
|
||||
def test_old_fallback_returns_none_for_empty_segments(self):
|
||||
"""Should return None when folder parses to no segments."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_parse_external_project_path",
|
||||
return_value=(None, None),
|
||||
):
|
||||
path = str(_FAKE_HOME) + "/.claude/projects//file.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_old_fallback_last_segment_known(self):
|
||||
"""Should return last segment if it matches a known branch."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("CLI")
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_parse_external_project_path",
|
||||
return_value=(None, None),
|
||||
):
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/unknown-cli/file.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
|
||||
assert result == "CLI"
|
||||
|
||||
def test_old_fallback_returns_none_when_no_branch_match(self):
|
||||
"""Should return None when no segments match known branches."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_parse_external_project_path",
|
||||
return_value=(None, None),
|
||||
):
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/completely-unknown-xyz/file.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_internal_aipass_fallback_last_segment(self):
|
||||
"""When no known branch found, should use last segment uppercased."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod, branches={})
|
||||
|
||||
path = str(_FAKE_HOME) + "/.claude/projects/" + "-fakehome-user-Projects-AIPass-src-aipass-newmod/file.jsonl"
|
||||
result = detector._detect_from_claude_project(path)
|
||||
assert result is not None
|
||||
assert "NEWMOD" in result
|
||||
|
||||
|
||||
class TestDetectFromCompoundParts:
|
||||
"""Tests for BranchDetector._detect_from_compound_parts()."""
|
||||
|
||||
def test_finds_known_branch_in_underscore_part(self):
|
||||
"""Should detect branch from underscore-delimited path part."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("MAIL")
|
||||
|
||||
result = detector._detect_from_compound_parts(["src", "ai_mail", "handler"])
|
||||
assert result == "MAIL"
|
||||
|
||||
def test_returns_none_when_no_match(self):
|
||||
"""Should return None when no underscore subparts match."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
result = detector._detect_from_compound_parts(["src", "something_else"])
|
||||
assert result is None
|
||||
|
||||
def test_skips_parts_without_underscore(self):
|
||||
"""Should only check parts that contain underscores."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
result = detector._detect_from_compound_parts(["simple", "parts", "here"])
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestDetectFromExternalProjectPath:
|
||||
"""Tests for BranchDetector._detect_from_external_project_path()."""
|
||||
|
||||
def test_path_not_under_projects(self):
|
||||
"""Should return None if path is not under ~/Projects/."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
path = Path("/tmp/some/random/file.py")
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_skips_aipass_directory(self):
|
||||
"""Should return None for paths under AIPass directory."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
path = _FAKE_PROJECTS / "AIPass" / "src" / "prax" / "file.py"
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_returns_project_with_agent(self):
|
||||
"""Should return PROJECT/AGENT for path with src subdirectory."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._external_project_cache["AIPL"] = "AIPL"
|
||||
|
||||
path = _FAKE_PROJECTS / "AIPL" / "src" / "polyglot" / "file.py"
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result == "AIPL/POLYGLOT"
|
||||
|
||||
def test_returns_project_without_agent(self):
|
||||
"""Should return PROJECT for path without src subdirectory."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._external_project_cache["MyApp"] = "MYAPP"
|
||||
|
||||
path = _FAKE_PROJECTS / "MyApp" / "config.json"
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result == "MYAPP"
|
||||
|
||||
def test_test_suffix_appended(self):
|
||||
"""Should append TESTS suffix for test-related paths."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._external_project_cache["AIPL"] = "AIPL"
|
||||
|
||||
path = _FAKE_PROJECTS / "AIPL" / "src" / "polyglot" / "tests" / "test_main.py"
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result == "AIPL/POLYGLOT TESTS"
|
||||
|
||||
def test_cached_project_name(self):
|
||||
"""Should use cached project name on subsequent lookups."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._external_project_cache["CachedProj"] = "CACHED"
|
||||
|
||||
path = _FAKE_PROJECTS / "CachedProj" / "file.py"
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result == "CACHED"
|
||||
|
||||
def test_empty_rel_parts_returns_none(self):
|
||||
"""Should return None if relative path has no parts."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(_FAKE_PROJECTS)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_when_no_registry_file(self):
|
||||
"""Should return None when project directory has no _REGISTRY.json."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._external_project_cache.clear()
|
||||
|
||||
path = _FAKE_PROJECTS / "SomeProject" / "src" / "module" / "file.py"
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
mock_project_dir = MagicMock(spec=Path)
|
||||
mock_project_dir.iterdir.return_value = iter([])
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_test_suffix_for_test_log_file(self):
|
||||
"""Should append TESTS for paths ending in _test.log."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector._external_project_cache["Proj"] = "PROJ"
|
||||
|
||||
path = _FAKE_PROJECTS / "Proj" / "output_test.log"
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
result = detector._detect_from_external_project_path(path)
|
||||
|
||||
assert result == "PROJ TESTS"
|
||||
|
||||
|
||||
class TestExtractBranchFromCentral:
|
||||
"""Tests for BranchDetector._extract_branch_from_central()."""
|
||||
|
||||
def test_dot_central_json(self):
|
||||
"""Should extract branch name from .central.json filename."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
path = Path("/some/ai_mail/DRONE.central.json")
|
||||
result = detector._extract_branch_from_central(str(path), path)
|
||||
assert result == "DRONE"
|
||||
|
||||
def test_underscore_central_json(self):
|
||||
"""Should extract branch name from _central.json filename."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
path = Path("/some/dir/ai_mail/PRAX_central.json")
|
||||
result = detector._extract_branch_from_central("ai_mail/PRAX_central.json", path)
|
||||
assert result == "PRAX"
|
||||
|
||||
def test_returns_none_for_non_mail_path(self):
|
||||
"""Should return None if path doesn't contain ai_mail or AI_MAIL."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
path = Path("/some/dir/config.json")
|
||||
result = detector._extract_branch_from_central(str(path), path)
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_for_non_central_filename(self):
|
||||
"""Should return None for non-central filename in ai_mail path."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
path = Path("/ai_mail/inbox.json")
|
||||
result = detector._extract_branch_from_central("ai_mail/inbox.json", path)
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestDetectFromPathStrategies:
|
||||
"""Tests for detect_from_path covering all strategy branches."""
|
||||
|
||||
def test_cache_hit_returns_cached_value(self):
|
||||
"""Strategy 0: Should return cached value from log_map."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
resolved = "/cached/path"
|
||||
detector.log_map[resolved] = "CACHED_BRANCH"
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path_cls.return_value = mock_path
|
||||
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert result == "CACHED_BRANCH"
|
||||
|
||||
def test_claude_project_path_strategy(self):
|
||||
"""Strategy 3: Should detect from .claude/projects/ path."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("PRAX")
|
||||
|
||||
resolved = (
|
||||
str(_FAKE_HOME) + "/.claude/projects/" + "-fakehome-user-Projects-AIPass-src-aipass-prax/session.jsonl"
|
||||
)
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path.parents = []
|
||||
mock_path.parent = MagicMock()
|
||||
mock_path.parent.__eq__ = MagicMock(return_value=False)
|
||||
mock_path.name = "session.jsonl"
|
||||
mock_path_cls.return_value = mock_path
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
detector._repo_root = MagicMock(spec=Path)
|
||||
detector._repo_root.__str__ = MagicMock(return_value=str(_FAKE_PROJECTS / "AIPass"))
|
||||
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert "PRAX" in result
|
||||
|
||||
def test_ai_mail_central_strategy(self):
|
||||
"""Strategy 4: Should detect from ai_mail central filename."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
resolved = str(_FAKE_PROJECTS / "AIPass" / "src" / "aipass" / "ai_mail" / "central" / "DRONE.central.json")
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path.parents = []
|
||||
mock_path.parent = MagicMock()
|
||||
mock_path.parent.__eq__ = MagicMock(return_value=False)
|
||||
mock_path.name = "DRONE.central.json"
|
||||
mock_path_cls.return_value = mock_path
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
detector._repo_root = MagicMock(spec=Path)
|
||||
detector._repo_root.__str__ = MagicMock(return_value=str(_FAKE_PROJECTS / "AIPass"))
|
||||
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert result == "DRONE"
|
||||
|
||||
def test_root_level_system_files_strategy(self):
|
||||
"""Strategy 5: Should return SYSTEM for repo root files."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
repo_root = _FAKE_PROJECTS / "AIPass"
|
||||
resolved = str(repo_root / "README.md")
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path.parents = []
|
||||
mock_path.parent = repo_root
|
||||
mock_path.name = "README.md"
|
||||
mock_path_cls.return_value = mock_path
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
detector._repo_root = repo_root
|
||||
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert result == "SYSTEM"
|
||||
|
||||
def test_root_claude_dir_system_files(self):
|
||||
"""Strategy 5: Should return SYSTEM for .claude dir under repo root."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
repo_root = _FAKE_PROJECTS / "AIPass"
|
||||
resolved = str(repo_root / ".claude" / "settings.json")
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path.parents = []
|
||||
mock_path.parent = repo_root / ".claude"
|
||||
mock_path.name = "settings.json"
|
||||
mock_path_cls.return_value = mock_path
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
detector._repo_root = repo_root
|
||||
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert result == "SYSTEM"
|
||||
|
||||
def test_known_branch_in_path_parts_strategy(self):
|
||||
"""Strategy 6: Should detect branch from path parts."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("DRONE")
|
||||
|
||||
resolved = "/some/random/drone/handler.py"
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path.parents = []
|
||||
mock_path.parent = MagicMock()
|
||||
mock_path.parent.__eq__ = MagicMock(return_value=False)
|
||||
mock_path.name = "handler.py"
|
||||
mock_path_cls.return_value = mock_path
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
detector._repo_root = MagicMock(spec=Path)
|
||||
detector._repo_root.__str__ = MagicMock(return_value=str(_FAKE_PROJECTS / "AIPass"))
|
||||
detector._repo_root.__truediv__ = MagicMock(return_value=MagicMock())
|
||||
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert result == "DRONE"
|
||||
|
||||
def test_compound_parts_detection_strategy(self):
|
||||
"""Strategy 6 (compound): Should detect from compound underscore parts."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
# Add WIDGET as known branch but NOT "SOME_WIDGET" — so only
|
||||
# compound splitting of "some_widget" -> ["some", "widget"] can match.
|
||||
detector.known_branches.add("WIDGET")
|
||||
|
||||
resolved = "/unrelated/some_widget/handler.py"
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path.parents = []
|
||||
mock_path.parent = MagicMock()
|
||||
mock_path.parent.__eq__ = MagicMock(return_value=False)
|
||||
mock_path.name = "handler.py"
|
||||
mock_path_cls.return_value = mock_path
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
detector._repo_root = MagicMock(spec=Path)
|
||||
detector._repo_root.__str__ = MagicMock(return_value=str(_FAKE_PROJECTS / "AIPass"))
|
||||
detector._repo_root.__truediv__ = MagicMock(return_value=MagicMock())
|
||||
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert result == "WIDGET"
|
||||
|
||||
def test_external_project_path_priority(self):
|
||||
"""Strategy 0 (external): Should detect external project with priority."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
|
||||
resolved = str(_FAKE_PROJECTS / "AIPL" / "src" / "polyglot" / "tests" / "test_main.py")
|
||||
|
||||
with patch(f"{mod.__name__}.Path") as mock_path_cls:
|
||||
mock_path = MagicMock(spec=Path)
|
||||
mock_path.__str__ = MagicMock(return_value=resolved)
|
||||
mock_path.resolve.return_value = mock_path
|
||||
mock_path.parents = []
|
||||
mock_path.parent = MagicMock()
|
||||
mock_path.name = "test_main.py"
|
||||
mock_path_cls.return_value = mock_path
|
||||
mock_path_cls.home.return_value = _FAKE_HOME
|
||||
|
||||
detector._repo_root = MagicMock(spec=Path)
|
||||
detector._repo_root.__str__ = MagicMock(return_value=str(_FAKE_PROJECTS / "AIPass"))
|
||||
|
||||
with patch.object(
|
||||
detector,
|
||||
"_detect_from_external_project_path",
|
||||
return_value="AIPL/POLYGLOT TESTS",
|
||||
):
|
||||
result = detector.detect_from_path(resolved)
|
||||
|
||||
assert result == "AIPL/POLYGLOT TESTS"
|
||||
|
||||
|
||||
class TestDetectFromLogAdditional:
|
||||
"""Additional tests for detect_from_log covering uncovered lines."""
|
||||
|
||||
def test_log_name_matches_branch_exactly(self):
|
||||
"""Should detect branch when log name equals known branch."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.known_branches.add("BACKUP")
|
||||
|
||||
result = detector.detect_from_log("backup.log")
|
||||
assert result == "BACKUP"
|
||||
assert "backup" in detector.log_map
|
||||
|
||||
def test_cache_hit_on_stem(self):
|
||||
"""Should return cached stem from log_map."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.log_map["cached_log"] = "CACHED"
|
||||
|
||||
result = detector.detect_from_log("cached_log.log")
|
||||
assert result == "CACHED"
|
||||
|
||||
|
||||
class TestDetectFromModuleAdditional:
|
||||
"""Additional tests for detect_from_module covering uncovered lines."""
|
||||
|
||||
def test_module_cache_hit(self):
|
||||
"""Should return cached module from module_map."""
|
||||
mod = _import_branch_detector()
|
||||
detector = _make_detector_with_branches(mod)
|
||||
detector.module_map["cached.module"] = "CACHED"
|
||||
|
||||
result = detector.detect_from_module("cached.module")
|
||||
assert result == "CACHED"
|
||||
|
||||
|
||||
class TestPublicAPIFunctions:
|
||||
"""Tests for module-level public API functions."""
|
||||
|
||||
def test_detect_branch_from_path_delegates(self):
|
||||
"""detect_branch_from_path should delegate to get_detector."""
|
||||
mod = _import_branch_detector()
|
||||
mock_detector = MagicMock()
|
||||
mock_detector.detect_from_path.return_value = "PRAX"
|
||||
setattr(mod, "_detector_instance", mock_detector)
|
||||
|
||||
result = mod.detect_branch_from_path("/some/path")
|
||||
assert result == "PRAX"
|
||||
mock_detector.detect_from_path.assert_called_once_with("/some/path")
|
||||
|
||||
def test_detect_branch_from_log_delegates(self):
|
||||
"""detect_branch_from_log should delegate to get_detector."""
|
||||
mod = _import_branch_detector()
|
||||
mock_detector = MagicMock()
|
||||
mock_detector.detect_from_log.return_value = "FLOW"
|
||||
setattr(mod, "_detector_instance", mock_detector)
|
||||
|
||||
result = mod.detect_branch_from_log("flow_plan.log")
|
||||
assert result == "FLOW"
|
||||
mock_detector.detect_from_log.assert_called_once_with("flow_plan.log")
|
||||
|
||||
def test_reload_registry_delegates(self):
|
||||
"""reload_registry should delegate to get_detector."""
|
||||
mod = _import_branch_detector()
|
||||
mock_detector = MagicMock()
|
||||
setattr(mod, "_detector_instance", mock_detector)
|
||||
|
||||
mod.reload_registry()
|
||||
mock_detector.reload_registry.assert_called_once()
|
||||
|
||||
@@ -1318,3 +1318,624 @@ class TestPrintTemplate:
|
||||
mod = _load_dashboard_module()
|
||||
# Should not raise
|
||||
mod.print_template()
|
||||
|
||||
|
||||
# =============================================
|
||||
# print_introspection (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestDashboardPrintIntrospection:
|
||||
"""Tests for print_introspection -- CLI module info display."""
|
||||
|
||||
def test_prints_introspection_without_error(self):
|
||||
"""Introspection output runs without raising."""
|
||||
mod = _load_dashboard_module()
|
||||
mod.print_introspection()
|
||||
|
||||
def test_introspection_lists_template_sections(self):
|
||||
"""Introspection lists all template section names."""
|
||||
mod = _load_dashboard_module()
|
||||
mod.print_introspection()
|
||||
# console.print should have been called multiple times
|
||||
assert mod.console.print.call_count > 3
|
||||
|
||||
|
||||
# =============================================
|
||||
# print_help (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestDashboardPrintHelp:
|
||||
"""Tests for print_help -- CLI help output."""
|
||||
|
||||
def test_prints_help_without_error(self):
|
||||
"""Help output runs without raising."""
|
||||
mod = _load_dashboard_module()
|
||||
mod.print_help()
|
||||
|
||||
def test_help_lists_commands(self):
|
||||
"""Help output includes subcommand names."""
|
||||
mod = _load_dashboard_module()
|
||||
mod.print_help()
|
||||
assert mod.console.print.call_count > 5
|
||||
|
||||
|
||||
# =============================================
|
||||
# handle_command (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestDashboardHandleCommand:
|
||||
"""Tests for handle_command -- command routing."""
|
||||
|
||||
def test_returns_false_for_non_dashboard_command(self):
|
||||
"""handle_command returns False for commands other than 'dashboard'."""
|
||||
mod = _load_dashboard_module()
|
||||
assert mod.handle_command("monitor", []) is False
|
||||
|
||||
def test_no_args_calls_print_introspection(self):
|
||||
"""Bare 'dashboard' with no args shows introspection."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "print_introspection") as mock_intro:
|
||||
result = mod.handle_command("dashboard", [])
|
||||
assert result is True
|
||||
mock_intro.assert_called_once()
|
||||
|
||||
def test_help_flag_calls_print_help(self):
|
||||
"""--help flag shows help and returns True."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
for flag in ("--help", "-h", "help"):
|
||||
with _patch.object(mod, "print_help") as mock_help:
|
||||
result = mod.handle_command("dashboard", [flag])
|
||||
assert result is True
|
||||
mock_help.assert_called_once()
|
||||
|
||||
def test_status_subcommand(self):
|
||||
"""'status' subcommand dispatches to print_status."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "print_status") as mock_status:
|
||||
result = mod.handle_command("dashboard", ["status"])
|
||||
assert result is True
|
||||
mock_status.assert_called_once()
|
||||
|
||||
def test_template_subcommand(self):
|
||||
"""'template' subcommand dispatches to print_template."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "print_template") as mock_tmpl:
|
||||
result = mod.handle_command("dashboard", ["template"])
|
||||
assert result is True
|
||||
mock_tmpl.assert_called_once()
|
||||
|
||||
def test_refresh_subcommand(self):
|
||||
"""'refresh' subcommand dispatches to _handle_refresh."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "_handle_refresh") as mock_refresh:
|
||||
result = mod.handle_command("dashboard", ["refresh"])
|
||||
assert result is True
|
||||
mock_refresh.assert_called_once_with([])
|
||||
|
||||
def test_refresh_subcommand_passes_args(self):
|
||||
"""'refresh' subcommand forwards trailing args."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "_handle_refresh") as mock_refresh:
|
||||
mod.handle_command("dashboard", ["refresh", "--all"])
|
||||
mock_refresh.assert_called_once_with(["--all"])
|
||||
|
||||
def test_push_template_subcommand(self):
|
||||
"""'push-template' subcommand dispatches to _handle_push_template."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "_handle_push_template") as mock_push:
|
||||
result = mod.handle_command("dashboard", ["push-template"])
|
||||
assert result is True
|
||||
mock_push.assert_called_once_with([])
|
||||
|
||||
def test_diff_template_subcommand(self):
|
||||
"""'diff-template' subcommand dispatches to _handle_diff_template."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "_handle_diff_template") as mock_diff:
|
||||
result = mod.handle_command("dashboard", ["diff-template"])
|
||||
assert result is True
|
||||
mock_diff.assert_called_once_with([])
|
||||
|
||||
def test_template_status_subcommand(self):
|
||||
"""'template-status' subcommand dispatches to _handle_template_status."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "_handle_template_status") as mock_ts:
|
||||
result = mod.handle_command("dashboard", ["template-status"])
|
||||
assert result is True
|
||||
mock_ts.assert_called_once()
|
||||
|
||||
def test_unknown_subcommand_shows_help(self):
|
||||
"""Unknown subcommand shows help and returns True."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
with _patch.object(mod, "print_help") as mock_help:
|
||||
result = mod.handle_command("dashboard", ["bogus"])
|
||||
assert result is True
|
||||
mock_help.assert_called_once()
|
||||
|
||||
|
||||
# =============================================
|
||||
# _handle_refresh (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestHandleRefresh:
|
||||
"""Tests for _handle_refresh -- refresh command handler."""
|
||||
|
||||
def test_refresh_all(self, monkeypatch):
|
||||
"""--all flag refreshes all branches."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_all_dashboards",
|
||||
lambda: {"status": "success", "branches_updated": 3},
|
||||
)
|
||||
mod._handle_refresh(["--all"])
|
||||
|
||||
def test_refresh_all_partial(self, monkeypatch):
|
||||
"""Partial success path for --all refresh."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_all_dashboards",
|
||||
lambda: {
|
||||
"status": "partial",
|
||||
"branches_updated": 2,
|
||||
"branches_failed": 1,
|
||||
"errors": ["branch X failed"],
|
||||
},
|
||||
)
|
||||
mod._handle_refresh(["--all"])
|
||||
|
||||
def test_refresh_all_failure(self, monkeypatch):
|
||||
"""Full failure path for --all refresh."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_all_dashboards",
|
||||
lambda: {
|
||||
"status": "error",
|
||||
"branches_updated": 0,
|
||||
"branches_failed": 3,
|
||||
"errors": ["total failure"],
|
||||
},
|
||||
)
|
||||
mod._handle_refresh(["--all"])
|
||||
|
||||
def test_refresh_specific_branch_success(self, tmp_path, monkeypatch):
|
||||
"""@branch arg refreshes specific branch on success."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(mod, "_resolve_branch_path", lambda ref: tmp_path / "flow")
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_single_dashboard",
|
||||
lambda bp: {"status": "success", "branch": "FLOW"},
|
||||
)
|
||||
mod._handle_refresh(["@flow"])
|
||||
|
||||
def test_refresh_specific_branch_failure(self, tmp_path, monkeypatch):
|
||||
"""@branch arg shows error on refresh failure."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(mod, "_resolve_branch_path", lambda ref: tmp_path / "flow")
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_single_dashboard",
|
||||
lambda bp: {"status": "error", "error": "disk full"},
|
||||
)
|
||||
mod._handle_refresh(["@flow"])
|
||||
|
||||
def test_refresh_specific_branch_not_found(self, monkeypatch):
|
||||
"""@branch arg handles branch not found in registry."""
|
||||
mod = _load_dashboard_module()
|
||||
|
||||
def _raise(ref):
|
||||
"""Simulate branch not found."""
|
||||
raise FileNotFoundError("not found in registry")
|
||||
|
||||
monkeypatch.setattr(mod, "_resolve_branch_path", _raise)
|
||||
mod._handle_refresh(["@ghost"])
|
||||
|
||||
def test_refresh_cwd_success(self, tmp_path, monkeypatch):
|
||||
"""No args refreshes current branch from CWD."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_single_dashboard",
|
||||
lambda bp: {"status": "success", "branch": "PRAX"},
|
||||
)
|
||||
with _patch("pathlib.Path.cwd", return_value=tmp_path):
|
||||
mod._handle_refresh([])
|
||||
|
||||
def test_refresh_cwd_failure(self, tmp_path, monkeypatch):
|
||||
"""No args shows error on CWD refresh failure."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_single_dashboard",
|
||||
lambda bp: {"status": "error", "error": "no dashboard"},
|
||||
)
|
||||
with _patch("pathlib.Path.cwd", return_value=tmp_path):
|
||||
mod._handle_refresh([])
|
||||
|
||||
def test_refresh_cwd_walks_up_to_find_branch(self, tmp_path, monkeypatch):
|
||||
"""No args walks up directory tree to find branch root."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
# Create a branch root with .aipass marker above CWD
|
||||
branch_root = tmp_path / "src" / "aipass" / "flow"
|
||||
branch_root.mkdir(parents=True)
|
||||
(branch_root / ".aipass").mkdir()
|
||||
deep_cwd = branch_root / "apps" / "modules"
|
||||
deep_cwd.mkdir(parents=True)
|
||||
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"refresh_single_dashboard",
|
||||
lambda bp: {"status": "success", "branch": bp.name.upper()},
|
||||
)
|
||||
with _patch("pathlib.Path.cwd", return_value=deep_cwd):
|
||||
mod._handle_refresh([])
|
||||
|
||||
|
||||
# =============================================
|
||||
# _handle_push_template (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestHandlePushTemplate:
|
||||
"""Tests for _handle_push_template -- push template handler."""
|
||||
|
||||
def test_push_template_with_changes(self, monkeypatch):
|
||||
"""Push template shows changes when branches need updating."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"push_dashboard_template",
|
||||
lambda dry_run=False: {
|
||||
"branches_scanned": 5,
|
||||
"branches_updated": 2,
|
||||
"branches_created": 1,
|
||||
"branches_skipped": 2,
|
||||
"changes": [
|
||||
{
|
||||
"branch": "FLOW",
|
||||
"actions": ["removed bulletin_board"],
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
},
|
||||
)
|
||||
mod._handle_push_template([])
|
||||
|
||||
def test_push_template_dry_run(self, monkeypatch):
|
||||
"""Dry run flag is forwarded to push_dashboard_template."""
|
||||
mod = _load_dashboard_module()
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def _mock_push(dry_run=False):
|
||||
"""Capture dry_run flag."""
|
||||
captured["dry_run"] = dry_run
|
||||
return {
|
||||
"branches_scanned": 3,
|
||||
"branches_updated": 0,
|
||||
"branches_created": 0,
|
||||
"branches_skipped": 3,
|
||||
"changes": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(mod, "push_dashboard_template", _mock_push)
|
||||
mod._handle_push_template(["--dry-run"])
|
||||
assert captured["dry_run"] is True
|
||||
|
||||
def test_push_template_with_errors(self, monkeypatch):
|
||||
"""Push template shows errors when some branches fail."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"push_dashboard_template",
|
||||
lambda dry_run=False: {
|
||||
"branches_scanned": 3,
|
||||
"branches_updated": 1,
|
||||
"branches_created": 0,
|
||||
"branches_skipped": 1,
|
||||
"changes": [],
|
||||
"errors": ["branch X: permission denied"],
|
||||
},
|
||||
)
|
||||
mod._handle_push_template([])
|
||||
|
||||
def test_push_template_all_up_to_date(self, monkeypatch):
|
||||
"""Push template shows 'all up to date' message."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"push_dashboard_template",
|
||||
lambda dry_run=False: {
|
||||
"branches_scanned": 3,
|
||||
"branches_updated": 0,
|
||||
"branches_created": 0,
|
||||
"branches_skipped": 3,
|
||||
"changes": [],
|
||||
"errors": [],
|
||||
},
|
||||
)
|
||||
mod._handle_push_template([])
|
||||
|
||||
|
||||
# =============================================
|
||||
# _handle_diff_template (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestHandleDiffTemplate:
|
||||
"""Tests for _handle_diff_template -- diff template handler."""
|
||||
|
||||
def test_diff_template_error(self, monkeypatch):
|
||||
"""Diff template shows error when result has error key."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"diff_dashboard_template",
|
||||
lambda branch_name=None: {"error": "template not found"},
|
||||
)
|
||||
mod._handle_diff_template([])
|
||||
|
||||
def test_diff_template_with_branch_flag(self, monkeypatch):
|
||||
"""--branch flag filters diff to single branch."""
|
||||
mod = _load_dashboard_module()
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def _mock_diff(branch_name=None):
|
||||
"""Capture branch_name arg."""
|
||||
captured["branch_name"] = branch_name
|
||||
return {
|
||||
"summary": {
|
||||
"needs_update": 0,
|
||||
"up_to_date": 1,
|
||||
"missing": 0,
|
||||
},
|
||||
"branches": [],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(mod, "diff_dashboard_template", _mock_diff)
|
||||
mod._handle_diff_template(["--branch", "FLOW"])
|
||||
assert captured["branch_name"] == "FLOW"
|
||||
|
||||
def test_diff_template_branch_flag_missing_name(self):
|
||||
"""--branch without name prints error."""
|
||||
mod = _load_dashboard_module()
|
||||
mod._handle_diff_template(["--branch"])
|
||||
mod.error.assert_called()
|
||||
|
||||
def test_diff_template_with_changes(self, monkeypatch):
|
||||
"""Diff template shows additions, removals, and modifications."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"diff_dashboard_template",
|
||||
lambda branch_name=None: {
|
||||
"summary": {
|
||||
"needs_update": 1,
|
||||
"up_to_date": 1,
|
||||
"missing": 1,
|
||||
"invalid_json": 1,
|
||||
},
|
||||
"branches": [
|
||||
{
|
||||
"branch": "FLOW",
|
||||
"status": "needs_update",
|
||||
"additions": ["+ commons_activity"],
|
||||
"removals": ["- bulletin_board"],
|
||||
"modifications": ["~ ai_mail.new default changed"],
|
||||
},
|
||||
{"branch": "MISSING", "status": "missing"},
|
||||
{"branch": "GOOD", "status": "up_to_date"},
|
||||
],
|
||||
},
|
||||
)
|
||||
mod._handle_diff_template([])
|
||||
|
||||
|
||||
# =============================================
|
||||
# _handle_template_status (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestHandleTemplateStatus:
|
||||
"""Tests for _handle_template_status -- template status display."""
|
||||
|
||||
def test_template_status_basic(self, monkeypatch):
|
||||
"""Template status displays version info."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"get_template_status",
|
||||
lambda: {
|
||||
"templates_dir": "/path/to/templates",
|
||||
"template_exists": True,
|
||||
"version": "3.0.0",
|
||||
"last_updated": "2026-03-01",
|
||||
"updated_by": "prax",
|
||||
"last_push": "2026-03-02",
|
||||
"last_push_branches": ["FLOW", "AI_MAIL"],
|
||||
"changes": [],
|
||||
},
|
||||
)
|
||||
mod._handle_template_status()
|
||||
|
||||
def test_template_status_with_many_branches(self, monkeypatch):
|
||||
"""Template status truncates branch list at 5 with ellipsis."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"get_template_status",
|
||||
lambda: {
|
||||
"templates_dir": "/path/to/templates",
|
||||
"template_exists": True,
|
||||
"version": "3.0.0",
|
||||
"last_updated": "2026-03-01",
|
||||
"updated_by": "prax",
|
||||
"last_push": "2026-03-02",
|
||||
"last_push_branches": [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
],
|
||||
"changes": ["added commons"],
|
||||
},
|
||||
)
|
||||
mod._handle_template_status()
|
||||
|
||||
def test_template_status_missing(self, monkeypatch):
|
||||
"""Template status handles missing template."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"get_template_status",
|
||||
lambda: {
|
||||
"templates_dir": "/path/to/templates",
|
||||
"template_exists": False,
|
||||
"version": None,
|
||||
"last_updated": None,
|
||||
"updated_by": None,
|
||||
"last_push": None,
|
||||
"last_push_branches": [],
|
||||
"changes": [],
|
||||
},
|
||||
)
|
||||
mod._handle_template_status()
|
||||
|
||||
def test_template_status_no_push(self, monkeypatch):
|
||||
"""Template status handles never-pushed template."""
|
||||
mod = _load_dashboard_module()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"get_template_status",
|
||||
lambda: {
|
||||
"templates_dir": "/path/to/templates",
|
||||
"template_exists": True,
|
||||
"version": "1.0.0",
|
||||
"last_updated": "2026-01-01",
|
||||
"updated_by": "dev",
|
||||
"last_push": None,
|
||||
"last_push_branches": [],
|
||||
"changes": [],
|
||||
},
|
||||
)
|
||||
mod._handle_template_status()
|
||||
|
||||
|
||||
# =============================================
|
||||
# _resolve_branch_path (dashboard.py wrapper)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestResolveBranchPathWrapper:
|
||||
"""Tests for _resolve_branch_path -- dashboard module wrapper."""
|
||||
|
||||
def test_delegates_to_handler(self, monkeypatch):
|
||||
"""Wrapper delegates to resolve_branch_path handler."""
|
||||
mod = _load_dashboard_module()
|
||||
from pathlib import Path
|
||||
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"resolve_branch_path",
|
||||
lambda ref: Path("/fake/flow"),
|
||||
)
|
||||
result = mod._resolve_branch_path("@flow")
|
||||
assert result == Path("/fake/flow")
|
||||
|
||||
|
||||
# =============================================
|
||||
# main (dashboard.py)
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestDashboardMain:
|
||||
"""Tests for main -- CLI entry point."""
|
||||
|
||||
def test_main_no_args(self, monkeypatch):
|
||||
"""No args shows introspection."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["dashboard"])
|
||||
with _patch.object(mod, "print_introspection") as mock_intro:
|
||||
mod.main()
|
||||
mock_intro.assert_called_once()
|
||||
|
||||
def test_main_help_flag(self, monkeypatch):
|
||||
"""--help flag shows help."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["dashboard", "--help"])
|
||||
with _patch.object(mod, "print_help") as mock_help:
|
||||
mod.main()
|
||||
mock_help.assert_called_once()
|
||||
|
||||
def test_main_h_flag(self, monkeypatch):
|
||||
"""-h flag shows help."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["dashboard", "-h"])
|
||||
with _patch.object(mod, "print_help") as mock_help:
|
||||
mod.main()
|
||||
mock_help.assert_called_once()
|
||||
|
||||
def test_main_valid_command(self, monkeypatch):
|
||||
"""Valid command delegates to handle_command."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["dashboard", "dashboard", "status"])
|
||||
with _patch.object(mod, "handle_command", return_value=True) as mock_hc:
|
||||
mod.main()
|
||||
mock_hc.assert_called_once_with("dashboard", ["status"])
|
||||
|
||||
def test_main_unknown_command(self, monkeypatch):
|
||||
"""Unknown command shows error and help."""
|
||||
mod = _load_dashboard_module()
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["dashboard", "bogus"])
|
||||
with (
|
||||
_patch.object(mod, "handle_command", return_value=False),
|
||||
_patch.object(mod, "print_help") as mock_help,
|
||||
):
|
||||
mod.main()
|
||||
mock_help.assert_called_once()
|
||||
mod.error.assert_called()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -155,7 +155,7 @@
|
||||
"content_hash": "a4cf0a8e3b4f",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f015": {
|
||||
"f026": {
|
||||
"path": "apps/modules/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "e3b0c44298fc",
|
||||
@@ -263,7 +263,7 @@
|
||||
"content_hash": "28e9ae373563",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f026": {
|
||||
"f015": {
|
||||
"path": "apps/plugins/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "e3b0c44298fc",
|
||||
|
||||
Reference in New Issue
Block a user