gpt-5.2 ChatGPT: refactor tools code into cli.tools.vscode and add unit tests
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
* Move VS Code workspace logic (incl. guards) from cli.commands.tools into cli.tools.vscode * Extract shared repo path resolution into cli.tools.paths and reuse for explore/terminal * Simplify cli.commands.tools to pure orchestration via open_vscode_workspace * Update existing tools command unit test to assert delegation instead of patching removed internals * Add new unit tests for cli.tools.paths and cli.tools.vscode (workspace creation, reuse, guard errors) https://chatgpt.com/share/69419a6a-c9e4-800f-9538-b6652b2da6b3
This commit is contained in:
@@ -1,64 +1,27 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from pkgmgr.cli.context import CLIContext
|
from pkgmgr.cli.context import CLIContext
|
||||||
|
from pkgmgr.cli.tools import open_vscode_workspace
|
||||||
|
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||||
from pkgmgr.core.command.run import run_command
|
from pkgmgr.core.command.run import run_command
|
||||||
from pkgmgr .core .repository .identifier import get_repo_identifier
|
|
||||||
from pkgmgr .core .repository .dir import get_repo_dir
|
|
||||||
|
|
||||||
|
|
||||||
Repository = Dict[str, Any]
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
def _resolve_repository_path(repository: Repository, ctx: CLIContext) -> str:
|
|
||||||
"""
|
|
||||||
Resolve the filesystem path for a repository.
|
|
||||||
|
|
||||||
Priority:
|
|
||||||
1. Use explicit keys if present (directory / path / workspace / workspace_dir).
|
|
||||||
2. Fallback to get_repo_dir(...) using the repositories base directory
|
|
||||||
from the CLI context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 1) Explicit path-like keys on the repository object
|
|
||||||
for key in ("directory", "path", "workspace", "workspace_dir"):
|
|
||||||
value = repository.get(key)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
|
|
||||||
# 2) Fallback: compute from base dir + repository metadata
|
|
||||||
base_dir = (
|
|
||||||
getattr(ctx, "repositories_base_dir", None)
|
|
||||||
or getattr(ctx, "repositories_dir", None)
|
|
||||||
)
|
|
||||||
if not base_dir:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Cannot resolve repositories base directory from context; "
|
|
||||||
"expected ctx.repositories_base_dir or ctx.repositories_dir."
|
|
||||||
)
|
|
||||||
|
|
||||||
return get_repo_dir(base_dir, repository)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_tools_command(
|
def handle_tools_command(
|
||||||
args,
|
args,
|
||||||
ctx: CLIContext,
|
ctx: CLIContext,
|
||||||
selected: List[Repository],
|
selected: List[Repository],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# nautilus "explore" command
|
# nautilus "explore" command
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
if args.command == "explore":
|
if args.command == "explore":
|
||||||
for repository in selected:
|
for repository in selected:
|
||||||
repo_path = _resolve_repository_path(repository, ctx)
|
repo_path = resolve_repository_path(repository, ctx)
|
||||||
run_command(
|
run_command(f'nautilus "{repo_path}" & disown')
|
||||||
f'nautilus "{repo_path}" & disown'
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -66,50 +29,13 @@ def handle_tools_command(
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
if args.command == "terminal":
|
if args.command == "terminal":
|
||||||
for repository in selected:
|
for repository in selected:
|
||||||
repo_path = _resolve_repository_path(repository, ctx)
|
repo_path = resolve_repository_path(repository, ctx)
|
||||||
run_command(
|
run_command(f'gnome-terminal --tab --working-directory="{repo_path}"')
|
||||||
f'gnome-terminal --tab --working-directory="{repo_path}"'
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# VS Code workspace command
|
# VS Code workspace command
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
if args.command == "code":
|
if args.command == "code":
|
||||||
if not selected:
|
open_vscode_workspace(ctx, selected)
|
||||||
print("No repositories selected.")
|
|
||||||
return
|
|
||||||
|
|
||||||
identifiers = [
|
|
||||||
get_repo_identifier(repo, ctx.all_repositories)
|
|
||||||
for repo in selected
|
|
||||||
]
|
|
||||||
sorted_identifiers = sorted(identifiers)
|
|
||||||
workspace_name = "_".join(sorted_identifiers) + ".code-workspace"
|
|
||||||
|
|
||||||
directories_cfg = ctx.config_merged.get("directories") or {}
|
|
||||||
workspaces_dir = os.path.expanduser(
|
|
||||||
directories_cfg.get("workspaces", "~/Workspaces")
|
|
||||||
)
|
|
||||||
os.makedirs(workspaces_dir, exist_ok=True)
|
|
||||||
workspace_file = os.path.join(workspaces_dir, workspace_name)
|
|
||||||
|
|
||||||
folders = [
|
|
||||||
{"path": _resolve_repository_path(repository, ctx)}
|
|
||||||
for repository in selected
|
|
||||||
]
|
|
||||||
|
|
||||||
workspace_data = {
|
|
||||||
"folders": folders,
|
|
||||||
"settings": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if not os.path.exists(workspace_file):
|
|
||||||
with open(workspace_file, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(workspace_data, f, indent=4)
|
|
||||||
print(f"Created workspace file: {workspace_file}")
|
|
||||||
else:
|
|
||||||
print(f"Using existing workspace file: {workspace_file}")
|
|
||||||
|
|
||||||
run_command(f'code "{workspace_file}"')
|
|
||||||
return
|
return
|
||||||
|
|||||||
5
src/pkgmgr/cli/tools/__init__.py
Normal file
5
src/pkgmgr/cli/tools/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .vscode import open_vscode_workspace
|
||||||
|
|
||||||
|
__all__ = ["open_vscode_workspace"]
|
||||||
35
src/pkgmgr/cli/tools/paths.py
Normal file
35
src/pkgmgr/cli/tools/paths.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from pkgmgr.cli.context import CLIContext
|
||||||
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
|
|
||||||
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_repository_path(repository: Repository, ctx: CLIContext) -> str:
|
||||||
|
"""
|
||||||
|
Resolve the filesystem path for a repository.
|
||||||
|
|
||||||
|
Priority:
|
||||||
|
1. Use explicit keys if present (directory / path / workspace / workspace_dir).
|
||||||
|
2. Fallback to get_repo_dir(...) using the repositories base directory
|
||||||
|
from the CLI context.
|
||||||
|
"""
|
||||||
|
for key in ("directory", "path", "workspace", "workspace_dir"):
|
||||||
|
value = repository.get(key)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
|
||||||
|
base_dir = (
|
||||||
|
getattr(ctx, "repositories_base_dir", None)
|
||||||
|
or getattr(ctx, "repositories_dir", None)
|
||||||
|
)
|
||||||
|
if not base_dir:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot resolve repositories base directory from context; "
|
||||||
|
"expected ctx.repositories_base_dir or ctx.repositories_dir."
|
||||||
|
)
|
||||||
|
|
||||||
|
return get_repo_dir(base_dir, repository)
|
||||||
102
src/pkgmgr/cli/tools/vscode.py
Normal file
102
src/pkgmgr/cli/tools/vscode.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from pkgmgr.cli.context import CLIContext
|
||||||
|
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||||
|
from pkgmgr.core.command.run import run_command
|
||||||
|
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||||
|
|
||||||
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_vscode_cli_available() -> None:
|
||||||
|
"""
|
||||||
|
Ensure that the VS Code CLI ('code') is available in PATH.
|
||||||
|
"""
|
||||||
|
if shutil.which("code") is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"VS Code CLI ('code') not found in PATH.\n\n"
|
||||||
|
"Hint:\n"
|
||||||
|
" Install Visual Studio Code and ensure the 'code' command is available.\n"
|
||||||
|
" VS Code → Command Palette → 'Shell Command: Install code command in PATH'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_identifiers_are_filename_safe(identifiers: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Ensure identifiers can be used in a filename.
|
||||||
|
|
||||||
|
If an identifier contains '/', it likely means the repository has not yet
|
||||||
|
been explicitly identified (no short identifier configured).
|
||||||
|
"""
|
||||||
|
invalid = [i for i in identifiers if "/" in i or os.sep in i]
|
||||||
|
if invalid:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot create VS Code workspace.\n\n"
|
||||||
|
"The following repositories are not yet identified "
|
||||||
|
"(identifier contains '/'): \n"
|
||||||
|
+ "\n".join(f" - {i}" for i in invalid)
|
||||||
|
+ "\n\n"
|
||||||
|
"Hint:\n"
|
||||||
|
" The repository has no short identifier yet.\n"
|
||||||
|
" Add an explicit identifier in your configuration before using `pkgmgr tools code`.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_workspaces_dir(ctx: CLIContext) -> str:
|
||||||
|
directories_cfg = ctx.config_merged.get("directories") or {}
|
||||||
|
return os.path.expanduser(directories_cfg.get("workspaces", "~/Workspaces"))
|
||||||
|
|
||||||
|
|
||||||
|
def _build_workspace_filename(identifiers: List[str]) -> str:
|
||||||
|
sorted_identifiers = sorted(identifiers)
|
||||||
|
return "_".join(sorted_identifiers) + ".code-workspace"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_workspace_data(selected: List[Repository], ctx: CLIContext) -> Dict[str, Any]:
|
||||||
|
folders = [{"path": resolve_repository_path(repo, ctx)} for repo in selected]
|
||||||
|
return {
|
||||||
|
"folders": folders,
|
||||||
|
"settings": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def open_vscode_workspace(ctx: CLIContext, selected: List[Repository]) -> None:
|
||||||
|
"""
|
||||||
|
Create (if missing) and open a VS Code workspace for the selected repositories.
|
||||||
|
|
||||||
|
Policy:
|
||||||
|
- Fail with a clear error if VS Code CLI is missing.
|
||||||
|
- Fail with a clear error if any repository identifier contains '/', because that
|
||||||
|
indicates the repo has not been explicitly identified (no short identifier).
|
||||||
|
- Do NOT auto-sanitize identifiers and do NOT create subfolders under workspaces.
|
||||||
|
"""
|
||||||
|
if not selected:
|
||||||
|
print("No repositories selected.")
|
||||||
|
return
|
||||||
|
|
||||||
|
_ensure_vscode_cli_available()
|
||||||
|
|
||||||
|
identifiers = [get_repo_identifier(repo, ctx.all_repositories) for repo in selected]
|
||||||
|
_ensure_identifiers_are_filename_safe(identifiers)
|
||||||
|
|
||||||
|
workspaces_dir = _resolve_workspaces_dir(ctx)
|
||||||
|
os.makedirs(workspaces_dir, exist_ok=True)
|
||||||
|
|
||||||
|
workspace_name = _build_workspace_filename(identifiers)
|
||||||
|
workspace_file = os.path.join(workspaces_dir, workspace_name)
|
||||||
|
|
||||||
|
workspace_data = _build_workspace_data(selected, ctx)
|
||||||
|
|
||||||
|
if not os.path.exists(workspace_file):
|
||||||
|
with open(workspace_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(workspace_data, f, indent=4)
|
||||||
|
print(f"Created workspace file: {workspace_file}")
|
||||||
|
else:
|
||||||
|
print(f"Using existing workspace file: {workspace_file}")
|
||||||
|
|
||||||
|
run_command(f'code "{workspace_file}"')
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
import unittest
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
from pkgmgr.cli.commands.tools import handle_tools_command
|
from pkgmgr.cli.commands.tools import handle_tools_command
|
||||||
|
|
||||||
@@ -26,36 +24,23 @@ class TestHandleToolsCommand(unittest.TestCase):
|
|||||||
Unit tests for pkgmgr.cli.commands.tools.handle_tools_command.
|
Unit tests for pkgmgr.cli.commands.tools.handle_tools_command.
|
||||||
|
|
||||||
We focus on:
|
We focus on:
|
||||||
- Correct path resolution for repositories that have a 'directory' key.
|
- Correct path resolution and shell commands for 'explore' and 'terminal'.
|
||||||
- Correct shell commands for 'explore' and 'terminal'.
|
- For 'code': delegation to pkgmgr.cli.tools.open_vscode_workspace.
|
||||||
- Proper workspace creation and invocation of 'code' for the 'code' command.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
# Two fake repositories with explicit 'directory' entries so that
|
|
||||||
# _resolve_repository_path() does not need to call get_repo_dir().
|
|
||||||
self.repos: List[Repository] = [
|
self.repos: List[Repository] = [
|
||||||
{"alias": "repo1", "directory": "/tmp/repo1"},
|
{"alias": "repo1", "directory": "/tmp/repo1"},
|
||||||
{"alias": "repo2", "directory": "/tmp/repo2"},
|
{"alias": "repo2", "directory": "/tmp/repo2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Minimal CLI context; only attributes used in tools.py are provided.
|
|
||||||
self.ctx = SimpleNamespace(
|
self.ctx = SimpleNamespace(
|
||||||
config_merged={"directories": {"workspaces": "~/Workspaces"}},
|
config_merged={"directories": {"workspaces": "~/Workspaces"}},
|
||||||
all_repositories=self.repos,
|
all_repositories=self.repos,
|
||||||
repositories_base_dir="/base/dir",
|
repositories_base_dir="/base/dir",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Helper
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
def _patch_run_command(self):
|
def _patch_run_command(self):
|
||||||
"""
|
|
||||||
Convenience context manager for patching run_command in tools module.
|
|
||||||
"""
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
return patch("pkgmgr.cli.commands.tools.run_command")
|
return patch("pkgmgr.cli.commands.tools.run_command")
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -63,12 +48,6 @@ class TestHandleToolsCommand(unittest.TestCase):
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_explore_uses_directory_paths(self) -> None:
|
def test_explore_uses_directory_paths(self) -> None:
|
||||||
"""
|
|
||||||
The 'explore' command should call Nautilus with the resolved
|
|
||||||
repository paths and use '& disown' as in the implementation.
|
|
||||||
"""
|
|
||||||
from unittest.mock import call
|
|
||||||
|
|
||||||
args = _Args(command="explore")
|
args = _Args(command="explore")
|
||||||
|
|
||||||
with self._patch_run_command() as mock_run_command:
|
with self._patch_run_command() as mock_run_command:
|
||||||
@@ -85,12 +64,6 @@ class TestHandleToolsCommand(unittest.TestCase):
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_terminal_uses_directory_paths(self) -> None:
|
def test_terminal_uses_directory_paths(self) -> None:
|
||||||
"""
|
|
||||||
The 'terminal' command should open a GNOME Terminal tab with the
|
|
||||||
repository as its working directory.
|
|
||||||
"""
|
|
||||||
from unittest.mock import call
|
|
||||||
|
|
||||||
args = _Args(command="terminal")
|
args = _Args(command="terminal")
|
||||||
|
|
||||||
with self._patch_run_command() as mock_run_command:
|
with self._patch_run_command() as mock_run_command:
|
||||||
@@ -106,63 +79,10 @@ class TestHandleToolsCommand(unittest.TestCase):
|
|||||||
# Tests for 'code'
|
# Tests for 'code'
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_code_creates_workspace_and_calls_code(self) -> None:
|
def test_code_delegates_to_open_vscode_workspace(self) -> None:
|
||||||
"""
|
|
||||||
The 'code' command should:
|
|
||||||
|
|
||||||
- Build a workspace file name from sorted repository identifiers.
|
|
||||||
- Resolve the repository paths into VS Code 'folders'.
|
|
||||||
- Create the workspace file if it does not exist.
|
|
||||||
- Call 'code "<workspace_file>"' via run_command.
|
|
||||||
"""
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
args = _Args(command="code")
|
args = _Args(command="code")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with patch("pkgmgr.cli.commands.tools.open_vscode_workspace") as m:
|
||||||
# Patch expanduser so that the configured '~/Workspaces'
|
|
||||||
# resolves into our temporary directory.
|
|
||||||
with patch(
|
|
||||||
"pkgmgr.cli.commands.tools.os.path.expanduser"
|
|
||||||
) as mock_expanduser:
|
|
||||||
mock_expanduser.return_value = tmpdir
|
|
||||||
|
|
||||||
# Patch get_repo_identifier so the resulting workspace file
|
|
||||||
# name is deterministic and easy to assert.
|
|
||||||
with patch(
|
|
||||||
"pkgmgr.cli.commands.tools.get_repo_identifier"
|
|
||||||
) as mock_get_identifier:
|
|
||||||
mock_get_identifier.side_effect = ["repo-b", "repo-a"]
|
|
||||||
|
|
||||||
with self._patch_run_command() as mock_run_command:
|
|
||||||
handle_tools_command(args, self.ctx, self.repos)
|
handle_tools_command(args, self.ctx, self.repos)
|
||||||
|
|
||||||
# The identifiers are ['repo-b', 'repo-a'], which are
|
m.assert_called_once_with(self.ctx, self.repos)
|
||||||
# sorted to ['repo-a', 'repo-b'] and joined with '_'.
|
|
||||||
expected_workspace_name = "repo-a_repo-b.code-workspace"
|
|
||||||
expected_workspace_file = os.path.join(
|
|
||||||
tmpdir, expected_workspace_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# Workspace file should have been created.
|
|
||||||
self.assertTrue(
|
|
||||||
os.path.exists(expected_workspace_file),
|
|
||||||
"Workspace file was not created.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# The content of the workspace must be valid JSON with
|
|
||||||
# the expected folder paths.
|
|
||||||
with open(expected_workspace_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
self.assertIn("folders", data)
|
|
||||||
folder_paths = {f["path"] for f in data["folders"]}
|
|
||||||
self.assertEqual(
|
|
||||||
folder_paths,
|
|
||||||
{"/tmp/repo1", "/tmp/repo2"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# And VS Code must have been invoked with that workspace.
|
|
||||||
mock_run_command.assert_called_once_with(
|
|
||||||
f'code "{expected_workspace_file}"'
|
|
||||||
)
|
|
||||||
|
|||||||
38
tests/unit/pkgmgr/cli/tools/test_paths.py
Normal file
38
tests/unit/pkgmgr/cli/tools/test_paths.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveRepositoryPath(unittest.TestCase):
|
||||||
|
def test_explicit_directory_key_wins(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(repositories_base_dir="/base", repositories_dir="/base2")
|
||||||
|
repo = {"directory": "/explicit/repo"}
|
||||||
|
|
||||||
|
self.assertEqual(resolve_repository_path(repo, ctx), "/explicit/repo")
|
||||||
|
|
||||||
|
def test_fallback_uses_get_repo_dir_with_repositories_base_dir(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(repositories_base_dir="/base", repositories_dir="/base2")
|
||||||
|
repo = {"provider": "github.com", "account": "acme", "repository": "demo"}
|
||||||
|
|
||||||
|
with patch("pkgmgr.cli.tools.paths.get_repo_dir", return_value="/computed/repo") as m:
|
||||||
|
out = resolve_repository_path(repo, ctx)
|
||||||
|
|
||||||
|
self.assertEqual(out, "/computed/repo")
|
||||||
|
m.assert_called_once_with("/base", repo)
|
||||||
|
|
||||||
|
def test_raises_if_no_base_dir_in_context(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.paths import resolve_repository_path
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(repositories_base_dir=None, repositories_dir=None)
|
||||||
|
repo = {"provider": "github.com", "account": "acme", "repository": "demo"}
|
||||||
|
|
||||||
|
with self.assertRaises(RuntimeError) as cm:
|
||||||
|
resolve_repository_path(repo, ctx)
|
||||||
|
|
||||||
|
self.assertIn("Cannot resolve repositories base directory", str(cm.exception))
|
||||||
131
tests/unit/pkgmgr/cli/tools/test_vscode.py
Normal file
131
tests/unit/pkgmgr/cli/tools/test_vscode.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenVSCodeWorkspace(unittest.TestCase):
|
||||||
|
def test_no_selected_repos_prints_message_and_returns(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(config_merged={}, all_repositories=[])
|
||||||
|
|
||||||
|
with patch("builtins.print") as p:
|
||||||
|
open_vscode_workspace(ctx, [])
|
||||||
|
|
||||||
|
p.assert_called_once()
|
||||||
|
self.assertIn("No repositories selected.", str(p.call_args[0][0]))
|
||||||
|
|
||||||
|
def test_raises_if_code_cli_missing(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(config_merged={}, all_repositories=[])
|
||||||
|
selected: List[Repository] = [{"provider": "github.com", "account": "x", "repository": "y"}]
|
||||||
|
|
||||||
|
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value=None):
|
||||||
|
with self.assertRaises(RuntimeError) as cm:
|
||||||
|
open_vscode_workspace(ctx, selected)
|
||||||
|
|
||||||
|
self.assertIn("VS Code CLI ('code') not found", str(cm.exception))
|
||||||
|
|
||||||
|
def test_raises_if_identifier_contains_slash(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(
|
||||||
|
config_merged={"directories": {"workspaces": "~/Workspaces"}},
|
||||||
|
all_repositories=[],
|
||||||
|
)
|
||||||
|
selected: List[Repository] = [{"provider": "github.com", "account": "x", "repository": "y"}]
|
||||||
|
|
||||||
|
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
|
||||||
|
"pkgmgr.cli.tools.vscode.get_repo_identifier",
|
||||||
|
return_value="github.com/x/y",
|
||||||
|
):
|
||||||
|
with self.assertRaises(RuntimeError) as cm:
|
||||||
|
open_vscode_workspace(ctx, selected)
|
||||||
|
|
||||||
|
msg = str(cm.exception)
|
||||||
|
self.assertIn("not yet identified", msg)
|
||||||
|
self.assertIn("identifier contains '/'", msg)
|
||||||
|
|
||||||
|
def test_creates_workspace_file_and_calls_code(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
workspaces_dir = os.path.join(tmp, "Workspaces")
|
||||||
|
repo_path = os.path.join(tmp, "Repos", "dotlinker")
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(
|
||||||
|
config_merged={"directories": {"workspaces": workspaces_dir}},
|
||||||
|
all_repositories=[],
|
||||||
|
repositories_base_dir=os.path.join(tmp, "Repos"),
|
||||||
|
)
|
||||||
|
selected: List[Repository] = [
|
||||||
|
{"provider": "github.com", "account": "kevin", "repository": "dotlinker"}
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
|
||||||
|
"pkgmgr.cli.tools.vscode.get_repo_identifier",
|
||||||
|
return_value="dotlinker",
|
||||||
|
), patch(
|
||||||
|
"pkgmgr.cli.tools.vscode.resolve_repository_path",
|
||||||
|
return_value=repo_path,
|
||||||
|
), patch(
|
||||||
|
"pkgmgr.cli.tools.vscode.run_command"
|
||||||
|
) as run_cmd:
|
||||||
|
open_vscode_workspace(ctx, selected)
|
||||||
|
|
||||||
|
workspace_file = os.path.join(workspaces_dir, "dotlinker.code-workspace")
|
||||||
|
self.assertTrue(os.path.exists(workspace_file))
|
||||||
|
|
||||||
|
with open(workspace_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
self.assertEqual(data["folders"], [{"path": repo_path}])
|
||||||
|
self.assertEqual(data["settings"], {})
|
||||||
|
|
||||||
|
run_cmd.assert_called_once_with(f'code "{workspace_file}"')
|
||||||
|
|
||||||
|
def test_uses_existing_workspace_file_without_overwriting(self) -> None:
|
||||||
|
from pkgmgr.cli.tools.vscode import open_vscode_workspace
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
workspaces_dir = os.path.join(tmp, "Workspaces")
|
||||||
|
os.makedirs(workspaces_dir, exist_ok=True)
|
||||||
|
|
||||||
|
workspace_file = os.path.join(workspaces_dir, "dotlinker.code-workspace")
|
||||||
|
original = {"folders": [{"path": "/original"}], "settings": {"x": 1}}
|
||||||
|
with open(workspace_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(original, f)
|
||||||
|
|
||||||
|
ctx = SimpleNamespace(
|
||||||
|
config_merged={"directories": {"workspaces": workspaces_dir}},
|
||||||
|
all_repositories=[],
|
||||||
|
)
|
||||||
|
selected: List[Repository] = [
|
||||||
|
{"provider": "github.com", "account": "kevin", "repository": "dotlinker"}
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch("pkgmgr.cli.tools.vscode.shutil.which", return_value="/usr/bin/code"), patch(
|
||||||
|
"pkgmgr.cli.tools.vscode.get_repo_identifier",
|
||||||
|
return_value="dotlinker",
|
||||||
|
), patch(
|
||||||
|
"pkgmgr.cli.tools.vscode.resolve_repository_path",
|
||||||
|
return_value="/new/path",
|
||||||
|
), patch(
|
||||||
|
"pkgmgr.cli.tools.vscode.run_command"
|
||||||
|
) as run_cmd:
|
||||||
|
open_vscode_workspace(ctx, selected)
|
||||||
|
|
||||||
|
with open(workspace_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
self.assertEqual(data, original)
|
||||||
|
run_cmd.assert_called_once_with(f'code "{workspace_file}"')
|
||||||
Reference in New Issue
Block a user