Files
ar9av__obsidian-wiki/tests/test_cli_install.py
georgelichen 7620af79ff Keep Windows skill setup working without link privileges (#189)
Windows agent installs can contain directory junctions, and standard accounts
often cannot create new symbolic links. Detect link-like entries safely, remove
junctions without traversing their targets, fall back to copied skills only for
WinError 1314, and make status output tolerant of GBK consoles.

Constraint: Windows users may lack Developer Mode or Create symbolic links privilege
Rejected: Require elevation or Developer Mode | blocks standard Windows accounts
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep --copy as the explicit deterministic mode; only WinError 1314 may trigger automatic fallback
Tested: pytest -q tests/test_cli_install.py tests/test_ast_extractor.py tests/test_cache.py tests/test_graph_analysis.py tests/test_graphrag.py (196 passed); py_compile; actual obsidian-wiki setup with the target vault (exit 0)
Not-tested: Full suite still has unrelated Windows doctor/environment failures
Related: #186 #187 #188

Co-authored-by: Chen Li <george_lichne@163.com>
2026-08-23 06:49:57 -07:00

105 lines
3.6 KiB
Python

from __future__ import annotations
import io
import os
import subprocess
from pathlib import Path
import pytest
import obsidian_wiki.cli as cli
def test_configure_console_output_replaces_unencodable_status_text(
monkeypatch: pytest.MonkeyPatch,
) -> None:
raw = io.BytesIO()
stream = io.TextIOWrapper(raw, encoding="gbk", errors="strict")
monkeypatch.setattr(cli.sys, "stdout", stream)
cli._configure_console_output()
print("✅", file=cli.sys.stdout)
cli.sys.stdout.flush()
assert raw.getvalue()
stream.detach()
@pytest.mark.skipif(os.name != "nt", reason="directory junctions are Windows-only")
def test_install_skills_replaces_windows_junction(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
source_root = tmp_path / "bundled-skills"
skill = source_root / "junction-skill"
skill.mkdir(parents=True)
(skill / "SKILL.md").write_text("---\nname: junction-skill\n---\n", encoding="utf-8")
target_root = tmp_path / "target-skills"
target_root.mkdir()
old_root = tmp_path / "old-skills"
old_skill = old_root / "junction-skill"
old_skill.mkdir(parents=True)
(old_skill / "SKILL.md").write_text("old\n", encoding="utf-8")
junction = target_root / "junction-skill"
result = subprocess.run(
["cmd", "/c", "mklink", "/J", str(junction), str(old_skill)],
capture_output=True,
text=True,
)
if result.returncode != 0:
pytest.skip(f"directory junctions unavailable: {result.stderr or result.stdout}")
monkeypatch.setattr(cli, "skills_dir", lambda: source_root)
cli.install_skills(target_root, "test", mode="copy")
assert junction.is_dir()
assert (junction / "SKILL.md").read_text(encoding="utf-8") != "old\n"
assert (old_skill / "SKILL.md").read_text(encoding="utf-8") == "old\n"
@pytest.mark.skipif(os.name != "nt", reason="Windows symlink privileges are Windows-only")
def test_install_skills_falls_back_to_copy_without_symlink_privilege(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
source_root = tmp_path / "bundled-skills"
skill = source_root / "privilege-skill"
skill.mkdir(parents=True)
(skill / "SKILL.md").write_text("copied\n", encoding="utf-8")
target_root = tmp_path / "target-skills"
def deny_symlink(self: Path, target: Path, target_is_directory: bool = False) -> None:
error = OSError("symbolic links are not permitted")
error.winerror = 1314
raise error
monkeypatch.setattr(cli, "skills_dir", lambda: source_root)
monkeypatch.setattr(Path, "symlink_to", deny_symlink)
cli.install_skills(target_root, "test")
installed = target_root / "privilege-skill"
assert installed.is_dir()
assert not installed.is_symlink()
assert (installed / "SKILL.md").read_text(encoding="utf-8") == "copied\n"
@pytest.mark.skipif(os.name != "nt", reason="Windows symlink errors are Windows-only")
def test_install_skills_keeps_unrelated_symlink_errors(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
source_root = tmp_path / "bundled-skills"
skill = source_root / "broken-skill"
skill.mkdir(parents=True)
(skill / "SKILL.md").write_text("broken\n", encoding="utf-8")
def fail_symlink(self: Path, target: Path, target_is_directory: bool = False) -> None:
error = OSError("access denied")
error.winerror = 5
raise error
monkeypatch.setattr(cli, "skills_dir", lambda: source_root)
monkeypatch.setattr(Path, "symlink_to", fail_symlink)
with pytest.raises(OSError, match="access denied"):
cli.install_skills(tmp_path / "target-skills", "test")