Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ jobs:
run: |
pytest -q

windows-test:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install package and test deps
run: |
python -m pip install --upgrade pip
pip install -c constraints.txt -e .[dev]

- name: Run tests
run: |
pytest -q

# Editable installs (-e) never exercise wheel packaging, so a broken wheel
# can pass tests and still fail every real `pip install` from source.
# This job builds the actual wheel and installs it into a clean venv.
Expand Down
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
from agent_reach.config import Config


@pytest.fixture
def symlink_or_skip():
"""Create a symlink or skip when the host has no symlink privilege."""

def create(link, target, *, target_is_directory=False):
try:
link.symlink_to(target, target_is_directory=target_is_directory)
except (NotImplementedError, OSError) as exc:
pytest.skip(f"symlink creation is unavailable: {exc}")
return link

return create


@pytest.fixture(autouse=True)
def isolated_home(tmp_path, monkeypatch):
"""Redirect every common home/config root before each test runs."""
Expand Down
10 changes: 6 additions & 4 deletions tests/test_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ def test_saved_cli_cookie_is_unverified_not_active(
assert ch.active_backend is None

def test_saved_cli_cookie_refuses_ancestor_symlink(
self, monkeypatch, isolated_home
self, monkeypatch, isolated_home, symlink_or_skip
):
self._isolate(monkeypatch)
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/xhs")
Expand All @@ -946,7 +946,8 @@ def test_saved_cli_cookie_refuses_ancestor_symlink(
json.dumps({"a1": "do-not-read"}),
encoding="utf-8",
)
(isolated_home / ".xiaohongshu-cli").symlink_to(
symlink_or_skip(
isolated_home / ".xiaohongshu-cli",
victim_dir,
target_is_directory=True,
)
Expand Down Expand Up @@ -1414,7 +1415,7 @@ def test_hosts_metadata_is_read_without_exposing_token(
assert "super-secret-token" not in message

def test_hosts_metadata_refuses_ancestor_symlink(
self, monkeypatch, isolated_home
self, monkeypatch, isolated_home, symlink_or_skip
):
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/gh")
monkeypatch.delenv("GH_TOKEN", raising=False)
Expand All @@ -1426,7 +1427,8 @@ def test_hosts_metadata_refuses_ancestor_symlink(
"github.com:\n oauth_token: do-not-read\n",
encoding="utf-8",
)
(isolated_home / ".config").symlink_to(
symlink_or_skip(
isolated_home / ".config",
real_config,
target_is_directory=True,
)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_p0_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,6 @@ def test_system_install_uses_ytdlp_first_user_config(

import agent_reach.utils.paths as paths

monkeypatch.setattr(paths.sys, "platform", "darwin")
monkeypatch.setattr(paths.Path, "home", classmethod(lambda cls: tmp_path))
monkeypatch.delenv("XDG_CONFIG_HOME")
monkeypatch.setattr(
Expand Down Expand Up @@ -852,8 +851,9 @@ def test_uninstall_warns_about_opt_in_legacy_credential_copies(
cli._cmd_uninstall(Namespace(dry_run=True, keep_config=False))

output = capsys.readouterr().out
assert str(xfetch) in output
assert str(bird) in output
normalized_output = output.replace("\\", "/")
assert xfetch.as_posix() in normalized_output
assert bird.as_posix() in normalized_output
assert "不会自动删除" in output
assert xfetch.exists()
assert bird.exists()
Expand Down
10 changes: 8 additions & 2 deletions tests/test_paths.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
"""Behavior tests for cross-platform path and remediation helpers."""

import shutil
import subprocess
from pathlib import Path

import pytest

from agent_reach.utils import paths


Expand All @@ -17,8 +20,11 @@ def test_posix_ytdlp_fix_is_single_line_executable_and_idempotent(
command = paths.render_ytdlp_fix_command()

assert "\n" not in command
subprocess.run(["/bin/sh", "-c", command], check=True)
subprocess.run(["/bin/sh", "-c", command], check=True)
shell = shutil.which("sh")
if shell is None:
pytest.skip("requires a POSIX-compatible sh executable")
subprocess.run([shell, "-c", command], check=True)
subprocess.run([shell, "-c", command], check=True)

config = tmp_path / ".config" / "yt-dlp" / "config"
assert config.read_text(encoding="utf-8") == "--js-runtimes node\n"
Expand Down
2 changes: 2 additions & 0 deletions tests/test_private_file_writes.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def test_legacy_xfetch_sync_refuses_oversized_existing_session(
tmp_path, monkeypatch
):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
session_path = tmp_path / ".config" / "xfetch" / "session.json"
session_path.parent.mkdir(parents=True)
previous = json.dumps({"untrusted": "x" * (1024 * 1024)})
Expand Down Expand Up @@ -125,6 +126,7 @@ def test_xhs_cookie_editor_json_ignores_non_xhs_domains(
tmp_path, monkeypatch, capsys
):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.setattr("shutil.which", lambda name: None)
exported = [
{
Expand Down
9 changes: 5 additions & 4 deletions tests/test_reddit_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,28 +98,29 @@ def test_check_rdt_unparseable_credential_is_warn(isolated_home):
assert "无法安全解析" in message


def test_check_rdt_refuses_symlink_credential(isolated_home):
def test_check_rdt_refuses_symlink_credential(isolated_home, symlink_or_skip):
victim = isolated_home / "victim.json"
victim.write_text('{"secret": "do-not-read"}', encoding="utf-8")
path = isolated_home / ".config" / "rdt-cli" / "credential.json"
path.parent.mkdir(parents=True)
path.symlink_to(victim)
symlink_or_skip(path, victim)
with patch("shutil.which", return_value="/usr/local/bin/rdt"):
status, message = RedditChannel()._check_rdt()

assert status == "warn"
assert "符号链接" in message


def test_check_rdt_refuses_ancestor_symlink(isolated_home):
def test_check_rdt_refuses_ancestor_symlink(isolated_home, symlink_or_skip):
victim_dir = isolated_home / "victim-config"
credential_path = victim_dir / "rdt-cli" / "credential.json"
credential_path.parent.mkdir(parents=True)
credential_path.write_text(
'{"cookies": {"reddit_session": "do-not-read"}}',
encoding="utf-8",
)
(isolated_home / ".config").symlink_to(
symlink_or_skip(
isolated_home / ".config",
victim_dir,
target_is_directory=True,
)
Expand Down