feat: add lockfile to prevent concurrent instances (#30)
This commit is contained in:
@@ -29,32 +29,26 @@ def _patch_config(monkeypatch, config):
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"load_telegram_config",
|
||||
lambda: (config, Path("takopi.toml")),
|
||||
lambda *args, **kwargs: (config, Path("takopi.toml")),
|
||||
)
|
||||
|
||||
|
||||
def test_parse_bridge_config_rejects_empty_token(monkeypatch) -> None:
|
||||
def test_load_and_validate_config_rejects_empty_token(monkeypatch) -> None:
|
||||
from takopi import cli
|
||||
|
||||
_patch_config(monkeypatch, {"bot_token": " ", "chat_id": 123})
|
||||
|
||||
with pytest.raises(cli.ConfigError, match="bot_token"):
|
||||
cli._parse_bridge_config(
|
||||
final_notify=True,
|
||||
default_engine_override=None,
|
||||
)
|
||||
cli.load_and_validate_config()
|
||||
|
||||
|
||||
def test_parse_bridge_config_rejects_string_chat_id(monkeypatch) -> None:
|
||||
def test_load_and_validate_config_rejects_string_chat_id(monkeypatch) -> None:
|
||||
from takopi import cli
|
||||
|
||||
_patch_config(monkeypatch, {"bot_token": "token", "chat_id": "123"})
|
||||
|
||||
with pytest.raises(cli.ConfigError, match="chat_id"):
|
||||
cli._parse_bridge_config(
|
||||
final_notify=True,
|
||||
default_engine_override=None,
|
||||
)
|
||||
cli.load_and_validate_config()
|
||||
|
||||
|
||||
def test_codex_extract_resume_finds_command() -> None:
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import takopi.lockfile as lockfile
|
||||
|
||||
|
||||
def test_lockfile_creates_and_cleans_up(tmp_path) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
config_path.write_text("ok", encoding="utf-8")
|
||||
|
||||
handle = lockfile.acquire_lock(
|
||||
config_path=config_path,
|
||||
token_fingerprint="deadbeef",
|
||||
)
|
||||
try:
|
||||
assert lockfile.lock_path_for_config(config_path).exists()
|
||||
finally:
|
||||
handle.release()
|
||||
|
||||
assert not lockfile.lock_path_for_config(config_path).exists()
|
||||
|
||||
|
||||
def test_lockfile_refuses_running_pid(tmp_path) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
config_path.write_text("ok", encoding="utf-8")
|
||||
|
||||
handle = lockfile.acquire_lock(
|
||||
config_path=config_path,
|
||||
token_fingerprint="deadbeef",
|
||||
)
|
||||
try:
|
||||
with pytest.raises(lockfile.LockError) as exc:
|
||||
lockfile.acquire_lock(
|
||||
config_path=config_path,
|
||||
token_fingerprint="deadbeef",
|
||||
)
|
||||
message = str(exc.value).lower()
|
||||
assert "already running" in message
|
||||
assert str(lockfile.lock_path_for_config(config_path)) in str(exc.value)
|
||||
finally:
|
||||
handle.release()
|
||||
|
||||
|
||||
def test_lockfile_replaces_dead_pid(tmp_path, monkeypatch) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
config_path.write_text("ok", encoding="utf-8")
|
||||
lock_path = lockfile.lock_path_for_config(config_path)
|
||||
payload = {"pid": 424242, "token_fingerprint": "deadbeef"}
|
||||
lock_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(lockfile, "_pid_running", lambda pid: False)
|
||||
|
||||
handle = lockfile.acquire_lock(
|
||||
config_path=config_path,
|
||||
token_fingerprint="deadbeef",
|
||||
)
|
||||
try:
|
||||
updated = json.loads(lock_path.read_text(encoding="utf-8"))
|
||||
assert updated["pid"] == os.getpid()
|
||||
assert updated["token_fingerprint"] == "deadbeef"
|
||||
finally:
|
||||
handle.release()
|
||||
|
||||
|
||||
def test_lockfile_replaces_token_mismatch(tmp_path) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
config_path.write_text("ok", encoding="utf-8")
|
||||
lock_path = lockfile.lock_path_for_config(config_path)
|
||||
payload = {"pid": os.getpid(), "token_fingerprint": "other"}
|
||||
lock_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
|
||||
handle = lockfile.acquire_lock(
|
||||
config_path=config_path,
|
||||
token_fingerprint="deadbeef",
|
||||
)
|
||||
try:
|
||||
updated = json.loads(lock_path.read_text(encoding="utf-8"))
|
||||
assert updated["token_fingerprint"] == "deadbeef"
|
||||
finally:
|
||||
handle.release()
|
||||
Reference in New Issue
Block a user