refactor: Decouple roles.
This commit is contained in:
@@ -16,9 +16,17 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
||||
config = {
|
||||
"gateway": {"port": 18000},
|
||||
"components": {
|
||||
"test-tool": {
|
||||
"description": "Test tool",
|
||||
"install": {
|
||||
"path": {"alias": "test-tool"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
"test-svc": {
|
||||
"component": "test-svc-comp",
|
||||
"description": "Test service",
|
||||
"source": "test-svc",
|
||||
"run": {
|
||||
"runner": "python",
|
||||
"tool": "test-svc",
|
||||
@@ -39,11 +47,15 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
||||
"systemd": {},
|
||||
},
|
||||
},
|
||||
"test-tool": {
|
||||
"description": "Test tool",
|
||||
"install": {
|
||||
"path": {"alias": "test-tool"},
|
||||
},
|
||||
"jobs": {
|
||||
"test-job": {
|
||||
"description": "Test job",
|
||||
"run": {
|
||||
"runner": "command",
|
||||
"argv": ["test-job"],
|
||||
},
|
||||
"schedule": "0 2 * * *",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from castle_cli.config import load_config
|
||||
from castle_cli.manifest import Role
|
||||
|
||||
|
||||
class TestCreateCommand:
|
||||
@@ -42,11 +41,12 @@ class TestCreateCommand:
|
||||
assert (project_dir / "tests" / "test_health.py").exists()
|
||||
assert (project_dir / "CLAUDE.md").exists()
|
||||
|
||||
# Verify registered as ComponentManifest
|
||||
# Verify registered as ComponentSpec + ServiceSpec
|
||||
assert "my-api" in config.components
|
||||
manifest = config.components["my-api"]
|
||||
assert Role.SERVICE in manifest.roles
|
||||
assert manifest.expose.http.internal.port == 9050
|
||||
assert "my-api" in config.services
|
||||
svc = config.services["my-api"]
|
||||
assert svc.expose.http.internal.port == 9050
|
||||
assert svc.component == "my-api"
|
||||
mock_save.assert_called_once()
|
||||
|
||||
def test_create_tool(self, castle_root: Path) -> None:
|
||||
@@ -60,17 +60,18 @@ class TestCreateCommand:
|
||||
|
||||
from castle_cli.commands.create import run_create
|
||||
|
||||
args = Namespace(name="my-tool", type="tool", description="My tool", port=None)
|
||||
args = Namespace(name="my-tool2", type="tool", description="My tool", port=None)
|
||||
result = run_create(args)
|
||||
|
||||
assert result == 0
|
||||
project_dir = castle_root / "components" / "my-tool"
|
||||
project_dir = castle_root / "components" / "my-tool2"
|
||||
assert project_dir.exists()
|
||||
assert (project_dir / "src" / "my_tool" / "main.py").exists()
|
||||
assert (project_dir / "src" / "my_tool2" / "main.py").exists()
|
||||
assert (project_dir / "CLAUDE.md").exists()
|
||||
assert "my-tool" in config.components
|
||||
manifest = config.components["my-tool"]
|
||||
assert Role.TOOL in manifest.roles
|
||||
assert "my-tool2" in config.components
|
||||
comp = config.components["my-tool2"]
|
||||
assert comp.tool is not None
|
||||
assert comp.install is not None
|
||||
|
||||
def test_create_library(self, castle_root: Path) -> None:
|
||||
"""Create a new library project."""
|
||||
@@ -101,6 +102,7 @@ class TestCreateCommand:
|
||||
|
||||
from castle_cli.commands.create import run_create
|
||||
|
||||
# test-svc exists in the services section
|
||||
args = Namespace(
|
||||
name="test-svc",
|
||||
type="service",
|
||||
@@ -130,8 +132,8 @@ class TestCreateCommand:
|
||||
)
|
||||
run_create(args)
|
||||
|
||||
manifest = config.components["auto-port-svc"]
|
||||
port = manifest.expose.http.internal.port
|
||||
svc = config.services["auto-port-svc"]
|
||||
port = svc.expose.http.internal.port
|
||||
# Port 18000 is gateway, 19000 is test-svc, so next should be 9001+
|
||||
assert port is not None
|
||||
assert port not in (18000, 19000)
|
||||
|
||||
@@ -59,8 +59,8 @@ class TestInfoCommand:
|
||||
output = capsys.readouterr().out
|
||||
assert "not found" in output
|
||||
|
||||
def test_info_json(self, castle_root: Path, capsys) -> None:
|
||||
"""--json produces valid JSON with manifest fields."""
|
||||
def test_info_json_service(self, castle_root: Path, capsys) -> None:
|
||||
"""--json produces valid JSON with service fields."""
|
||||
from castle_cli.config import load_config
|
||||
|
||||
with patch("castle_cli.commands.info.load_config") as mock_load:
|
||||
@@ -73,48 +73,8 @@ class TestInfoCommand:
|
||||
assert result == 0
|
||||
output = capsys.readouterr().out
|
||||
data = json.loads(output)
|
||||
assert data["id"] == "test-svc"
|
||||
assert "service" in data["roles"]
|
||||
assert data["expose"]["http"]["internal"]["port"] == 19000
|
||||
|
||||
def test_info_shows_claude_md(self, castle_root: Path, capsys) -> None:
|
||||
"""Info shows CLAUDE.md content when present."""
|
||||
project_dir = castle_root / "test-svc"
|
||||
project_dir.mkdir(exist_ok=True)
|
||||
(project_dir / "CLAUDE.md").write_text("# Test Service\n\nSome docs here.")
|
||||
|
||||
from castle_cli.config import load_config
|
||||
|
||||
with patch("castle_cli.commands.info.load_config") as mock_load:
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
|
||||
from castle_cli.commands.info import run_info
|
||||
|
||||
result = run_info(Namespace(project="test-svc", json=False))
|
||||
|
||||
assert result == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "Some docs here" in output
|
||||
|
||||
def test_info_json_includes_claude_md(self, castle_root: Path, capsys) -> None:
|
||||
"""--json includes claude_md field when CLAUDE.md exists."""
|
||||
project_dir = castle_root / "test-svc"
|
||||
project_dir.mkdir(exist_ok=True)
|
||||
(project_dir / "CLAUDE.md").write_text("# Docs\n")
|
||||
|
||||
from castle_cli.config import load_config
|
||||
|
||||
with patch("castle_cli.commands.info.load_config") as mock_load:
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
|
||||
from castle_cli.commands.info import run_info
|
||||
|
||||
result = run_info(Namespace(project="test-svc", json=True))
|
||||
|
||||
assert result == 0
|
||||
data = json.loads(capsys.readouterr().out)
|
||||
assert "claude_md" in data
|
||||
assert "# Docs" in data["claude_md"]
|
||||
assert data["category"] == "service"
|
||||
assert data["service"]["expose"]["http"]["internal"]["port"] == 19000
|
||||
|
||||
def test_info_shows_env(self, castle_root: Path, capsys) -> None:
|
||||
"""Info displays environment variables."""
|
||||
@@ -131,8 +91,8 @@ class TestInfoCommand:
|
||||
output = capsys.readouterr().out
|
||||
assert "TEST_SVC_DATA_DIR" in output
|
||||
|
||||
def test_info_shows_roles(self, castle_root: Path, capsys) -> None:
|
||||
"""Info displays derived roles."""
|
||||
def test_info_shows_category(self, castle_root: Path, capsys) -> None:
|
||||
"""Info displays category instead of roles."""
|
||||
from castle_cli.config import load_config
|
||||
|
||||
with patch("castle_cli.commands.info.load_config") as mock_load:
|
||||
@@ -144,5 +104,5 @@ class TestInfoCommand:
|
||||
|
||||
assert result == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "roles" in output
|
||||
assert "category" in output
|
||||
assert "service" in output
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestListCommand:
|
||||
from castle_cli.config import load_config
|
||||
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
args = Namespace(role=None, json=False)
|
||||
args = Namespace(type=None, json=False)
|
||||
result = run_list(args)
|
||||
|
||||
assert result == 0
|
||||
@@ -27,27 +27,13 @@ class TestListCommand:
|
||||
assert "test-svc" in captured.out
|
||||
assert "test-tool" in captured.out
|
||||
|
||||
def test_list_filter_role(self, castle_root: Path, capsys: object) -> None:
|
||||
"""List filtered by role."""
|
||||
with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
|
||||
from castle_cli.config import load_config
|
||||
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
args = Namespace(role="tool", json=False)
|
||||
result = run_list(args)
|
||||
|
||||
assert result == 0
|
||||
captured = capsys.readouterr() # type: ignore[attr-defined]
|
||||
assert "test-tool" in captured.out
|
||||
assert "test-svc" not in captured.out
|
||||
|
||||
def test_list_filter_service(self, castle_root: Path, capsys: object) -> None:
|
||||
"""List filtered to services."""
|
||||
with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
|
||||
from castle_cli.config import load_config
|
||||
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
args = Namespace(role="service", json=False)
|
||||
args = Namespace(type="service", json=False)
|
||||
result = run_list(args)
|
||||
|
||||
assert result == 0
|
||||
@@ -55,13 +41,41 @@ class TestListCommand:
|
||||
assert "test-svc" in captured.out
|
||||
assert "test-tool" not in captured.out
|
||||
|
||||
def test_list_filter_tool(self, castle_root: Path, capsys: object) -> None:
|
||||
"""List filtered to tools."""
|
||||
with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
|
||||
from castle_cli.config import load_config
|
||||
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
args = Namespace(type="tool", json=False)
|
||||
result = run_list(args)
|
||||
|
||||
assert result == 0
|
||||
captured = capsys.readouterr() # type: ignore[attr-defined]
|
||||
assert "test-tool" in captured.out
|
||||
assert "test-svc" not in captured.out
|
||||
|
||||
def test_list_filter_job(self, castle_root: Path, capsys: object) -> None:
|
||||
"""List filtered to jobs."""
|
||||
with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
|
||||
from castle_cli.config import load_config
|
||||
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
args = Namespace(type="job", json=False)
|
||||
result = run_list(args)
|
||||
|
||||
assert result == 0
|
||||
captured = capsys.readouterr() # type: ignore[attr-defined]
|
||||
assert "test-job" in captured.out
|
||||
assert "test-svc" not in captured.out
|
||||
|
||||
def test_list_json(self, castle_root: Path, capsys: object) -> None:
|
||||
"""List output as JSON."""
|
||||
with patch("castle_cli.commands.list_cmd.load_config") as mock_load:
|
||||
from castle_cli.config import load_config
|
||||
|
||||
mock_load.return_value = load_config(castle_root)
|
||||
args = Namespace(role=None, json=True)
|
||||
args = Namespace(type=None, json=True)
|
||||
result = run_list(args)
|
||||
|
||||
assert result == 0
|
||||
@@ -71,5 +85,4 @@ class TestListCommand:
|
||||
assert "test-svc" in names
|
||||
assert "test-tool" in names
|
||||
svc = next(p for p in data if p["name"] == "test-svc")
|
||||
assert "roles" in svc
|
||||
assert "service" in svc["roles"]
|
||||
assert svc["category"] == "service"
|
||||
|
||||
Reference in New Issue
Block a user