Windows CI cross-platform fixes (14 failures -> windows-setup green, PR659). Unmasked by the #691 collection fix; 6 branches. CAUSE 1 pid-liveness (ai_mail/flow/hooks/skills): production _is_pid_alive already cross-plat (ctypes OpenProcess on win32), but tests mocked os.kill which the win32 path never reaches -> pinned sys.platform=linux (or patched _is_pid_alive) so tests exercise the POSIX contract on every platform. CAUSE 2 path assumptions: prax jsonl str(Path) backslash, hooks rollover repr()/%r, ai_mail darwin lsof fixed posix path, seedgo is_bypassed Path.as_posix normalization (only production change). 10 files (9 test, 1 code). Owners self-fixed; devpulse verified diffs + Linux no-regression 525 changed-test green. CI Windows verifies.
This commit is contained in:
@@ -34,6 +34,22 @@ PyPI version — not the changelog header.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **Windows CI cross-platform fixes — `windows-setup` green (PR659).** Fixing the
|
||||||
|
telegram collection errors unmasked 14 pre-existing Windows-only failures across
|
||||||
|
six branches. Two root causes. **(1) pid-liveness tests** (ai_mail, flow, hooks,
|
||||||
|
skills) mocked `os.kill`, but the production `_is_pid_alive` already branches to a
|
||||||
|
ctypes `OpenProcess` path on Windows and never reaches `os.kill`, so the mocks had
|
||||||
|
no effect and the real path ran instead — pinned `sys.platform` to `linux` in those
|
||||||
|
tests (or patched `_is_pid_alive` directly) so they exercise the POSIX contract
|
||||||
|
deterministically on every platform. **(2) POSIX path assumptions** — prax's jsonl
|
||||||
|
test hardcoded `/some/path` (backslashes under `str(Path)` on Windows) now asserts
|
||||||
|
against `str(test_path)`; hooks' rollover test compares `repr()` (matches `%r`
|
||||||
|
logging); ai_mail's darwin lsof-parser test uses a fixed POSIX path; and seedgo's
|
||||||
|
`is_bypassed()` now normalizes the rule file via `Path(rule_file).as_posix()` before
|
||||||
|
matching (the one production fix — Windows backslash rule paths never matched the
|
||||||
|
forward-slash file path). 10 files (9 test, 1 code); owners self-fixed, devpulse
|
||||||
|
verified every diff + Linux no-regression (525 changed-test assertions green).
|
||||||
|
|
||||||
- **Flaky `test_deletes_old_system_log` made deterministic (@prax log-sweep tests).**
|
- **Flaky `test_deletes_old_system_log` made deterministic (@prax log-sweep tests).**
|
||||||
The sweep integration test reached `log_watchdog._get_system_logs_dir` through a
|
The sweep integration test reached `log_watchdog._get_system_logs_dir` through a
|
||||||
`_get_sweep()` wrapper and patched it by string path; a sibling test
|
`_get_sweep()` wrapper and patched it by string path; a sibling test
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
"""Tests for dispatch daemon handler -- config loading, state management, inbox scanning."""
|
"""Tests for dispatch daemon handler -- config loading, state management, inbox scanning."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import MagicMock, mock_open, patch
|
||||||
|
|
||||||
import aipass.ai_mail.apps.handlers.dispatch.daemon as daemon_mod
|
import aipass.ai_mail.apps.handlers.dispatch.daemon as daemon_mod
|
||||||
from aipass.ai_mail.apps.handlers.dispatch.daemon import (
|
from aipass.ai_mail.apps.handlers.dispatch.daemon import (
|
||||||
@@ -25,6 +26,17 @@ from aipass.ai_mail.apps.handlers.dispatch.daemon import (
|
|||||||
get_registered_branches,
|
get_registered_branches,
|
||||||
check_inbox_for_dispatch,
|
check_inbox_for_dispatch,
|
||||||
is_protected_branch,
|
is_protected_branch,
|
||||||
|
_handle_signal,
|
||||||
|
_check_lock,
|
||||||
|
_acquire_lock,
|
||||||
|
_is_registered_sender,
|
||||||
|
poll_cycle,
|
||||||
|
_write_pid_file,
|
||||||
|
_remove_pid_file,
|
||||||
|
_read_session_type,
|
||||||
|
_is_branch_occupied,
|
||||||
|
spawn_agent,
|
||||||
|
run_daemon,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -764,26 +776,6 @@ def test_poll_cycle_absolute_path_unchanged(tmp_path, monkeypatch):
|
|||||||
assert spawned_paths[0] == branch_dir
|
assert spawned_paths[0] == branch_dir
|
||||||
|
|
||||||
|
|
||||||
# ---- Additional imports for new tests --------------------------------
|
|
||||||
|
|
||||||
import os
|
|
||||||
from unittest.mock import MagicMock, mock_open
|
|
||||||
|
|
||||||
from aipass.ai_mail.apps.handlers.dispatch.daemon import (
|
|
||||||
_handle_signal,
|
|
||||||
_check_lock,
|
|
||||||
_acquire_lock,
|
|
||||||
_is_registered_sender,
|
|
||||||
poll_cycle,
|
|
||||||
_write_pid_file,
|
|
||||||
_remove_pid_file,
|
|
||||||
_read_session_type,
|
|
||||||
_is_branch_occupied,
|
|
||||||
spawn_agent,
|
|
||||||
run_daemon,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- _handle_signal tests --------------------------------------
|
# ---- _handle_signal tests --------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@@ -1003,6 +995,7 @@ def test_write_pid_file_existing_dead_pid(tmp_path, monkeypatch):
|
|||||||
|
|
||||||
def test_write_pid_file_existing_permission_error(tmp_path, monkeypatch):
|
def test_write_pid_file_existing_permission_error(tmp_path, monkeypatch):
|
||||||
"""Existing PID file with PermissionError on kill returns False."""
|
"""Existing PID file with PermissionError on kill returns False."""
|
||||||
|
monkeypatch.setattr("sys.platform", "linux")
|
||||||
pid_file = tmp_path / "daemon.pid"
|
pid_file = tmp_path / "daemon.pid"
|
||||||
pid_file.write_text("888888", encoding="utf-8")
|
pid_file.write_text("888888", encoding="utf-8")
|
||||||
monkeypatch.setattr(daemon_mod, "DAEMON_PID_FILE", pid_file)
|
monkeypatch.setattr(daemon_mod, "DAEMON_PID_FILE", pid_file)
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ def test_check_pid_alive_dead(monkeypatch):
|
|||||||
|
|
||||||
def test_check_pid_alive_permission_error(monkeypatch):
|
def test_check_pid_alive_permission_error(monkeypatch):
|
||||||
"""PermissionError means process exists but cannot signal -- returns True."""
|
"""PermissionError means process exists but cannot signal -- returns True."""
|
||||||
|
monkeypatch.setattr("sys.platform", "linux")
|
||||||
monkeypatch.setattr(os, "kill", _raise_permission)
|
monkeypatch.setattr(os, "kill", _raise_permission)
|
||||||
assert _check_pid_alive(1) is True
|
assert _check_pid_alive(1) is True
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ def test_get_pid_cwd_linux_oserror(monkeypatch):
|
|||||||
def test_get_pid_cwd_darwin(monkeypatch, tmp_path):
|
def test_get_pid_cwd_darwin(monkeypatch, tmp_path):
|
||||||
"""macOS: reads cwd via lsof."""
|
"""macOS: reads cwd via lsof."""
|
||||||
monkeypatch.setattr("sys.platform", "darwin")
|
monkeypatch.setattr("sys.platform", "darwin")
|
||||||
target = str(tmp_path / "project")
|
target = "/tmp/pytest-project"
|
||||||
|
|
||||||
class FakeResult:
|
class FakeResult:
|
||||||
returncode = 0
|
returncode = 0
|
||||||
@@ -362,6 +363,7 @@ def test_check_lock_stale_old_timestamp(tmp_path, monkeypatch):
|
|||||||
|
|
||||||
def test_check_lock_permission_error_treated_active(tmp_path, monkeypatch):
|
def test_check_lock_permission_error_treated_active(tmp_path, monkeypatch):
|
||||||
"""Lock PID that raises PermissionError is treated as active."""
|
"""Lock PID that raises PermissionError is treated as active."""
|
||||||
|
monkeypatch.setattr("sys.platform", "linux")
|
||||||
lock_dir = tmp_path / ".ai_mail.local"
|
lock_dir = tmp_path / ".ai_mail.local"
|
||||||
lock_dir.mkdir(parents=True)
|
lock_dir.mkdir(parents=True)
|
||||||
lock_file = lock_dir / ".dispatch.lock"
|
lock_file = lock_dir / ".dispatch.lock"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class TestIsLockStale:
|
|||||||
mod = _import_lock_ops()
|
mod = _import_lock_ops()
|
||||||
lock = tmp_path / ".test.lock"
|
lock = tmp_path / ".test.lock"
|
||||||
lock.write_text("999999999", encoding="utf-8")
|
lock.write_text("999999999", encoding="utf-8")
|
||||||
with patch(f"{_MOD}.os.kill", side_effect=ProcessLookupError):
|
with patch(f"{_MOD}._pid_alive", return_value=False):
|
||||||
result = mod.is_lock_stale(lock)
|
result = mod.is_lock_stale(lock)
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
@@ -100,11 +100,11 @@ class TestIsLockStale:
|
|||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
def test_permission_error_treated_as_alive(self, tmp_path):
|
def test_permission_error_treated_as_alive(self, tmp_path):
|
||||||
"""PermissionError from os.kill means process exists — lock valid."""
|
"""When _pid_alive says process exists, lock is valid (not stale)."""
|
||||||
mod = _import_lock_ops()
|
mod = _import_lock_ops()
|
||||||
lock = tmp_path / ".test.lock"
|
lock = tmp_path / ".test.lock"
|
||||||
lock.write_text("1", encoding="utf-8")
|
lock.write_text("1", encoding="utf-8")
|
||||||
with patch(f"{_MOD}.os.kill", side_effect=PermissionError):
|
with patch(f"{_MOD}._pid_alive", return_value=True):
|
||||||
result = mod.is_lock_stale(lock)
|
result = mod.is_lock_stale(lock)
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ class TestAcquireLock:
|
|||||||
mod = _import_lock_ops()
|
mod = _import_lock_ops()
|
||||||
lock = tmp_path / ".test.lock"
|
lock = tmp_path / ".test.lock"
|
||||||
lock.write_text("999999999", encoding="utf-8")
|
lock.write_text("999999999", encoding="utf-8")
|
||||||
with patch(f"{_MOD}.os.kill", side_effect=ProcessLookupError):
|
with patch(f"{_MOD}._pid_alive", return_value=False):
|
||||||
result = mod.acquire_lock(lock)
|
result = mod.acquire_lock(lock)
|
||||||
assert result is True
|
assert result is True
|
||||||
assert lock.read_text(encoding="utf-8") == str(os.getpid())
|
assert lock.read_text(encoding="utf-8") == str(os.getpid())
|
||||||
@@ -149,7 +149,7 @@ class TestAcquireLock:
|
|||||||
lock = tmp_path / ".test.lock"
|
lock = tmp_path / ".test.lock"
|
||||||
lock.write_text("999999999", encoding="utf-8")
|
lock.write_text("999999999", encoding="utf-8")
|
||||||
with (
|
with (
|
||||||
patch(f"{_MOD}.os.kill", side_effect=ProcessLookupError),
|
patch(f"{_MOD}._pid_alive", return_value=False),
|
||||||
patch.object(Path, "unlink", side_effect=OSError("permission denied")),
|
patch.object(Path, "unlink", side_effect=OSError("permission denied")),
|
||||||
):
|
):
|
||||||
result = mod.acquire_lock(lock)
|
result = mod.acquire_lock(lock)
|
||||||
@@ -170,7 +170,7 @@ class TestAcquireLock:
|
|||||||
mod = _import_lock_ops()
|
mod = _import_lock_ops()
|
||||||
lock = tmp_path / ".test.lock"
|
lock = tmp_path / ".test.lock"
|
||||||
lock.write_text("999999999", encoding="utf-8")
|
lock.write_text("999999999", encoding="utf-8")
|
||||||
with patch(f"{_MOD}.os.kill", side_effect=ProcessLookupError):
|
with patch(f"{_MOD}._pid_alive", return_value=False):
|
||||||
mod.acquire_lock(lock)
|
mod.acquire_lock(lock)
|
||||||
call_args = mock_json_handler.call_args
|
call_args = mock_json_handler.call_args
|
||||||
assert call_args[0][1]["stale_recovery"] is True
|
assert call_args[0][1]["stale_recovery"] is True
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class TestIsPidAlive:
|
|||||||
assert cc_sessions._is_pid_alive(1) is False
|
assert cc_sessions._is_pid_alive(1) is False
|
||||||
|
|
||||||
def test_permission_error_treated_as_alive(self):
|
def test_permission_error_treated_as_alive(self):
|
||||||
with patch("os.kill", side_effect=PermissionError("denied")):
|
with patch("sys.platform", "linux"), patch("os.kill", side_effect=PermissionError("denied")):
|
||||||
assert cc_sessions._is_pid_alive(42) is True
|
assert cc_sessions._is_pid_alive(42) is True
|
||||||
|
|
||||||
def test_oserror_treated_as_dead(self):
|
def test_oserror_treated_as_dead(self):
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ class TestReadAll:
|
|||||||
|
|
||||||
class TestLiveness:
|
class TestLiveness:
|
||||||
def test_is_pid_alive_true(self):
|
def test_is_pid_alive_true(self):
|
||||||
with patch("os.kill") as mock_kill:
|
with patch("sys.platform", "linux"), patch("os.kill") as mock_kill:
|
||||||
assert presence._is_pid_alive(1234) is True
|
assert presence._is_pid_alive(1234) is True
|
||||||
mock_kill.assert_called_once_with(1234, 0)
|
mock_kill.assert_called_once_with(1234, 0)
|
||||||
|
|
||||||
@@ -444,7 +444,7 @@ class TestLiveness:
|
|||||||
assert presence._is_pid_alive(1234) is False
|
assert presence._is_pid_alive(1234) is False
|
||||||
|
|
||||||
def test_is_pid_alive_permission_error(self):
|
def test_is_pid_alive_permission_error(self):
|
||||||
with patch("os.kill", side_effect=PermissionError):
|
with patch("sys.platform", "linux"), patch("os.kill", side_effect=PermissionError):
|
||||||
assert presence._is_pid_alive(1234) is True
|
assert presence._is_pid_alive(1234) is True
|
||||||
|
|
||||||
def test_cwd_matches_linux(self):
|
def test_cwd_matches_linux(self):
|
||||||
|
|||||||
@@ -166,4 +166,4 @@ class TestFindRepoRootFailLoud:
|
|||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
assert "_find_repo_root failed" in caplog.text
|
assert "_find_repo_root failed" in caplog.text
|
||||||
assert bad_home in caplog.text
|
assert repr(bad_home) in caplog.text
|
||||||
|
|||||||
@@ -69,11 +69,12 @@ class TestAppendJsonl:
|
|||||||
"""Verify non-serializable types fall back to str()."""
|
"""Verify non-serializable types fall back to str()."""
|
||||||
append_jsonl = _get_append_jsonl()
|
append_jsonl = _get_append_jsonl()
|
||||||
target = tmp_path / "test.jsonl"
|
target = tmp_path / "test.jsonl"
|
||||||
|
test_path = Path("/some/path")
|
||||||
|
|
||||||
append_jsonl(target, {"path": Path("/some/path")})
|
append_jsonl(target, {"path": test_path})
|
||||||
|
|
||||||
line = json.loads(target.read_text().strip())
|
line = json.loads(target.read_text().strip())
|
||||||
assert line["path"] == "/some/path"
|
assert line["path"] == str(test_path)
|
||||||
|
|
||||||
|
|
||||||
class TestRotation:
|
class TestRotation:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def is_bypassed(
|
|||||||
if rule.get("standard") and rule.get("standard") != standard:
|
if rule.get("standard") and rule.get("standard") != standard:
|
||||||
continue
|
continue
|
||||||
rule_file = rule.get("file", "")
|
rule_file = rule.get("file", "")
|
||||||
if rule_file and rule_file not in file_path_posix:
|
if rule_file and Path(rule_file).as_posix() not in file_path_posix:
|
||||||
continue
|
continue
|
||||||
functions = rule.get("functions")
|
functions = rule.get("functions")
|
||||||
if functions and name is not None:
|
if functions and name is not None:
|
||||||
|
|||||||
@@ -923,28 +923,28 @@ class TestCreateCommand:
|
|||||||
mock_validate.return_value = {"name": "dev_central", "path": "/home/aipass/dev_central"}
|
mock_validate.return_value = {"name": "dev_central", "path": "/home/aipass/dev_central"}
|
||||||
self.bot._handle_create_command(self.chat_id, "chat dev_central")
|
self.bot._handle_create_command(self.chat_id, "chat dev_central")
|
||||||
assert self.chat_id in self.bot._create_state
|
assert self.chat_id in self.bot._create_state
|
||||||
self.bot.send_message.assert_called_once()
|
self.bot.send_message.assert_called_once() # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1] # type: ignore[union-attr]
|
||||||
assert "dev_central" in msg
|
assert "dev_central" in msg
|
||||||
assert "token" in msg.lower()
|
assert "token" in msg.lower()
|
||||||
|
|
||||||
def test_create_missing_args(self):
|
def test_create_missing_args(self):
|
||||||
self.bot._handle_create_command(self.chat_id, "")
|
self.bot._handle_create_command(self.chat_id, "")
|
||||||
self.bot.send_message.assert_called_once()
|
self.bot.send_message.assert_called_once() # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1] # type: ignore[union-attr]
|
||||||
assert "Usage" in msg
|
assert "Usage" in msg
|
||||||
|
|
||||||
def test_create_invalid_format(self):
|
def test_create_invalid_format(self):
|
||||||
self.bot._handle_create_command(self.chat_id, "foo bar")
|
self.bot._handle_create_command(self.chat_id, "foo bar")
|
||||||
self.bot.send_message.assert_called_once()
|
self.bot.send_message.assert_called_once() # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1] # type: ignore[union-attr]
|
||||||
assert "Usage" in msg
|
assert "Usage" in msg
|
||||||
|
|
||||||
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.validate_branch", return_value=None)
|
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.validate_branch", return_value=None)
|
||||||
def test_create_branch_not_found(self, mock_validate):
|
def test_create_branch_not_found(self, mock_validate):
|
||||||
self.bot._handle_create_command(self.chat_id, "chat nonexistent")
|
self.bot._handle_create_command(self.chat_id, "chat nonexistent")
|
||||||
self.bot.send_message.assert_called_once()
|
self.bot.send_message.assert_called_once() # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1] # type: ignore[union-attr]
|
||||||
assert "not found" in msg
|
assert "not found" in msg
|
||||||
|
|
||||||
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.get_bot_by_branch")
|
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.get_bot_by_branch")
|
||||||
@@ -953,8 +953,8 @@ class TestCreateCommand:
|
|||||||
mock_validate.return_value = {"name": "dev_central", "path": "/tmp"}
|
mock_validate.return_value = {"name": "dev_central", "path": "/tmp"}
|
||||||
mock_get_bot.return_value = {"bot_id": "dev_central", "username": "dc_bot"}
|
mock_get_bot.return_value = {"bot_id": "dev_central", "username": "dc_bot"}
|
||||||
self.bot._handle_create_command(self.chat_id, "chat dev_central")
|
self.bot._handle_create_command(self.chat_id, "chat dev_central")
|
||||||
self.bot.send_message.assert_called_once()
|
self.bot.send_message.assert_called_once() # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1] # type: ignore[union-attr]
|
||||||
assert "already has a bot" in msg
|
assert "already has a bot" in msg
|
||||||
|
|
||||||
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.get_bot_by_branch", return_value=None)
|
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.get_bot_by_branch", return_value=None)
|
||||||
@@ -970,8 +970,8 @@ class TestCreateCommand:
|
|||||||
|
|
||||||
def test_create_single_arg_no_branch(self):
|
def test_create_single_arg_no_branch(self):
|
||||||
"""Calling /create with only 'chat' and no branch name shows usage."""
|
"""Calling /create with only 'chat' and no branch name shows usage."""
|
||||||
self.bot._handle_create_command(self.chat_id, "chat")
|
self.bot._handle_create_command(self.chat_id, "chat") # type: ignore[union-attr]
|
||||||
self.bot.send_message.assert_called_once()
|
self.bot.send_message.assert_called_once() # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "Usage" in msg
|
assert "Usage" in msg
|
||||||
|
|
||||||
@@ -1022,13 +1022,13 @@ class TestCreateToken:
|
|||||||
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr")
|
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr")
|
||||||
# Should have been called
|
# Should have been called
|
||||||
mock_create_bot.assert_called_once()
|
mock_create_bot.assert_called_once()
|
||||||
# Last send_message should contain success info
|
# Last send_message should contain success info # type: ignore[union-attr]
|
||||||
last_msg = self.bot.send_message.call_args[0][1]
|
last_msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "my_new_bot" in last_msg
|
assert "my_new_bot" in last_msg
|
||||||
|
|
||||||
def test_invalid_token_format_no_colon(self):
|
def test_invalid_token_format_no_colon(self):
|
||||||
self._set_create_state()
|
self._set_create_state()
|
||||||
self.bot._handle_create_token(self.chat_id, "shorttoken")
|
self.bot._handle_create_token(self.chat_id, "shorttoken") # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "doesn't look like a valid" in msg
|
assert "doesn't look like a valid" in msg
|
||||||
# State should still be present (user can retry)
|
# State should still be present (user can retry)
|
||||||
@@ -1036,7 +1036,7 @@ class TestCreateToken:
|
|||||||
|
|
||||||
def test_invalid_token_format_too_short(self):
|
def test_invalid_token_format_too_short(self):
|
||||||
self._set_create_state()
|
self._set_create_state()
|
||||||
self.bot._handle_create_token(self.chat_id, "1:A")
|
self.bot._handle_create_token(self.chat_id, "1:A") # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "doesn't look like a valid" in msg
|
assert "doesn't look like a valid" in msg
|
||||||
|
|
||||||
@@ -1044,7 +1044,7 @@ class TestCreateToken:
|
|||||||
def test_token_validation_fails(self, mock_validate_token):
|
def test_token_validation_fails(self, mock_validate_token):
|
||||||
self._set_create_state()
|
self._set_create_state()
|
||||||
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr")
|
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr")
|
||||||
mock_validate_token.assert_called_once()
|
mock_validate_token.assert_called_once() # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "validation failed" in msg.lower()
|
assert "validation failed" in msg.lower()
|
||||||
|
|
||||||
@@ -1053,7 +1053,7 @@ class TestCreateToken:
|
|||||||
def test_bot_creation_fails(self, mock_validate_token, mock_create_bot):
|
def test_bot_creation_fails(self, mock_validate_token, mock_create_bot):
|
||||||
self._set_create_state()
|
self._set_create_state()
|
||||||
mock_validate_token.return_value = {"username": "test_bot"}
|
mock_validate_token.return_value = {"username": "test_bot"}
|
||||||
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr")
|
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr") # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "failed" in msg.lower()
|
assert "failed" in msg.lower()
|
||||||
|
|
||||||
@@ -1061,7 +1061,7 @@ class TestCreateToken:
|
|||||||
"""State older than _create_state_ttl should be rejected."""
|
"""State older than _create_state_ttl should be rejected."""
|
||||||
old_time = time.time() - 600 # 10 minutes ago, TTL is 300s
|
old_time = time.time() - 600 # 10 minutes ago, TTL is 300s
|
||||||
self._set_create_state(started_at=old_time)
|
self._set_create_state(started_at=old_time)
|
||||||
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr")
|
self.bot._handle_create_token(self.chat_id, "123456789:ABCdefGHIjklMNOpqr") # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "expired" in msg.lower()
|
assert "expired" in msg.lower()
|
||||||
assert self.chat_id not in self.bot._create_state
|
assert self.chat_id not in self.bot._create_state
|
||||||
@@ -1125,7 +1125,7 @@ class TestCancelCommand:
|
|||||||
}
|
}
|
||||||
with patch("aipass.skills.lib.telegram.apps.handlers.base_bot.parse_command", return_value=("cancel", "")):
|
with patch("aipass.skills.lib.telegram.apps.handlers.base_bot.parse_command", return_value=("cancel", "")):
|
||||||
self.bot.process_update(update)
|
self.bot.process_update(update)
|
||||||
assert self.chat_id not in self.bot._create_state
|
assert self.chat_id not in self.bot._create_state # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "cancelled" in msg.lower()
|
assert "cancelled" in msg.lower()
|
||||||
|
|
||||||
@@ -1140,7 +1140,7 @@ class TestCancelCommand:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
with patch("aipass.skills.lib.telegram.apps.handlers.base_bot.parse_command", return_value=("cancel", "")):
|
with patch("aipass.skills.lib.telegram.apps.handlers.base_bot.parse_command", return_value=("cancel", "")):
|
||||||
self.bot.process_update(update)
|
self.bot.process_update(update) # type: ignore[union-attr]
|
||||||
msg = self.bot.send_message.call_args[0][1]
|
msg = self.bot.send_message.call_args[0][1]
|
||||||
assert "Nothing to cancel" in msg
|
assert "Nothing to cancel" in msg
|
||||||
|
|
||||||
@@ -1711,6 +1711,7 @@ class TestLockPidReuse:
|
|||||||
assert self.bot._check_lock() is False
|
assert self.bot._check_lock() is False
|
||||||
assert not self.bot._lock_file.exists()
|
assert not self.bot._lock_file.exists()
|
||||||
|
|
||||||
|
@patch("sys.platform", "linux")
|
||||||
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.os.kill")
|
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.os.kill")
|
||||||
def test_alive_pid_same_bot_returns_true(self, mock_kill):
|
def test_alive_pid_same_bot_returns_true(self, mock_kill):
|
||||||
"""Live PID running this bot returns True (lock held)."""
|
"""Live PID running this bot returns True (lock held)."""
|
||||||
@@ -1726,6 +1727,7 @@ class TestLockPidReuse:
|
|||||||
assert self.bot._check_lock() is True
|
assert self.bot._check_lock() is True
|
||||||
assert self.bot._lock_file.exists() # Lock preserved
|
assert self.bot._lock_file.exists() # Lock preserved
|
||||||
|
|
||||||
|
@patch("sys.platform", "linux")
|
||||||
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.os.kill")
|
@patch("aipass.skills.lib.telegram.apps.handlers.base_bot.os.kill")
|
||||||
def test_alive_pid_different_bot_cleans_lock(self, mock_kill):
|
def test_alive_pid_different_bot_cleans_lock(self, mock_kill):
|
||||||
"""Live PID running a DIFFERENT bot cleans stale lock (PID reuse)."""
|
"""Live PID running a DIFFERENT bot cleans stale lock (PID reuse)."""
|
||||||
|
|||||||
@@ -221,12 +221,12 @@ class TestIsPidAlive:
|
|||||||
|
|
||||||
def test_permission_error_treated_as_alive(self):
|
def test_permission_error_treated_as_alive(self):
|
||||||
"""Treats PermissionError from os.kill as evidence the PID is alive."""
|
"""Treats PermissionError from os.kill as evidence the PID is alive."""
|
||||||
with patch("os.kill", side_effect=PermissionError("denied")):
|
with patch("sys.platform", "linux"), patch("os.kill", side_effect=PermissionError("denied")):
|
||||||
assert BaseBot._is_pid_alive(42) is True
|
assert BaseBot._is_pid_alive(42) is True
|
||||||
|
|
||||||
def test_os_error_treated_as_dead(self):
|
def test_os_error_treated_as_dead(self):
|
||||||
"""Treats a generic OSError from os.kill as evidence the PID is dead."""
|
"""Treats a generic OSError from os.kill as evidence the PID is dead."""
|
||||||
with patch("os.kill", side_effect=OSError("some error")):
|
with patch("sys.platform", "linux"), patch("os.kill", side_effect=OSError("some error")):
|
||||||
assert BaseBot._is_pid_alive(42) is False
|
assert BaseBot._is_pid_alive(42) is False
|
||||||
|
|
||||||
|
|
||||||
@@ -546,7 +546,7 @@ class TestHandleMessageNoSession:
|
|||||||
patch("subprocess.run", return_value=MagicMock(returncode=1)),
|
patch("subprocess.run", return_value=MagicMock(returncode=1)),
|
||||||
):
|
):
|
||||||
bot.handle_message(42, "hello", {"message_id": 1})
|
bot.handle_message(42, "hello", {"message_id": 1})
|
||||||
msg = bot.send_message.call_args[0][1]
|
msg = bot.send_message.call_args[0][1] # type: ignore[union-attr]
|
||||||
assert "No live Claude session" in msg
|
assert "No live Claude session" in msg
|
||||||
assert "api" in msg
|
assert "api" in msg
|
||||||
|
|
||||||
@@ -559,5 +559,5 @@ class TestHandleMessageNoSession:
|
|||||||
patch("subprocess.run", return_value=MagicMock(returncode=1)),
|
patch("subprocess.run", return_value=MagicMock(returncode=1)),
|
||||||
):
|
):
|
||||||
bot.handle_message(42, "hello", {"message_id": 1})
|
bot.handle_message(42, "hello", {"message_id": 1})
|
||||||
msg = bot.send_message.call_args[0][1]
|
msg = bot.send_message.call_args[0][1] # type: ignore[union-attr]
|
||||||
assert "No live Claude session" in msg
|
assert "No live Claude session" in msg
|
||||||
|
|||||||
Reference in New Issue
Block a user