mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
test: make the suite run under Windows
Windows spells a path with backslashes, and three fixtures took the POSIX shape: a config value written into a TOML basic string read `C:\users\…` as an escape, tool arguments built with an f-string broke the same way, and a root was compared as `str(path)` against `/srv/skills`. Paths are encoded with `json.dumps` (whose escaping is valid TOML and valid JSON) and compared as `Path` objects, so the same root matches in either spelling. `test_interrupt.py` delivers SIGINT with `os.kill(pid, SIGINT)`, which is `TerminateProcess` on Windows, and the router itself needs `add_signal_handler`: the module is skipped there. `python scripts/wine_pdm.py ci` is green: ruff, basedpyright 0 errors, 1156 passed / 30 skipped.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -147,7 +148,10 @@ async def test_config_allow_paths_installed(
|
|||||||
) -> None:
|
) -> None:
|
||||||
outside = (tmp_path.parent / "outside-config").resolve()
|
outside = (tmp_path.parent / "outside-config").resolve()
|
||||||
outside.mkdir(exist_ok=True)
|
outside.mkdir(exist_ok=True)
|
||||||
document = tomlkit.parse(f'[agent]\nallow_paths = {{ "{outside}" = "read", "/does/not/exist" = "read" }}\n')
|
# JSON string escaping is TOML escaping too, so a Windows path's backslashes
|
||||||
|
# survive instead of reading as an escape (``\u``).
|
||||||
|
key = json.dumps(str(outside))
|
||||||
|
document = tomlkit.parse(f'[agent]\nallow_paths = {{ {key} = "read", "/does/not/exist" = "read" }}\n')
|
||||||
state, memory = await _new_state(tmp_path, document=document)
|
state, memory = await _new_state(tmp_path, document=document)
|
||||||
try:
|
try:
|
||||||
assert state.instance_state.workspace.config_allow == {outside: AccessMode.READ}
|
assert state.instance_state.workspace.config_allow == {outside: AccessMode.READ}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -35,6 +36,14 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
import pytest as pytest_module
|
import pytest as pytest_module
|
||||||
|
|
||||||
|
# The router is asyncio's SIGINT handler, which only exists where signals do; on
|
||||||
|
# Windows a test could not deliver one either — ``os.kill(pid, SIGINT)`` is
|
||||||
|
# ``TerminateProcess`` there, so it kills the runner instead of interrupting it.
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
sys.platform == "win32",
|
||||||
|
reason="SIGINT routing needs POSIX signal handling",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeTarget:
|
class FakeTarget:
|
||||||
"""Minimal ``InterruptTarget`` recording its cancels."""
|
"""Minimal ``InterruptTarget`` recording its cancels."""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -44,7 +45,9 @@ def _skill_notice_rows(state: ReplState) -> list[str]:
|
|||||||
def _config_doc(tmp_path: Path, *, paths: list[Path] | None = None, extra: str = "") -> tomlkit.TOMLDocument:
|
def _config_doc(tmp_path: Path, *, paths: list[Path] | None = None, extra: str = "") -> tomlkit.TOMLDocument:
|
||||||
lines = ["[skills]", "discover = []"]
|
lines = ["[skills]", "discover = []"]
|
||||||
if paths:
|
if paths:
|
||||||
rendered = ", ".join(f'"{path}"' for path in paths)
|
# JSON string escaping is TOML escaping too, so a Windows path's
|
||||||
|
# backslashes survive instead of reading as an escape (``\u``).
|
||||||
|
rendered = ", ".join(json.dumps(str(path)) for path in paths)
|
||||||
lines.append(f"paths = [{rendered}]")
|
lines.append(f"paths = [{rendered}]")
|
||||||
if extra:
|
if extra:
|
||||||
lines.append(extra)
|
lines.append(extra)
|
||||||
@@ -215,12 +218,12 @@ async def test_skill_tools_are_offered_and_can_read_skill_files(
|
|||||||
|
|
||||||
skill_file = skills_state.skills.skills[0].skill_file if skills_state.skills else None
|
skill_file = skills_state.skills.skills[0].skill_file if skills_state.skills else None
|
||||||
assert skill_file is not None
|
assert skill_file is not None
|
||||||
read = await skills_state.agent.tools.execute("read_file", f'{{"path": "{skill_file}"}}')
|
read = await skills_state.agent.tools.execute("read_file", json.dumps({"path": str(skill_file)}))
|
||||||
assert "Deploy the acme stack" in read
|
assert "Deploy the acme stack" in read
|
||||||
|
|
||||||
denied = await skills_state.agent.tools.execute(
|
denied = await skills_state.agent.tools.execute(
|
||||||
"write_file",
|
"write_file",
|
||||||
f'{{"path": "{skill_file}", "content": "x"}}',
|
json.dumps({"path": str(skill_file), "content": "x"}),
|
||||||
)
|
)
|
||||||
assert denied.startswith("error:")
|
assert denied.startswith("error:")
|
||||||
assert "escapes workspace root" in denied
|
assert "escapes workspace root" in denied
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from pathlib import Path
|
||||||
|
|
||||||
from plyngent.config.models import SkillsConfig
|
from plyngent.config.models import SkillsConfig
|
||||||
from plyngent.skills import SKILL_FILE, Skill, SkillStore, build_roots, validate_skill_name
|
from plyngent.skills import SKILL_FILE, Skill, SkillStore, build_roots, validate_skill_name
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _write_skill(root: Path, name: str, *, description: str | None = "A test skill", body: str = "Body.") -> Path:
|
def _write_skill(root: Path, name: str, *, description: str | None = "A test skill", body: str = "Body.") -> Path:
|
||||||
directory = root / name
|
directory = root / name
|
||||||
@@ -28,12 +25,13 @@ def test_build_roots_orders_explicit_paths_first(tmp_path: Path) -> None:
|
|||||||
user_config_dir=tmp_path / "config",
|
user_config_dir=tmp_path / "config",
|
||||||
home=tmp_path / "home",
|
home=tmp_path / "home",
|
||||||
)
|
)
|
||||||
assert [(r.origin, r.scope, str(r.path)) for r in roots] == [
|
# Paths, not strings: a Windows run spells the same root differently.
|
||||||
("config", "user", str(tmp_path / "home" / "team-skills")),
|
assert [(r.origin, r.scope, r.path) for r in roots] == [
|
||||||
("config", "user", "/srv/skills"),
|
("config", "user", tmp_path / "home" / "team-skills"),
|
||||||
("plyngent", "user", str(tmp_path / "config" / "skills")),
|
("config", "user", Path("/srv/skills")),
|
||||||
("claude", "user", str(tmp_path / "home" / ".claude" / "skills")),
|
("plyngent", "user", tmp_path / "config" / "skills"),
|
||||||
("claude", "project", str(tmp_path / "proj" / ".claude" / "skills")),
|
("claude", "user", tmp_path / "home" / ".claude" / "skills"),
|
||||||
|
("claude", "project", tmp_path / "proj" / ".claude" / "skills"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user