diff --git a/loopx/workflow_skill_install.py b/loopx/workflow_skill_install.py index ead07e7672..c28940c889 100644 --- a/loopx/workflow_skill_install.py +++ b/loopx/workflow_skill_install.py @@ -6,13 +6,13 @@ import os from pathlib import Path, PurePosixPath import shutil -import shlex import sys import tempfile from typing import Any, Iterator, Mapping from . import __version__ from .file_lock import exclusive_file_lock +from .python_install_owner import interpreter_invocation from .skill_install_readback import ( ARK_MANAGED_AGENT_REQUIRED_SKILL_IDS, PACKAGED_HOST_SKILL_IDS, @@ -391,7 +391,7 @@ def workflow_skill_install( "would_create", "updated", "upgraded_legacy_managed", } ), - "install_command": shlex.join(install_command), + "install_command": interpreter_invocation(install_command[0], install_command[1:]), } if entry_preview["status"] not in MANAGED_ENTRY_PREVIEW_STATUSES: @@ -464,9 +464,9 @@ def workflow_skill_install( "after": after, "skill_reconciliation": reconciliation, "ego_browser": ego, - "rollback_command": shlex.join( + "rollback_command": interpreter_invocation( + "loopx", [ - "loopx", "workflow-skills", "--uninstall", "--skills-dir", diff --git a/tests/test_workflow_skill_install.py b/tests/test_workflow_skill_install.py index 5c811ee4f2..83f4838dff 100644 --- a/tests/test_workflow_skill_install.py +++ b/tests/test_workflow_skill_install.py @@ -4,7 +4,9 @@ import json import os import re +import shlex import shutil +import subprocess import sys from pathlib import Path from typing import Any, Iterator @@ -13,6 +15,7 @@ from loopx.capabilities.project_skill_delivery import canonical_project_skill_source from loopx import file_lock +from loopx import python_install_owner from loopx import skill_install_readback from loopx import workflow_skill_install as install_module from loopx.skill_install_readback import ( @@ -40,6 +43,49 @@ def test_source_checkout_contains_packaged_workflow_skills() -> None: assert (Path(source["skills_root"]) / skill_id / "SKILL.md").is_file() +@pytest.mark.parametrize("host_surface", [None, "deepseek-harness-native"]) +@pytest.mark.parametrize("windows", [False, True]) +def test_printed_workflow_commands_preserve_target_and_host_arguments( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + host_surface: str | None, windows: bool, +) -> None: + pwsh = shutil.which("pwsh") + if windows and pwsh is None: + pytest.skip("native PowerShell is unavailable") + monkeypatch.setattr(python_install_owner, "_is_windows_host", lambda: windows) + skills_dir = tmp_path / "O'Brien $profile 中文" / "skills" + preview = workflow_skill_install(skills_dir=skills_dir, host_surface=host_surface) + installed = workflow_skill_install( + skills_dir=skills_dir, host_surface=host_surface, execute=True, + ) + assert installed["ok"] and installed["after"]["ready"] + for command, operation in [ + (preview["install_command"], "--install"), + (installed["rollback_command"], "--uninstall"), + ]: + expected = ["workflow-skills", operation, "--skills-dir", str(skills_dir)] + if operation == "--install" and host_surface is not None: + expected.extend(["--host-surface", host_surface]) + if windows: + completed = subprocess.run( + [pwsh, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", + "function loopx { ConvertTo-Json -InputObject @($args) -Compress }; " + + command], + capture_output=True, encoding="utf-8", timeout=30, check=True, + ) + assert json.loads(completed.stdout) == expected + else: + assert shlex.split(command) == ["loopx", *expected] + + modified = skills_dir / "loopx-project" / "SKILL.md" + modified.write_text(modified.read_text(encoding="utf-8") + "\nUser notes\n", encoding="utf-8") + removed = workflow_skill_install(skills_dir=skills_dir, execute=True, uninstall=True) + assert removed["ok"] is False + assert removed["result"]["preserved_modified"] == ["loopx-project"] + assert modified.is_file() + assert not (skills_dir / "loopx-benchmark").exists() + + def test_entry_metadata_repair_is_previewed_recorded_and_uninstalled(tmp_path: Path) -> None: skills_dir = tmp_path / "skills" workflow_skill_install(skills_dir=skills_dir, execute=True)