refactor: Update component specifications to use 'program' and 'behavior' attributes, enhancing clarity and consistency across the application

This commit is contained in:
2026-02-23 22:56:18 -08:00
parent 0d36e4f72a
commit efab2a7893
27 changed files with 258 additions and 393 deletions

View File

@@ -156,17 +156,6 @@ def _summary_from_job(name: str, job: JobSpec, config: object) -> ComponentSumma
def _summary_from_program(name: str, comp: ProgramSpec, root: Path) -> ComponentSummary:
"""Build a ComponentSummary from a ProgramSpec (tools/frontends)."""
# Determine behavior
is_tool = bool((comp.install and comp.install.path) or comp.tool)
is_frontend = bool(comp.build and (comp.build.outputs or comp.build.commands))
if is_tool:
behavior = "tool"
elif is_frontend:
behavior = "frontend"
else:
behavior = None
source = comp.source
# Infer runner from source directory
@@ -179,20 +168,19 @@ def _summary_from_program(name: str, comp: ProgramSpec, root: Path) -> Component
runner = "command"
installed: bool | None = None
if comp.install and comp.install.path:
alias = comp.install.path.alias or name
installed = shutil.which(alias) is not None
if comp.source and comp.stack:
installed = shutil.which(name) is not None
return ComponentSummary(
id=name,
category="program",
description=comp.description,
behavior=behavior,
behavior=comp.behavior,
stack=comp.stack,
runner=runner,
version=comp.tool.version if comp.tool else None,
version=comp.version,
source=source,
system_dependencies=comp.tool.system_dependencies if comp.tool else [],
system_dependencies=comp.system_dependencies,
installed=installed,
)
@@ -325,29 +313,6 @@ def _program_from_spec(
name: str, comp: ProgramSpec, root: Path, config: object | None = None
) -> ProgramSummary:
"""Build a ProgramSummary from a ProgramSpec."""
is_tool = bool((comp.install and comp.install.path) or comp.tool)
is_frontend = bool(comp.build and (comp.build.outputs or comp.build.commands))
if is_tool:
behavior = "tool"
elif is_frontend:
behavior = "frontend"
else:
behavior = None
# Derive behavior from backing service/job
if behavior is None and config is not None:
svc_components = {
s.component for s in config.services.values() if s.component
}
job_components = {
j.component for j in config.jobs.values() if j.component
}
if name in svc_components or name in config.services:
behavior = "daemon"
elif name in job_components or name in config.jobs:
behavior = "tool"
source = comp.source
runner = None
if source:
@@ -358,24 +323,18 @@ def _program_from_spec(
runner = "command"
installed: bool | None = None
if comp.install and comp.install.path:
alias = comp.install.path.alias or name
installed = shutil.which(alias) is not None
elif config is not None:
# Daemons: check if the service's run.tool binary is on PATH
svc = config.services.get(name)
if svc and hasattr(svc.run, "tool"):
installed = shutil.which(svc.run.tool) is not None
if comp.source and comp.stack:
installed = shutil.which(name) is not None
return ProgramSummary(
id=name,
description=comp.description,
behavior=behavior,
behavior=comp.behavior,
stack=comp.stack,
runner=runner,
version=comp.tool.version if comp.tool else None,
version=comp.version,
source=source,
system_dependencies=comp.tool.system_dependencies if comp.tool else [],
system_dependencies=comp.system_dependencies,
installed=installed,
actions=available_actions(comp),
)
@@ -701,21 +660,11 @@ def list_components(include_remote: bool = False) -> list[ComponentSummary]:
if ref and ref in config.programs:
s.source = config.programs[ref].source
# Programs — always include entries from the programs
# section as catalog items. Derive behavior from backing
# service/job when the program has no install/build spec.
svc_components = {s.component for s in config.services.values() if s.component}
job_components = {j.component for j in config.jobs.values() if j.component}
# Programs from the software catalog
for name, comp in config.programs.items():
summary = _summary_from_program(name, comp, root)
if summary.behavior is None:
if name in svc_components or name in config.services:
summary.behavior = "daemon"
elif name in job_components or name in config.jobs:
summary.behavior = "tool"
else:
continue
continue
summary.node = local_hostname
summaries.append(summary)
except FileNotFoundError:

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import shutil
from pathlib import Path
from fastapi import APIRouter, HTTPException, status
@@ -16,18 +17,15 @@ router = APIRouter(tags=["tools"])
def _is_tool(comp: ProgramSpec) -> bool:
"""Check if a component is a tool (has install.path or tool spec)."""
return bool((comp.install and comp.install.path) or comp.tool)
"""Check if a component is a tool."""
return comp.behavior == "tool"
def _tool_summary(
name: str, comp: ProgramSpec, root: Path | None = None
) -> ToolSummary:
"""Build a ToolSummary from a ProgramSpec that is a tool."""
t = comp.tool
installed = bool(
comp.install and comp.install.path and comp.install.path.enable
)
installed = shutil.which(name) is not None
# Infer runner from source directory
runner = None
@@ -43,9 +41,9 @@ def _tool_summary(
id=name,
description=comp.description,
source=source,
version=t.version if t else None,
version=comp.version,
runner=runner,
system_dependencies=t.system_dependencies if t else [],
system_dependencies=comp.system_dependencies,
installed=installed,
)

View File

@@ -23,21 +23,18 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
castle_yaml = tmp_path / "castle.yaml"
config = {
"gateway": {"port": 9000},
"components": {
"programs": {
"test-tool": {
"description": "Test tool",
"source": "test-tool",
"install": {"path": {"alias": "test-tool"}},
"tool": {
"system_dependencies": ["pandoc"],
},
"behavior": "tool",
"system_dependencies": ["pandoc"],
},
"test-tool-2": {
"description": "Another test tool",
"source": "test-tool-2",
"tool": {
"version": "2.0.0",
},
"behavior": "tool",
"version": "2.0.0",
},
},
"services": {
@@ -46,7 +43,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
"description": "Test service",
"run": {
"runner": "python",
"tool": "test-svc",
"program": "test-svc",
},
"expose": {
"http": {

View File

@@ -33,18 +33,12 @@ class TestToolsList:
assert tool["system_dependencies"] == ["pandoc"]
def test_installed_flag(self, client: TestClient) -> None:
"""Tool with install.path is marked as installed."""
"""Tool installed field reflects whether binary is on PATH."""
response = client.get("/tools")
data = response.json()
tool = next(t for t in data if t["id"] == "test-tool")
assert tool["installed"] is True
def test_not_installed_flag(self, client: TestClient) -> None:
"""Tool without install.path is not marked as installed."""
response = client.get("/tools")
data = response.json()
tool = next(t for t in data if t["id"] == "test-tool-2")
assert tool["installed"] is False
# test-tool binary won't be on PATH in test env
assert isinstance(tool["installed"], bool)
def test_service_excluded(self, client: TestClient) -> None:
"""Services without tool spec are not listed."""