Add project-aware chat startup defaults
Make repo-root chat startup native by letting MCP servers carry a default project source for workspace creation. When a chat host starts from a Git checkout, workspace_create can now omit seed_path and inherit the server startup source; explicit --project-path and clean-clone --repo-url/--repo-ref paths are supported as fallbacks. Add project startup resolution and materialization, surface origin_kind/origin_ref in workspace_seed, update chat-host docs and the repro/fix smoke to use project-aware workspace creation, and switch dist-check to uv run pyro so verification stays stable after uv reinstalls. Validated with uv lock, focused startup/server/CLI pytest coverage, UV_CACHE_DIR=.uv-cache make check, UV_CACHE_DIR=.uv-cache make dist-check, and real guest-backed smokes for both explicit project_path and bare repo-root auto-detection.
This commit is contained in:
parent
9b9b83ebeb
commit
535efc6919
28 changed files with 968 additions and 67 deletions
115
tests/test_project_startup.py
Normal file
115
tests/test_project_startup.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pyro_mcp.project_startup import (
|
||||
ProjectStartupSource,
|
||||
describe_project_startup_source,
|
||||
materialize_project_startup_source,
|
||||
resolve_project_startup_source,
|
||||
)
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> str:
|
||||
result = subprocess.run( # noqa: S603
|
||||
["git", *args],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def _make_repo(root: Path, *, filename: str = "note.txt", content: str = "hello\n") -> Path:
|
||||
root.mkdir()
|
||||
_git(root, "init")
|
||||
_git(root, "config", "user.name", "Pyro Tests")
|
||||
_git(root, "config", "user.email", "pyro-tests@example.com")
|
||||
(root / filename).write_text(content, encoding="utf-8")
|
||||
_git(root, "add", filename)
|
||||
_git(root, "commit", "-m", "init")
|
||||
return root
|
||||
|
||||
|
||||
def test_resolve_project_startup_source_detects_nearest_git_root(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path / "repo")
|
||||
nested = repo / "src" / "pkg"
|
||||
nested.mkdir(parents=True)
|
||||
|
||||
resolved = resolve_project_startup_source(cwd=nested)
|
||||
|
||||
assert resolved == ProjectStartupSource(
|
||||
kind="project_path",
|
||||
origin_ref=str(repo.resolve()),
|
||||
resolved_path=repo.resolve(),
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_project_startup_source_project_path_prefers_git_root(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path / "repo")
|
||||
nested = repo / "nested"
|
||||
nested.mkdir()
|
||||
|
||||
resolved = resolve_project_startup_source(project_path=nested, cwd=tmp_path)
|
||||
|
||||
assert resolved == ProjectStartupSource(
|
||||
kind="project_path",
|
||||
origin_ref=str(repo.resolve()),
|
||||
resolved_path=repo.resolve(),
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_project_startup_source_validates_flag_combinations(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path / "repo")
|
||||
|
||||
with pytest.raises(ValueError, match="mutually exclusive"):
|
||||
resolve_project_startup_source(project_path=repo, repo_url="https://example.com/repo.git")
|
||||
|
||||
with pytest.raises(ValueError, match="requires --repo-url"):
|
||||
resolve_project_startup_source(repo_ref="main")
|
||||
|
||||
with pytest.raises(ValueError, match="cannot be combined"):
|
||||
resolve_project_startup_source(project_path=repo, no_project_source=True)
|
||||
|
||||
|
||||
def test_materialize_project_startup_source_clones_local_repo_url_at_ref(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path / "repo", content="one\n")
|
||||
first_commit = _git(repo, "rev-parse", "HEAD")
|
||||
(repo / "note.txt").write_text("two\n", encoding="utf-8")
|
||||
_git(repo, "add", "note.txt")
|
||||
_git(repo, "commit", "-m", "update")
|
||||
|
||||
source = ProjectStartupSource(
|
||||
kind="repo_url",
|
||||
origin_ref=str(repo.resolve()),
|
||||
repo_ref=first_commit,
|
||||
)
|
||||
|
||||
with materialize_project_startup_source(source) as clone_dir:
|
||||
assert (clone_dir / "note.txt").read_text(encoding="utf-8") == "one\n"
|
||||
|
||||
|
||||
def test_describe_project_startup_source_formats_project_and_repo_sources(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path / "repo")
|
||||
|
||||
project_description = describe_project_startup_source(
|
||||
ProjectStartupSource(
|
||||
kind="project_path",
|
||||
origin_ref=str(repo.resolve()),
|
||||
resolved_path=repo.resolve(),
|
||||
)
|
||||
)
|
||||
repo_description = describe_project_startup_source(
|
||||
ProjectStartupSource(
|
||||
kind="repo_url",
|
||||
origin_ref="https://example.com/repo.git",
|
||||
repo_ref="main",
|
||||
)
|
||||
)
|
||||
|
||||
assert project_description == f"the current project at {repo.resolve()}"
|
||||
assert repo_description == "the clean clone source https://example.com/repo.git at ref main"
|
||||
Loading…
Add table
Add a link
Reference in a new issue