diff --git a/AGENTS.md b/AGENTS.md index bb0e81b..31aab4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,4 +19,4 @@ - Concise, engineering notebook tone. ## How to verify (tests/commands) -- Unknown / needs verification (no test harness yet). +- `pytest` diff --git a/pyproject.toml b/pyproject.toml index d5f8f29..0f78b33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,14 @@ authors = [ {name = "scawful"} ] +[project.optional-dependencies] +test = [ + "pytest>=7.4" +] + +[tool.pytest.ini_options] +testpaths = ["tests"] + [build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fdcbc1f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..ef963ea --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from pathlib import Path + +from afs.config import load_config, load_config_model + + +def test_load_config_merges_workspace_registry(tmp_path, monkeypatch) -> None: + context_root = tmp_path / "context" + context_root.mkdir() + workspace_dir = tmp_path / "workspace" + workspace_dir.mkdir() + + registry_path = context_root / "workspaces.toml" + registry_path.write_text( + "[[workspaces]]\n" + f"path = \"{workspace_dir}\"\n" + "description = \"Example\"\n", + encoding="utf-8", + ) + + config_path = tmp_path / "afs.toml" + config_path.write_text( + f"[general]\ncontext_root = \"{context_root}\"\n", + encoding="utf-8", + ) + + monkeypatch.chdir(tmp_path) + data = load_config(merge_user=False) + workspaces = data["general"]["workspace_directories"] + assert workspaces + assert Path(workspaces[0]["path"]).resolve() == workspace_dir.resolve() + + +def test_load_config_model_uses_explicit_path(tmp_path) -> None: + context_root = tmp_path / "context" + config_path = tmp_path / "custom.toml" + config_path.write_text( + f"[general]\ncontext_root = \"{context_root}\"\n", + encoding="utf-8", + ) + + model = load_config_model(config_path=config_path, merge_user=False) + assert model.general.context_root == context_root.resolve() diff --git a/tests/test_discovery.py b/tests/test_discovery.py new file mode 100644 index 0000000..7456c1d --- /dev/null +++ b/tests/test_discovery.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pathlib import Path + +from afs.discovery import discover_contexts, get_project_stats +from afs.manager import AFSManager +from afs.schema import AFSConfig, GeneralConfig + + +def test_discover_contexts_ignores_names(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + + alpha = root / "alpha" + alpha.mkdir() + + legacy = root / "legacy" + legacy.mkdir() + beta = legacy / "beta" + beta.mkdir() + + config = AFSConfig( + general=GeneralConfig(context_root=tmp_path / "context"), + ) + manager = AFSManager(config=config) + + manager.ensure(path=alpha) + manager.ensure(path=beta) + + contexts = discover_contexts(search_paths=[root], config=config, max_depth=2) + names = [context.project_name for context in contexts] + + assert "alpha" in names + assert "beta" not in names + + stats = get_project_stats(contexts) + assert stats["total_projects"] == 1 diff --git a/tests/test_manager.py b/tests/test_manager.py new file mode 100644 index 0000000..51461dd --- /dev/null +++ b/tests/test_manager.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from pathlib import Path + +from afs.manager import AFSManager +from afs.models import MountType +from afs.schema import AFSConfig, GeneralConfig + + +def _make_manager(tmp_path: Path) -> AFSManager: + context_root = tmp_path / "context" + general = GeneralConfig( + context_root=context_root, + agent_workspaces_dir=context_root / "workspaces", + ) + return AFSManager(config=AFSConfig(general=general)) + + +def test_ensure_creates_context_and_metadata(tmp_path: Path) -> None: + manager = _make_manager(tmp_path) + project_path = tmp_path / "project" + project_path.mkdir() + + context = manager.ensure(path=project_path, context_root=tmp_path / "context") + + assert context.path.exists() + assert (context.path / "metadata.json").exists() + assert (context.path / "memory").exists() + assert (context.path / "knowledge").exists() + + +def test_ensure_with_link_creates_symlink(tmp_path: Path) -> None: + manager = _make_manager(tmp_path) + project_path = tmp_path / "project" + project_path.mkdir() + context_root = tmp_path / "context" + + manager.ensure(path=project_path, context_root=context_root, link_context=True) + + link_path = project_path / ".context" + assert link_path.is_symlink() + assert link_path.resolve() == context_root.resolve() + + +def test_mount_and_unmount(tmp_path: Path) -> None: + manager = _make_manager(tmp_path) + project_path = tmp_path / "project" + project_path.mkdir() + context_root = tmp_path / "context" + + context = manager.ensure(path=project_path, context_root=context_root) + + source_dir = tmp_path / "source" + source_dir.mkdir() + + mount = manager.mount( + source_dir, + MountType.KNOWLEDGE, + context_path=context.path, + ) + + mount_path = context.path / "knowledge" / mount.name + assert mount_path.is_symlink() + assert mount_path.resolve() == source_dir.resolve() + + removed = manager.unmount(mount.name, MountType.KNOWLEDGE, context_path=context.path) + assert removed + assert not mount_path.exists() diff --git a/tests/test_plugins.py b/tests/test_plugins.py new file mode 100644 index 0000000..f6f0e93 --- /dev/null +++ b/tests/test_plugins.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from pathlib import Path + +from afs.plugins import discover_plugins +from afs.schema import AFSConfig, PluginsConfig + + +def test_discover_plugins_in_custom_dir(tmp_path: Path) -> None: + plugin_dir = tmp_path / "plugins" + plugin_dir.mkdir() + package_dir = plugin_dir / "afs_plugin_demo" + package_dir.mkdir() + (package_dir / "__init__.py").write_text("", encoding="utf-8") + + plugins = PluginsConfig(plugin_dirs=[plugin_dir], auto_discover=True) + config = AFSConfig(plugins=plugins) + + names = discover_plugins(config) + assert "afs_plugin_demo" in names