mirror of
https://github.com/Ar9av/obsidian-wiki.git
synced 2026-09-14 20:36:34 +08:00
7620af79ff
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>
105 lines
3.6 KiB
Python
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")
|