fix: YouTube JS runtime check, Douyin health check, cli bare except, config permission race (#104)
- YouTube: warn when only Node.js is installed but yt-dlp config file is missing (previously returned "ok" incorrectly) - Douyin: use `mcporter list` instead of calling with a hardcoded invalid share URL that always fails - cli: replace bare `except:` with `except Exception:` in `_detect_environment` to avoid catching KeyboardInterrupt/SystemExit - cli: fix unclosed file handle for cloud VM detection - config: use `os.open()` with 0o600 mode to eliminate permission race window when saving credentials
This commit is contained in:
@@ -29,6 +29,82 @@ def test_channel_check_contract_with_minimal_runtime(monkeypatch, tmp_path):
|
||||
assert isinstance(message, str) and message.strip()
|
||||
|
||||
|
||||
def test_youtube_warns_when_node_only_and_no_config(monkeypatch, tmp_path):
|
||||
"""YouTube should warn when only Node.js is installed but no yt-dlp config exists."""
|
||||
from agent_reach.channels.youtube import YouTubeChannel
|
||||
|
||||
def fake_which(cmd):
|
||||
if cmd == "yt-dlp":
|
||||
return "/usr/bin/yt-dlp"
|
||||
if cmd == "node":
|
||||
return "/usr/bin/node"
|
||||
return None # deno not installed
|
||||
|
||||
monkeypatch.setattr("shutil.which", fake_which)
|
||||
# Point to a non-existent config file
|
||||
monkeypatch.setattr("os.path.expanduser", lambda p: str(tmp_path / ".config/yt-dlp/config"))
|
||||
|
||||
ch = YouTubeChannel()
|
||||
status, message = ch.check()
|
||||
assert status == "warn"
|
||||
assert "--js-runtimes" in message
|
||||
|
||||
|
||||
def test_youtube_ok_when_deno_installed(monkeypatch):
|
||||
"""YouTube should return ok when Deno is installed (no config needed)."""
|
||||
from agent_reach.channels.youtube import YouTubeChannel
|
||||
|
||||
def fake_which(cmd):
|
||||
if cmd == "yt-dlp":
|
||||
return "/usr/bin/yt-dlp"
|
||||
if cmd == "deno":
|
||||
return "/usr/bin/deno"
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("shutil.which", fake_which)
|
||||
|
||||
ch = YouTubeChannel()
|
||||
status, _msg = ch.check()
|
||||
assert status == "ok"
|
||||
|
||||
|
||||
def test_douyin_check_does_not_call_with_invalid_url(monkeypatch, tmp_path):
|
||||
"""Douyin check should use 'mcporter list' instead of calling with a hardcoded URL."""
|
||||
import subprocess
|
||||
|
||||
from agent_reach.channels.douyin import DouyinChannel
|
||||
|
||||
calls = []
|
||||
original_run = subprocess.run
|
||||
|
||||
def tracking_run(cmd, **kwargs):
|
||||
calls.append(cmd)
|
||||
# Simulate mcporter config list returning douyin
|
||||
if "config" in cmd and "list" in cmd:
|
||||
class R:
|
||||
stdout = "douyin http://localhost:18070/mcp"
|
||||
returncode = 0
|
||||
return R()
|
||||
# Simulate mcporter list douyin returning tools
|
||||
if "list" in cmd and "douyin" in cmd:
|
||||
class R:
|
||||
stdout = "parse_douyin_video_info"
|
||||
returncode = 0
|
||||
return R()
|
||||
return original_run(cmd, **kwargs)
|
||||
|
||||
monkeypatch.setattr("shutil.which", lambda cmd: "/usr/bin/mcporter" if cmd == "mcporter" else None)
|
||||
monkeypatch.setattr("subprocess.run", tracking_run)
|
||||
|
||||
ch = DouyinChannel()
|
||||
status, _msg = ch.check()
|
||||
|
||||
# Should NOT contain any hardcoded douyin.com URL in subprocess calls
|
||||
for call in calls:
|
||||
call_str = " ".join(call) if isinstance(call, list) else str(call)
|
||||
assert "https://www.douyin.com" not in call_str
|
||||
|
||||
|
||||
def test_channel_can_handle_contract():
|
||||
url_samples = {
|
||||
"github": "https://github.com/panniantong/agent-reach",
|
||||
|
||||
@@ -78,3 +78,16 @@ class TestConfig:
|
||||
masked = tmp_config.to_dict()
|
||||
assert masked["exa_api_key"] == "super-se..."
|
||||
assert masked["normal_setting"] == "visible"
|
||||
|
||||
def test_save_creates_file_with_restricted_permissions(self, tmp_path):
|
||||
import stat
|
||||
import sys
|
||||
config_file = tmp_path / "secure_config.yaml"
|
||||
config = Config(config_path=config_file)
|
||||
config.set("secret_key", "my-secret")
|
||||
|
||||
if sys.platform != "win32":
|
||||
mode = config_file.stat().st_mode
|
||||
# File should be owner-only read/write (0o600)
|
||||
assert not (mode & stat.S_IRGRP), "group read should not be set"
|
||||
assert not (mode & stat.S_IROTH), "other read should not be set"
|
||||
|
||||
Reference in New Issue
Block a user