feat(resource): add virtual core resource types #661

Merged
hamza.khyari merged 1 commits from feature/post-resource-types-virtual-core into master 2026-03-18 01:52:25 +00:00
20 changed files with 1723 additions and 18 deletions
+9
View File
@@ -369,6 +369,15 @@
configurable debouncing and optional `RESOURCE_MODIFIED` EventBus emission.
Includes 166 Behave BDD scenarios, 9 Robot Framework integration tests, ASV
benchmarks, and reference documentation. (#578)
- Added built-in virtual core resource types (`file`, `directory`, `commit`,
`branch`, `tag`, `tree`) under `examples/resource-types/`. All use
`resource_kind: virtual`, `sandbox_strategy: none`, `user_addable: false`.
Equivalence metadata defines identity criteria per type (content hash for
files, git object identity for commits/branches/tags/trees). Extended
bootstrap registration in `ResourceRegistryService` to include virtual
types and updated `ResourceTypeSpec.BUILTIN_NAMES`. Added reference
documentation in `docs/reference/resource_types_builtin.md`. Includes
Behave BDD tests, Robot integration tests, and ASV benchmarks. (#329)
- Added general-purpose domain event system under
`cleveragents.infrastructure.events`. `EventType` StrEnum defines 38 typed
event identifiers across 9 domains (plan lifecycle, decision, invariant, actor,
@@ -0,0 +1,81 @@
"""ASV benchmarks for virtual core resource type YAML loading."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
from cleveragents.resource.schema import ResourceTypeConfigSchema
_EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" / "resource-types"
_VIRTUAL_CORE_NAMES = ("file", "directory", "commit", "branch", "tag", "tree")
_VIRTUAL_YAML_PATHS = {
name: _EXAMPLES_DIR / f"{name}.yaml" for name in _VIRTUAL_CORE_NAMES
}
class TimeVirtualCoreSchemaLoad:
"""Benchmark virtual core resource type YAML schema loading."""
timeout = 60
def setup(self) -> None:
self.schema_cls = ResourceTypeConfigSchema
self.paths = dict(_VIRTUAL_YAML_PATHS)
def time_load_file_yaml(self) -> None:
self.schema_cls.from_yaml_file(self.paths["file"])
def time_load_directory_yaml(self) -> None:
self.schema_cls.from_yaml_file(self.paths["directory"])
def time_load_all_virtual_core(self) -> None:
for path in self.paths.values():
self.schema_cls.from_yaml_file(path)
class TimeVirtualCoreDomainModelLoad:
"""Benchmark virtual core resource type domain model creation."""
timeout = 60
def setup(self) -> None:
self.spec_cls = ResourceTypeSpec
self.configs: dict[str, dict[str, Any]] = {}
for name, path in _VIRTUAL_YAML_PATHS.items():
with open(path, encoding="utf-8") as f:
self.configs[name] = yaml.safe_load(f)
def time_file_from_config(self) -> None:
self.spec_cls.from_config(self.configs["file"])
def time_commit_from_config(self) -> None:
self.spec_cls.from_config(self.configs["commit"])
def time_all_virtual_core_from_config(self) -> None:
for config in self.configs.values():
self.spec_cls.from_config(config)
class TimeVirtualCoreCliDict:
"""Benchmark as_cli_dict rendering for virtual core types."""
timeout = 60
def setup(self) -> None:
self.specs: dict[str, ResourceTypeSpec] = {}
for name, path in _VIRTUAL_YAML_PATHS.items():
with open(path, encoding="utf-8") as f:
config: dict[str, Any] = yaml.safe_load(f)
self.specs[name] = ResourceTypeSpec.from_config(config)
def time_file_cli_dict(self) -> None:
self.specs["file"].as_cli_dict()
def time_all_virtual_core_cli_dict(self) -> None:
for spec in self.specs.values():
spec.as_cli_dict()
+140
View File
@@ -4,6 +4,19 @@ CleverAgents ships with several built-in resource types that cover common
filesystem and version-control workflows. Built-in types use simple
unnamespaced names and are registered automatically at startup.
Built-in types are organized into two groups:
- **Physical types** represent tangible assets (files, directories, repos)
with direct read/write access and sandbox strategies.
- **Virtual types** represent abstract identities that link equivalent
physical resources across layers and repositories. Virtual types have
no handler, no sandbox strategy, and are never user-addable. They are
created automatically by the system when equivalent physical resources
are detected.
See `docs/specification.md` section "Physical vs Virtual Resources"
(~line 24548) for the full design rationale.
## fs-mount
A physical mount point on the local system. Use this type to represent
@@ -156,3 +169,130 @@ for version-controlled projects.
| write | yes |
| sandbox | yes |
| checkpoint | yes |
---
## Virtual Core Resource Types
Virtual types use simple unnamespaced names that mirror their physical
counterparts. A virtual resource answers the question: "where else does
this same thing exist?" Two physical resources share a virtual parent
when they are equivalent by the virtual type's criteria.
All virtual types share these properties:
| Field | Value |
|---|---|
| **resource_kind** | virtual |
| **sandbox_strategy** | none |
| **user_addable** | false |
| **handler** | none |
All capabilities are disabled (`read: false`, `write: false`,
`sandbox: false`, `checkpoint: false`) because virtual types represent
abstract identities, not concrete locations. Tools always operate on
their physical children.
---
### file
Cross-layer file identity. Links physical `fs-file` and `git-tree-entry`
resources that represent the same file -- identical content bytes,
filename, and permissions.
- **Child types**: `fs-file`, `git-tree-entry`
- **Parent types**: none (top-level virtual)
#### Equivalence
| Criterion | Description |
|---|---|
| `content_hash` | SHA-256 of file content bytes |
| `filename` | File name component |
| `permissions` | File permission mode |
---
### directory
Cross-layer directory identity. Links physical `fs-directory` and
`git-tree` resources whose recursive contents are equivalent.
- **Child types**: `fs-directory`, `git-tree`
- **Parent types**: none (top-level virtual)
#### Equivalence
| Criterion | Description |
|---|---|
| `merkle_hash` | Merkle hash of recursive child content hashes |
| `name` | Directory name |
| `permissions` | Directory permission mode |
---
### commit
Cross-repo commit identity. Links `git-commit` resources across
different repos that share the same commit hash (e.g., fork and
upstream).
- **Child types**: `git-commit`
- **Parent types**: none (top-level virtual)
#### Equivalence
| Criterion | Description |
|---|---|
| `commit_hash` | Git commit SHA |
---
### branch
Cross-repo branch identity. Links `git-branch` resources across
different repos with the same branch name and HEAD commit hash.
- **Child types**: `git-branch`
- **Parent types**: none (top-level virtual)
#### Equivalence
| Criterion | Description |
|---|---|
| `name` | Branch name |
| `head_commit_hash` | HEAD commit SHA |
---
### tag
Cross-repo tag identity. Links `git-tag` resources across different
repos with the same tag name and target object.
- **Child types**: `git-tag`
- **Parent types**: none (top-level virtual)
#### Equivalence
| Criterion | Description |
|---|---|
| `name` | Tag name |
| `target_sha` | Target object SHA |
---
### tree
Cross-repo/cross-commit tree identity. Links `git-tree` resources
across different commits or repos that have the same tree hash.
- **Child types**: `git-tree`
- **Parent types**: none (top-level virtual)
#### Equivalence
| Criterion | Description |
|---|---|
| `tree_hash` | Git tree object SHA |
+22
View File
@@ -0,0 +1,22 @@
# Spec reference: docs/specification.md ~lines 10133-10143, 24548-24632
schema_version: "1.0"
name: branch
description: "Cross-repo branch identity linking branches with the same name and HEAD"
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["git-branch"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria:
- name
- head_commit_hash
description: "Two physical branches are equivalent when they share the same branch name and HEAD commit hash"
handler: null
+21
View File
@@ -0,0 +1,21 @@
# Spec reference: docs/specification.md ~lines 10133-10143, 24548-24632
schema_version: "1.0"
name: commit
description: "Cross-repo commit identity linking commits with the same hash"
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["git-commit"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria:
- commit_hash
description: "Two physical commits are equivalent when they share the same commit hash"
handler: null
+23
View File
@@ -0,0 +1,23 @@
# Spec reference: docs/specification.md ~lines 10133-10143, 24548-24632
schema_version: "1.0"
name: directory
description: "Cross-layer directory identity linking equivalent physical directories"
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["fs-directory", "git-tree"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria:
- merkle_hash
- name
- permissions
description: "Two physical resources are equivalent when they share the same Merkle hash, name, and permissions"
handler: null
+23
View File
@@ -0,0 +1,23 @@
# Spec reference: docs/specification.md ~lines 10133-10143, 24548-24632
schema_version: "1.0"
name: file
description: "Cross-layer file identity linking equivalent physical files"
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["fs-file", "git-tree-entry"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria:
- content_hash
- filename
- permissions
description: "Two physical resources are equivalent when they share the same content bytes (SHA-256), filename, and permissions"
handler: null
+22
View File
@@ -0,0 +1,22 @@
# Spec reference: docs/specification.md ~lines 10133-10143, 24548-24632
schema_version: "1.0"
name: tag
description: "Cross-repo tag identity linking tags with the same name and target"
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["git-tag"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria:
- name
- target_sha
description: "Two physical tags are equivalent when they share the same tag name and target object"
handler: null
+21
View File
@@ -0,0 +1,21 @@
# Spec reference: docs/specification.md ~lines 10133-10143, 24548-24632
schema_version: "1.0"
name: tree
description: "Cross-repo/cross-commit tree identity linking trees with the same hash"
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["git-tree"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria:
- tree_hash
description: "Two physical tree objects are equivalent when they share the same tree hash"
handler: null
+354
View File
@@ -0,0 +1,354 @@
Feature: Virtual Core Resource Types
As a CleverAgents developer
I want built-in virtual resource type YAMLs (file, directory, commit, branch, tag, tree)
So that cross-layer and cross-repo identity linking is available out of the box
# ── YAML schema validation ────────────────────────────────
Scenario Outline: Virtual core YAML validates against schema for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema should be valid
And the vcore schema name should be "<type_name>"
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Resource kind is virtual ──────────────────────────────
Scenario Outline: Virtual core types have resource_kind virtual for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema resource_kind should be "virtual"
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Sandbox strategy is none ──────────────────────────────
Scenario Outline: Virtual core types have sandbox_strategy none for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema sandbox_strategy should be "none"
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Not user-addable ──────────────────────────────────────
Scenario Outline: Virtual core types are not user-addable for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema user_addable should be false
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Built-in and unnamespaced ─────────────────────────────
Scenario Outline: Virtual core types are built-in and unnamespaced for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema built_in should be true
And the vcore schema name should not contain "/"
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── All capabilities disabled ─────────────────────────────
Scenario Outline: Virtual core types have all capabilities disabled for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore capabilities should have read false
And the vcore capabilities should have write false
And the vcore capabilities should have sandbox false
And the vcore capabilities should have checkpoint false
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── No CLI args ───────────────────────────────────────────
Scenario Outline: Virtual core types have no cli_args for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema should have 0 cli_args
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── No parent types (top-level virtual) ───────────────────
Scenario Outline: Virtual core types have no parent types for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema should have empty parent_types
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Child types per spec ──────────────────────────────────
Scenario: file virtual type has correct child types for vcore
Given the built-in file virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema child_types should contain "fs-file"
And the vcore schema child_types should contain "git-tree-entry"
And the vcore schema child_types should have exactly 2 entries
Scenario: directory virtual type has correct child types for vcore
Given the built-in directory virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema child_types should contain "fs-directory"
And the vcore schema child_types should contain "git-tree"
And the vcore schema child_types should have exactly 2 entries
Scenario: commit virtual type has correct child types for vcore
Given the built-in commit virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema child_types should contain "git-commit"
And the vcore schema child_types should have exactly 1 entries
Scenario: branch virtual type has correct child types for vcore
Given the built-in branch virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema child_types should contain "git-branch"
And the vcore schema child_types should have exactly 1 entries
Scenario: tag virtual type has correct child types for vcore
Given the built-in tag virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema child_types should contain "git-tag"
And the vcore schema child_types should have exactly 1 entries
Scenario: tree virtual type has correct child types for vcore
Given the built-in tree virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema child_types should contain "git-tree"
And the vcore schema child_types should have exactly 1 entries
# ── Equivalence metadata present ──────────────────────────
Scenario: file virtual type has equivalence with content_hash for vcore
Given the built-in file virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema equivalence should not be none
And the vcore equivalence criteria should contain "content_hash"
And the vcore equivalence criteria should contain "filename"
And the vcore equivalence criteria should contain "permissions"
And the vcore equivalence criteria count should be 3
Scenario: directory virtual type has equivalence with merkle_hash for vcore
Given the built-in directory virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema equivalence should not be none
And the vcore equivalence criteria should contain "merkle_hash"
And the vcore equivalence criteria should contain "name"
And the vcore equivalence criteria should contain "permissions"
And the vcore equivalence criteria count should be 3
Scenario: commit virtual type has equivalence with commit_hash for vcore
Given the built-in commit virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema equivalence should not be none
And the vcore equivalence criteria should contain "commit_hash"
And the vcore equivalence criteria count should be 1
Scenario: branch virtual type has equivalence with name and head for vcore
Given the built-in branch virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema equivalence should not be none
And the vcore equivalence criteria should contain "name"
And the vcore equivalence criteria should contain "head_commit_hash"
And the vcore equivalence criteria count should be 2
Scenario: tag virtual type has equivalence with name and target for vcore
Given the built-in tag virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema equivalence should not be none
And the vcore equivalence criteria should contain "name"
And the vcore equivalence criteria should contain "target_sha"
And the vcore equivalence criteria count should be 2
Scenario: tree virtual type has equivalence with tree_hash for vcore
Given the built-in tree virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema equivalence should not be none
And the vcore equivalence criteria count should be 1
And the vcore equivalence criteria should contain "tree_hash"
# ── Domain model loading ──────────────────────────────────
Scenario Outline: Virtual core YAMLs load as domain model for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML as a ResourceTypeSpec for vcore
Then the vcore domain model should be valid
And the vcore domain model name should be "<type_name>"
And the vcore domain model resource_kind should be virtual
And the vcore domain model should have equivalence
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Bootstrap registration includes virtual types ─────────
Scenario: Bootstrap builtin types list includes virtual core types for vcore
Given the bootstrap builtin type definitions for vcore
Then the builtin definitions should include "file" for vcore
And the builtin definitions should include "directory" for vcore
And the builtin definitions should include "commit" for vcore
And the builtin definitions should include "branch" for vcore
And the builtin definitions should include "tag" for vcore
And the builtin definitions should include "tree" for vcore
Scenario Outline: Bootstrap virtual types have correct properties for vcore
Given the bootstrap builtin type definitions for vcore
When I find the virtual type "<type_name>" in builtin definitions for vcore
Then the found builtin type user_addable should be false for vcore
And the found builtin type resource_kind should be "virtual" for vcore
And the found builtin type should have equivalence for vcore
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── BUILTIN_NAMES includes virtual types ──────────────────
Scenario: ResourceTypeSpec BUILTIN_NAMES includes virtual core types for vcore
Then the ResourceTypeSpec BUILTIN_NAMES should contain "file" for vcore
And the ResourceTypeSpec BUILTIN_NAMES should contain "directory" for vcore
And the ResourceTypeSpec BUILTIN_NAMES should contain "commit" for vcore
And the ResourceTypeSpec BUILTIN_NAMES should contain "branch" for vcore
And the ResourceTypeSpec BUILTIN_NAMES should contain "tag" for vcore
And the ResourceTypeSpec BUILTIN_NAMES should contain "tree" for vcore
# ── Auto-discovery is none for virtual types ──────────────
Scenario Outline: Virtual core types have no auto-discovery for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema auto_discovery should be none
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Handler is none for virtual types ─────────────────────
Scenario Outline: Virtual core types have no handler for vcore
Given the built-in <type_name> virtual YAML file for vcore
When I load the virtual YAML via from_yaml_file for vcore
Then the vcore schema handler should be none
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Negative / validation tests ──────────────────────────────
Scenario: Virtual type without equivalence is rejected for vcore
Given a virtual type config missing equivalence for vcore
When I attempt to create a ResourceTypeSpec from it for vcore
Then the vcore creation should fail with a validation error
And the vcore error message should mention "equivalence"
Scenario: Virtual type with empty criteria list is rejected for vcore
Given a virtual type config with empty equivalence criteria for vcore
When I attempt to create a ResourceTypeSpec from it for vcore
Then the vcore creation should fail with a validation error
And the vcore error message should mention "criteria"
# ── YAML ↔ registry data consistency ─────────────────────
Scenario Outline: YAML and registry data match for vcore
Given the built-in <type_name> virtual YAML file for vcore
And the bootstrap builtin type definitions for vcore
When I load the virtual YAML as a ResourceTypeSpec for vcore
And I find the virtual type "<type_name>" in builtin definitions for vcore
Then the YAML and registry should have matching fields for vcore
Examples:
| type_name |
| file |
| directory |
| commit |
| branch |
| tag |
| tree |
# ── Virtual types cannot be user-added ───────────────────
Scenario: Attempting to register a virtual resource instance is rejected for vcore
Given a bootstrapped resource registry for vcore
When I attempt to register a resource of virtual type "file" for vcore
Then the vcore resource registration should fail with "not user-addable"
@@ -253,8 +253,8 @@ def step_verify_ad_json(context: Any) -> None:
@given("a ResourceTypeSpec with equivalence set")
def step_spec_with_equivalence(context: Any) -> None:
context.equivalence_data = {
"fields": ["content_hash", "location"],
"strategy": "hash_compare",
"criteria": ["content_hash", "location"],
"description": "Hash-based equivalence comparison",
}
# Virtual types require equivalence, so this is consistent
context.cov_spec_eq = ResourceTypeSpec(
@@ -346,7 +346,7 @@ def step_db_row_with_equivalence(context: Any) -> None:
context.cov_eq_engine = engine
context.cov_eq_factory = factory
context.eq_original = {"fields": ["hash"], "strategy": "exact"}
context.eq_original = {"criteria": ["hash"], "description": "Exact match"}
session = factory()
from datetime import UTC, datetime
@@ -415,9 +415,9 @@ def step_yaml_with_ad_and_eq(context: Any) -> None:
" glob_pattern: '*.md'\n"
" max_depth: 5\n"
"equivalence:\n"
" fields:\n"
" criteria:\n"
" - content_hash\n"
" strategy: hash_compare\n"
" description: Hash-based equivalence\n"
)
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
tmp.write(yaml_content)
@@ -447,5 +447,5 @@ def step_verify_rt_ad(context: Any) -> None:
def step_verify_rt_eq(context: Any) -> None:
eq = context.cov_rt_shown.equivalence
assert eq is not None, "equivalence should be preserved"
assert eq["fields"] == ["content_hash"]
assert eq["strategy"] == "hash_compare"
assert eq["criteria"] == ["content_hash"]
assert eq["description"] == "Hash-based equivalence"
+4 -1
View File
@@ -45,7 +45,10 @@ def step_config_with_kind(context, kind):
@given('a resource type config with resource_kind "{kind}" and equivalence set')
def step_config_with_kind_and_equiv(context, kind):
config = _base_config(resource_kind=kind)
config["equivalence"] = {"strategy": "content_hash", "fields": ["hash"]}
config["equivalence"] = {
"criteria": ["content_hash"],
"description": "Test equivalence",
}
context.rt_config = config
@@ -0,0 +1,581 @@
"""Step definitions for resource_type_virtual_core.feature.
Tests virtual core resource types: file, directory, commit, branch, tag, tree.
All step suffixes use 'for vcore' to avoid AmbiguousStep conflicts.
Context attributes are accessed via ``_ctx()`` typed wrapper to satisfy
Pyright without inline ``# type: ignore`` suppressions.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
from behave import given, then, when
from pydantic import ValidationError
from cleveragents.application.services._resource_registry_data import (
BUILTIN_TYPES,
)
from cleveragents.domain.models.core.resource_type import (
ResourceKind,
ResourceTypeSpec,
)
from cleveragents.resource.schema import ResourceTypeConfigSchema
_EXAMPLES_DIR = (
Path(__file__).resolve().parent.parent.parent / "examples" / "resource-types"
)
# ---------------------------------------------------------------------------
# Typed context accessor — eliminates inline type: ignore suppressions
# ---------------------------------------------------------------------------
_SENTINEL = object()
# Attribute name mapping: property name -> context attribute name.
_ATTR_MAP: dict[str, str] = {
"yaml_path": "vcore_yaml_path",
"schema": "vcore_schema",
"domain": "vcore_domain",
"builtin_defs": "vcore_builtin_defs",
"found_builtin": "vcore_found_builtin",
"test_config": "vcore_test_config",
"created_spec": "vcore_created_spec",
"creation_error": "vcore_creation_error",
"registry_service": "vcore_registry_service",
"register_error": "vcore_register_error",
}
class _Ctx:
"""Typed proxy for Behave's dynamically-typed ``context`` object.
Centralises all dynamic attribute access behind typed properties so
step functions never need inline ``# type: ignore`` comments.
Uses a dict-based lookup to satisfy both Pyright (typed properties)
and ruff B009/B010 (no constant-string getattr/setattr).
"""
__slots__ = ("_ctx",)
def __init__(self, ctx: object) -> None:
object.__setattr__(self, "_ctx", ctx)
def _get(self, key: str, default: Any = _SENTINEL) -> Any:
attr = _ATTR_MAP[key]
if default is _SENTINEL:
return getattr(self._ctx, attr)
return getattr(self._ctx, attr, default)
def _set(self, key: str, value: Any) -> None:
setattr(self._ctx, _ATTR_MAP[key], value)
@property
def yaml_path(self) -> Path:
return self._get("yaml_path")
@yaml_path.setter
def yaml_path(self, value: Path) -> None:
self._set("yaml_path", value)
@property
def schema(self) -> ResourceTypeConfigSchema:
return self._get("schema")
@schema.setter
def schema(self, value: ResourceTypeConfigSchema) -> None:
self._set("schema", value)
@property
def domain(self) -> ResourceTypeSpec:
return self._get("domain")
@domain.setter
def domain(self, value: ResourceTypeSpec) -> None:
self._set("domain", value)
@property
def builtin_defs(self) -> list[dict[str, Any]]:
return self._get("builtin_defs")
@builtin_defs.setter
def builtin_defs(self, value: list[dict[str, Any]]) -> None:
self._set("builtin_defs", value)
@property
def found_builtin(self) -> dict[str, Any]:
return self._get("found_builtin")
@found_builtin.setter
def found_builtin(self, value: dict[str, Any]) -> None:
self._set("found_builtin", value)
@property
def test_config(self) -> dict[str, Any]:
return self._get("test_config")
@test_config.setter
def test_config(self, value: dict[str, Any]) -> None:
self._set("test_config", value)
@property
def created_spec(self) -> ResourceTypeSpec | None:
return self._get("created_spec", None)
@created_spec.setter
def created_spec(self, value: ResourceTypeSpec | None) -> None:
self._set("created_spec", value)
@property
def creation_error(self) -> Exception | None:
return self._get("creation_error", None)
@creation_error.setter
def creation_error(self, value: Exception | None) -> None:
self._set("creation_error", value)
@property
def registry_service(self) -> Any:
return self._get("registry_service")
@registry_service.setter
def registry_service(self, value: Any) -> None:
self._set("registry_service", value)
@property
def register_error(self) -> Exception | None:
return self._get("register_error", None)
@register_error.setter
def register_error(self, value: Exception | None) -> None:
self._set("register_error", value)
def _ctx(context: object) -> _Ctx:
"""Wrap Behave context with typed accessors."""
return _Ctx(context)
# ── Given steps ──────────────────────────────────────────────
@given("the built-in {type_name} virtual YAML file for vcore")
def step_builtin_virtual_yaml(context: object, type_name: str) -> None:
"""Load path to a virtual core type YAML file."""
path = _EXAMPLES_DIR / f"{type_name}.yaml"
assert path.exists(), f"YAML file not found: {path}"
_ctx(context).yaml_path = path
@given("the bootstrap builtin type definitions for vcore")
def step_bootstrap_builtin_defs(context: object) -> None:
"""Access the BUILTIN_TYPES list from the registry data module."""
_ctx(context).builtin_defs = list(BUILTIN_TYPES)
# ── When steps ───────────────────────────────────────────────
@when("I load the virtual YAML via from_yaml_file for vcore")
def step_load_virtual_yaml(context: object) -> None:
"""Load virtual core YAML using the schema loader."""
c = _ctx(context)
c.schema = ResourceTypeConfigSchema.from_yaml_file(c.yaml_path)
@when("I load the virtual YAML as a ResourceTypeSpec for vcore")
def step_load_virtual_as_domain(context: object) -> None:
"""Load virtual core YAML as a domain model."""
c = _ctx(context)
raw: dict[str, Any] = yaml.safe_load(c.yaml_path.read_text(encoding="utf-8"))
c.domain = ResourceTypeSpec.from_config(raw)
@when('I find the virtual type "{type_name}" in builtin definitions for vcore')
def step_find_virtual_in_builtins(context: object, type_name: str) -> None:
"""Find a specific virtual type entry in BUILTIN_TYPES."""
c = _ctx(context)
found = [d for d in c.builtin_defs if d["name"] == type_name]
assert len(found) == 1, (
f"Expected exactly 1 entry for '{type_name}', found {len(found)}"
)
c.found_builtin = found[0]
# ── Then steps: schema validation ────────────────────────────
@then("the vcore schema should be valid")
def step_vcore_schema_valid(context: object) -> None:
schema = _ctx(context).schema
assert schema is not None
assert isinstance(schema, ResourceTypeConfigSchema)
@then('the vcore schema name should be "{name}"')
def step_vcore_schema_name(context: object, name: str) -> None:
assert _ctx(context).schema.name == name
@then('the vcore schema resource_kind should be "{kind}"')
def step_vcore_schema_kind(context: object, kind: str) -> None:
assert _ctx(context).schema.resource_kind == kind
@then('the vcore schema sandbox_strategy should be "{strategy}"')
def step_vcore_schema_sandbox(context: object, strategy: str) -> None:
assert _ctx(context).schema.sandbox_strategy == strategy
@then("the vcore schema user_addable should be false")
def step_vcore_schema_not_addable(context: object) -> None:
assert _ctx(context).schema.user_addable is False
@then("the vcore schema built_in should be true")
def step_vcore_schema_builtin(context: object) -> None:
assert _ctx(context).schema.built_in is True
@then('the vcore schema name should not contain "/"')
def step_vcore_schema_no_slash(context: object) -> None:
assert "/" not in _ctx(context).schema.name
@then("the vcore schema should have {count:d} cli_args")
def step_vcore_schema_cli_args_count(context: object, count: int) -> None:
actual = len(_ctx(context).schema.cli_args)
assert actual == count, f"Expected {count} cli_args, got {actual}"
@then("the vcore schema should have empty parent_types")
def step_vcore_schema_no_parents(context: object) -> None:
parent_types = _ctx(context).schema.parent_types
assert len(parent_types) == 0, f"Expected empty parent_types, got {parent_types}"
@then('the vcore schema child_types should contain "{child}"')
def step_vcore_schema_child(context: object, child: str) -> None:
children = _ctx(context).schema.child_types
assert child in children, f"'{child}' not in child_types: {children}"
@then("the vcore schema child_types should have exactly {count:d} entries")
def step_vcore_schema_child_count(context: object, count: int) -> None:
actual = len(_ctx(context).schema.child_types)
assert actual == count, f"Expected {count} child_types, got {actual}"
# ── Then steps: capabilities ─────────────────────────────────
@then("the vcore capabilities should have read {value}")
def step_vcore_caps_read(context: object, value: str) -> None:
expected = value.lower() == "true"
actual = _ctx(context).schema.capabilities["read"]
assert actual is expected, f"read: expected {expected}, got {actual}"
@then("the vcore capabilities should have write {value}")
def step_vcore_caps_write(context: object, value: str) -> None:
expected = value.lower() == "true"
actual = _ctx(context).schema.capabilities["write"]
assert actual is expected, f"write: expected {expected}, got {actual}"
@then("the vcore capabilities should have sandbox {value}")
def step_vcore_caps_sandbox(context: object, value: str) -> None:
expected = value.lower() == "true"
actual = _ctx(context).schema.capabilities["sandbox"]
assert actual is expected, f"sandbox: expected {expected}, got {actual}"
@then("the vcore capabilities should have checkpoint {value}")
def step_vcore_caps_checkpoint(context: object, value: str) -> None:
expected = value.lower() == "true"
actual = _ctx(context).schema.capabilities["checkpoint"]
assert actual is expected, f"checkpoint: expected {expected}, got {actual}"
# ── Then steps: equivalence ──────────────────────────────────
@then("the vcore schema equivalence should not be none")
def step_vcore_equivalence_not_none(context: object) -> None:
equiv = _ctx(context).schema.equivalence
assert equiv is not None, "equivalence should not be None for virtual types"
@then('the vcore equivalence criteria should contain "{criterion}"')
def step_vcore_equivalence_criterion(context: object, criterion: str) -> None:
equiv = _ctx(context).schema.equivalence
assert equiv is not None, "equivalence is None"
criteria = equiv.get("criteria", [])
assert criterion in criteria, f"'{criterion}' not in criteria: {criteria}"
# ── Then steps: auto-discovery and handler ───────────────────
@then("the vcore schema auto_discovery should be none")
def step_vcore_no_autodiscovery(context: object) -> None:
auto = _ctx(context).schema.auto_discovery
assert auto is None, f"Expected auto_discovery=None, got {auto}"
@then("the vcore schema handler should be none")
def step_vcore_no_handler(context: object) -> None:
handler = _ctx(context).schema.handler
assert handler is None, f"Expected handler=None, got {handler}"
# ── Then steps: domain model ────────────────────────────────
@then("the vcore domain model should be valid")
def step_vcore_domain_valid(context: object) -> None:
domain = _ctx(context).domain
assert domain is not None
assert isinstance(domain, ResourceTypeSpec)
@then('the vcore domain model name should be "{name}"')
def step_vcore_domain_name(context: object, name: str) -> None:
assert _ctx(context).domain.name == name
@then("the vcore domain model resource_kind should be virtual")
def step_vcore_domain_kind_virtual(context: object) -> None:
domain = _ctx(context).domain
assert domain.resource_kind == ResourceKind.VIRTUAL, (
f"Expected VIRTUAL, got {domain.resource_kind}"
)
# ── Then steps: bootstrap registration ──────────────────────
@then('the builtin definitions should include "{type_name}" for vcore')
def step_builtin_includes_type(context: object, type_name: str) -> None:
defs: list[dict[str, Any]] = _ctx(context).builtin_defs
names = [d["name"] for d in defs]
assert type_name in names, f"'{type_name}' not found in builtin defs: {names}"
@then("the found builtin type user_addable should be false for vcore")
def step_found_builtin_not_addable(context: object) -> None:
entry: dict[str, Any] = _ctx(context).found_builtin
assert entry["user_addable"] is False, (
f"Expected user_addable=False, got {entry['user_addable']}"
)
@then('the found builtin type resource_kind should be "{kind}" for vcore')
def step_found_builtin_kind(context: object, kind: str) -> None:
entry: dict[str, Any] = _ctx(context).found_builtin
assert entry["resource_kind"] == kind, (
f"Expected resource_kind='{kind}', got {entry['resource_kind']}"
)
@then("the found builtin type should have equivalence for vcore")
def step_found_builtin_has_equivalence(context: object) -> None:
entry: dict[str, Any] = _ctx(context).found_builtin
assert "equivalence" in entry, f"No 'equivalence' key in entry: {entry.keys()}"
assert entry["equivalence"] is not None, "equivalence is None"
assert "criteria" in entry["equivalence"], (
f"No 'criteria' in equivalence: {entry['equivalence']}"
)
# ── Then steps: BUILTIN_NAMES ──────────────────────────────
@then('the ResourceTypeSpec BUILTIN_NAMES should contain "{name}" for vcore')
def step_builtin_names_contains(context: object, name: str) -> None:
assert name in ResourceTypeSpec.BUILTIN_NAMES, (
f"'{name}' not in BUILTIN_NAMES: {ResourceTypeSpec.BUILTIN_NAMES}"
)
# ── Given/When/Then steps: negative validation tests ────────
@given("a virtual type config missing equivalence for vcore")
def step_virtual_config_missing_equiv(context: object) -> None:
"""Create a virtual type config dict without equivalence."""
_ctx(context).test_config = {
"name": "test-virtual-no-equiv",
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": [],
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
}
@given("a virtual type config with empty equivalence criteria for vcore")
def step_virtual_config_empty_criteria(context: object) -> None:
"""Create a virtual type config with equivalence but empty criteria."""
_ctx(context).test_config = {
"name": "test-virtual-empty-criteria",
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": [],
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": [],
"description": "Empty criteria test",
},
}
@when("I attempt to create a ResourceTypeSpec from it for vcore")
def step_attempt_create_spec(context: object) -> None:
"""Try to create a ResourceTypeSpec and capture any error."""
c = _ctx(context)
try:
c.created_spec = ResourceTypeSpec.from_config(c.test_config)
c.creation_error = None
except (ValueError, ValidationError) as exc:
c.created_spec = None
c.creation_error = exc
@then("the vcore creation should fail with a validation error")
def step_creation_should_fail(context: object) -> None:
assert _ctx(context).creation_error is not None, (
"Expected a validation error but creation succeeded"
)
@then('the vcore error message should mention "{keyword}"')
def step_error_mentions(context: object, keyword: str) -> None:
err = _ctx(context).creation_error
assert err is not None, "No error captured"
assert keyword.lower() in str(err).lower(), (
f"Error message does not mention '{keyword}': {err}"
)
@then("the vcore creation should succeed")
def step_creation_should_succeed(context: object) -> None:
c = _ctx(context)
assert c.creation_error is None, (
f"Expected success but got error: {c.creation_error}"
)
assert c.created_spec is not None
@then("the vcore created spec equivalence criteria should be empty")
def step_created_spec_empty_criteria(context: object) -> None:
spec = _ctx(context).created_spec
assert spec is not None
assert spec.equivalence is not None
assert spec.equivalence["criteria"] == [], (
f"Expected empty criteria, got {spec.equivalence['criteria']}"
)
@then("the vcore equivalence criteria count should be {count:d}")
def step_equiv_criteria_count(context: object, count: int) -> None:
schema = _ctx(context).schema
actual = len(schema.equivalence["criteria"])
assert actual == count, f"Expected {count} criteria, got {actual}"
@then("the vcore domain model should have equivalence")
def step_domain_has_equivalence(context: object) -> None:
domain = _ctx(context).domain
assert domain.equivalence is not None, (
f"Domain model {domain.name} has no equivalence"
)
@then("the YAML and registry should have matching fields for vcore")
def step_yaml_registry_match(context: object) -> None:
"""Assert the YAML-loaded domain model matches the registry dict."""
domain = _ctx(context).domain
found: dict[str, Any] = _ctx(context).found_builtin
assert domain.name == found["name"], (
f"name mismatch: {domain.name} != {found['name']}"
)
assert domain.resource_kind.value == found["resource_kind"], (
f"resource_kind mismatch: {domain.resource_kind} != {found['resource_kind']}"
)
assert domain.sandbox_strategy.value == found["sandbox_strategy"], (
"sandbox_strategy mismatch"
)
assert domain.user_addable == found["user_addable"], "user_addable mismatch"
assert sorted(domain.child_types) == sorted(found["child_types"]), (
f"child_types mismatch: {domain.child_types} != {found['child_types']}"
)
yaml_criteria = domain.equivalence["criteria"] if domain.equivalence else []
reg_criteria = found.get("equivalence", {}).get("criteria", [])
assert yaml_criteria == reg_criteria, (
f"equivalence criteria mismatch: {yaml_criteria} != {reg_criteria}"
)
# ── Given/When/Then: user-add rejection ──────────────────
@given("a bootstrapped resource registry for vcore")
def step_bootstrapped_registry(context: object) -> None:
"""Create a ResourceRegistryService and bootstrap built-in types."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.infrastructure.database.models import Base
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine)
service = ResourceRegistryService(session_factory=factory)
service.bootstrap_builtin_types()
_ctx(context).registry_service = service
@when('I attempt to register a resource of virtual type "{type_name}" for vcore')
def step_attempt_register_virtual(context: object, type_name: str) -> None:
"""Try to register a resource instance of a virtual type."""
c = _ctx(context)
try:
c.registry_service.register_resource(type_name, name=f"test-{type_name}")
c.register_error = None
except Exception as exc:
c.register_error = exc
@then('the vcore resource registration should fail with "{message}"')
def step_register_should_fail(context: object, message: str) -> None:
err = _ctx(context).register_error
assert err is not None, "Expected registration error but succeeded"
assert message.lower() in str(err).lower(), (
f"Error does not mention '{message}': {err}"
)
+145
View File
@@ -0,0 +1,145 @@
*** Settings ***
Documentation Virtual Core Resource Type Integration Tests
Library Process
Library OperatingSystem
*** Variables ***
${PYTHON} python
${FILE_YAML} ${CURDIR}/../examples/resource-types/file.yaml
${DIR_YAML} ${CURDIR}/../examples/resource-types/directory.yaml
${COMMIT_YAML} ${CURDIR}/../examples/resource-types/commit.yaml
${BRANCH_YAML} ${CURDIR}/../examples/resource-types/branch.yaml
${TAG_YAML} ${CURDIR}/../examples/resource-types/tag.yaml
${TREE_YAML} ${CURDIR}/../examples/resource-types/tree.yaml
*** Test Cases ***
Load All Virtual Core YAMLs And Validate Schema
[Documentation] Load all 6 virtual core YAMLs via schema loader and assert key fields
${script}= Catenate SEPARATOR=\n
... from cleveragents.resource.schema import ResourceTypeConfigSchema
... VIRTUAL_TYPES = {
... ${SPACE}${SPACE}${SPACE}${SPACE}"file": "${FILE_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"directory": "${DIR_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"commit": "${COMMIT_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"branch": "${BRANCH_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"tag": "${TAG_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"tree": "${TREE_YAML}",
... }
... for name, path in VIRTUAL_TYPES.items():
... ${SPACE}${SPACE}${SPACE}${SPACE}schema = ResourceTypeConfigSchema.from_yaml_file(path)
... ${SPACE}${SPACE}${SPACE}${SPACE}assert schema.name == name, f"name mismatch: {schema.name} != {name}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert schema.resource_kind == "virtual", f"kind: {schema.resource_kind}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert schema.sandbox_strategy == "none", f"strategy: {schema.sandbox_strategy}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert schema.user_addable is False, f"addable: {schema.user_addable}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert schema.built_in is True, f"built_in: {schema.built_in}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert schema.equivalence is not None, f"equivalence missing for {name}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert len(schema.cli_args) == 0, f"cli_args not empty for {name}"
... print("All 6 virtual core YAMLs validated successfully")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 Virtual core YAML load failed: ${result.stderr}
Should Contain ${result.stdout} All 6 virtual core YAMLs validated successfully
Load Virtual Core YAMLs As Domain Models
[Documentation] Load all virtual core YAMLs as ResourceTypeSpec domain models
${script}= Catenate SEPARATOR=\n
... import yaml
... from cleveragents.domain.models.core.resource_type import ResourceTypeSpec, ResourceKind
... PATHS = {
... ${SPACE}${SPACE}${SPACE}${SPACE}"file": "${FILE_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"directory": "${DIR_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"commit": "${COMMIT_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"branch": "${BRANCH_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"tag": "${TAG_YAML}",
... ${SPACE}${SPACE}${SPACE}${SPACE}"tree": "${TREE_YAML}",
... }
... for name, path in PATHS.items():
... ${SPACE}${SPACE}${SPACE}${SPACE}with open(path) as f:
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}config = yaml.safe_load(f)
... ${SPACE}${SPACE}${SPACE}${SPACE}spec = ResourceTypeSpec.from_config(config)
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.name == name, f"name: {spec.name}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.resource_kind == ResourceKind.VIRTUAL
... ${SPACE}${SPACE}${SPACE}${SPACE}cli_dict = spec.as_cli_dict()
... ${SPACE}${SPACE}${SPACE}${SPACE}assert "equivalence" in cli_dict, f"no equivalence in cli_dict for {name}"
... print("All 6 virtual core domain models validated successfully")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 Domain model load failed: ${result.stderr}
Should Contain ${result.stdout} All 6 virtual core domain models validated successfully
Verify Virtual Types In Bootstrap Registration
[Documentation] Verify all 6 virtual core types are in BUILTIN_TYPES
${script}= Catenate SEPARATOR=\n
... from cleveragents.application.services._resource_registry_data import BUILTIN_TYPES
... names = [d["name"] for d in BUILTIN_TYPES]
... expected = ["file", "directory", "commit", "branch", "tag", "tree"]
... for vt in expected:
... ${SPACE}${SPACE}${SPACE}${SPACE}assert vt in names, f"{vt} not in builtin types: {names}"
... ${SPACE}${SPACE}${SPACE}${SPACE}entry = next(d for d in BUILTIN_TYPES if d["name"] == vt)
... ${SPACE}${SPACE}${SPACE}${SPACE}assert entry["resource_kind"] == "virtual", f"{vt} kind: {entry['resource_kind']}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert entry["user_addable"] is False, f"{vt} addable: {entry['user_addable']}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert entry["sandbox_strategy"] == "none", f"{vt} strategy: {entry['sandbox_strategy']}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert entry.get("equivalence") is not None, f"{vt} missing equivalence"
... print("All 6 virtual core types present in bootstrap registration")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 Bootstrap verification failed: ${result.stderr}
Should Contain ${result.stdout} All 6 virtual core types present in bootstrap registration
Verify BUILTIN_NAMES Includes Virtual Types
[Documentation] Verify ResourceTypeSpec.BUILTIN_NAMES includes virtual core types
${script}= Catenate SEPARATOR=\n
... from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
... expected = {"file", "directory", "commit", "branch", "tag", "tree"}
... for name in expected:
... ${SPACE}${SPACE}${SPACE}${SPACE}assert name in ResourceTypeSpec.BUILTIN_NAMES, f"{name} not in BUILTIN_NAMES"
... print("All virtual core types in BUILTIN_NAMES")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 BUILTIN_NAMES check failed: ${result.stderr}
Should Contain ${result.stdout} All virtual core types in BUILTIN_NAMES
Virtual Types Appear In CLI Resource Type List
[Documentation] Verify virtual core types are listed via the CLI resource type list path
${script}= Catenate SEPARATOR=\n
... import json
... from sqlalchemy import create_engine
... from sqlalchemy.orm import sessionmaker
... from cleveragents.infrastructure.database.models import Base
... from cleveragents.application.services.resource_registry_service import ResourceRegistryService
... engine = create_engine("sqlite:///:memory:")
... Base.metadata.create_all(engine)
... service = ResourceRegistryService(session_factory=sessionmaker(bind=engine))
... service.bootstrap_builtin_types()
... types = service.list_types()
... type_names = [t.name for t in types]
... expected = ["file", "directory", "commit", "branch", "tag", "tree"]
... for vt in expected:
... ${SPACE}${SPACE}${SPACE}${SPACE}assert vt in type_names, f"{vt} not in listed types: {type_names}"
... ${SPACE}${SPACE}${SPACE}${SPACE}spec = next(t for t in types if t.name == vt)
... ${SPACE}${SPACE}${SPACE}${SPACE}assert str(spec.resource_kind) == "virtual", f"{vt} kind: {spec.resource_kind}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.user_addable is False, f"{vt} user_addable: {spec.user_addable}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.equivalence is not None, f"{vt} missing equivalence"
... print(f"CLI list path: all 6 virtual types present and correctly typed")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 CLI list path verification failed: ${result.stderr}
Should Contain ${result.stdout} all 6 virtual types present
Virtual Types Are Rejected By Register Resource
[Documentation] Verify virtual types cannot be user-added via register_resource
${script}= Catenate SEPARATOR=\n
... from sqlalchemy import create_engine
... from sqlalchemy.orm import sessionmaker
... from cleveragents.infrastructure.database.models import Base
... from cleveragents.application.services.resource_registry_service import ResourceRegistryService
... engine = create_engine("sqlite:///:memory:")
... Base.metadata.create_all(engine)
... service = ResourceRegistryService(session_factory=sessionmaker(bind=engine))
... service.bootstrap_builtin_types()
... try:
... ${SPACE}${SPACE}${SPACE}${SPACE}service.register_resource("file", name="test-file")
... ${SPACE}${SPACE}${SPACE}${SPACE}raise AssertionError("Expected ValidationError but register_resource succeeded")
... except Exception as e:
... ${SPACE}${SPACE}${SPACE}${SPACE}assert "not user-addable" in str(e).lower(), f"Wrong error: {e}"
... ${SPACE}${SPACE}${SPACE}${SPACE}print(f"Correctly rejected: {e}")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 Register rejection test failed: ${result.stderr}
Should Contain ${result.stdout} Correctly rejected
*** Keywords ***
@@ -17,6 +17,9 @@ from collections import deque
from datetime import UTC, datetime
from typing import Any
from cleveragents.application.services._resource_registry_virtual import (
VIRTUAL_CORE_TYPES,
)
from cleveragents.domain.models.core.resource import PhysVirt, Resource
from cleveragents.domain.models.core.resource_type import (
ResourceKind,
1
@@ -269,6 +272,8 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [
},
},
*DATABASE_TYPE_DEFS,
# Virtual core resource types -- see _resource_registry_virtual.py (#329)
*VIRTUAL_CORE_TYPES,
]
@@ -95,7 +95,8 @@ class ResourceInstanceMixin:
Raises:
NotFoundError: If the resource type does not exist.
ValidationError: If the resource name already exists.
ValidationError: If the resource type is not user-addable,
or if the resource name already exists.
"""
session = self._session()
try:
@@ -108,6 +109,12 @@ class ResourceInstanceMixin:
resource_id=type_name,
)
if not type_row.user_addable:
raise ValidationError(
f"Resource type '{type_name}' is not user-addable. "
"Only types with user_addable=true can be instantiated.",
)
resource_kind_str: str = str(type_row.resource_kind)
resource_id = str(ULID())
@@ -0,0 +1,205 @@
"""Virtual core resource type definitions for the Resource Registry.
Contains the built-in virtual type entries (file, directory, commit,
branch, tag, tree) that are appended to ``BUILTIN_TYPES`` in
``_resource_registry_data``. Extracted to keep each module under the
500-line CONTRIBUTING limit.
.. note::
This is an **internal** module (``_``-prefixed). External code
should import ``BUILTIN_TYPES`` from ``_resource_registry_data``
instead.
"""
from __future__ import annotations
from typing import Any
__all__ = ["VIRTUAL_CORE_TYPES"]
# === Virtual Core Resource Types (#329) ===
#
# Virtual types represent abstract identities that link equivalent
# physical resources across layers and repositories. They have no
# handler, no sandbox strategy, and are never user-addable.
#
# NOTE: Some ``child_types`` entries (e.g. ``git-tree-entry``,
# ``git-tree``, ``git-commit``, ``git-branch``, ``git-tag``) are
# forward references to deferred physical types introduced by PR #662
# (issue #330). This is safe because the registry stores child_types
# as opaque strings with no referential-integrity check at bootstrap
# time -- see ``bootstrap_builtin_types()`` and ``spec_to_db()``.
VIRTUAL_CORE_TYPES: list[dict[str, Any]] = [
{
"name": "file",
"description": (
"Cross-layer file identity linking equivalent physical files "
"by content hash, filename, and permissions."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["fs-file", "git-tree-entry"],
"handler": None,
Outdated
Review

P0: child_types here reference git-tree-entry and git-tree (lines ~48, ~70, ~163, ~192) which don't exist on master. They are introduced by PR #662. If this PR merges first, these are forward references. Confirm that the registry tolerates dangling child_type names at bootstrap, or establish #662 as a merge prerequisite.

P0: `child_types` here reference `git-tree-entry` and `git-tree` (lines ~48, ~70, ~163, ~192) which don't exist on master. They are introduced by PR #662. If this PR merges first, these are forward references. Confirm that the registry tolerates dangling child_type names at bootstrap, or establish #662 as a merge prerequisite.
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["content_hash", "filename", "permissions"],
"description": (
"Two physical resources are equivalent when they share "
"the same content bytes (SHA-256), filename, and permissions"
),
},
},
{
"name": "directory",
"description": (
"Cross-layer directory identity linking equivalent physical "
"directories by recursive content."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["fs-directory", "git-tree"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
# Spec identity table lists only merkle_hash, but name and permissions
# are added for stricter matching (same recursive content in a
# differently-named or differently-permissioned directory is not
# considered equivalent). See specification.md ~line 24623.
"equivalence": {
"criteria": ["merkle_hash", "name", "permissions"],
"description": (
"Two physical resources are equivalent when their "
"recursive contents produce the same Merkle hash"
),
},
},
{
"name": "commit",
"description": (
"Cross-repo commit identity linking commits with "
"the same hash across repositories."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["git-commit"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["commit_hash"],
"description": (
"Two physical commits are equivalent when they "
"share the same commit hash"
),
},
},
{
"name": "branch",
"description": (
"Cross-repo branch identity linking branches with "
"the same name and HEAD commit hash."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["git-branch"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["name", "head_commit_hash"],
"description": (
"Two physical branches are equivalent when they share "
"the same branch name and HEAD commit hash"
),
},
},
{
"name": "tag",
"description": (
"Cross-repo tag identity linking tags with the same name and target object."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["git-tag"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["name", "target_sha"],
"description": (
"Two physical tags are equivalent when they share "
"the same tag name and target object"
),
},
},
{
"name": "tree",
"description": (
"Cross-repo/cross-commit tree identity linking trees with the same hash."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["git-tree"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["tree_hash"],
"description": (
"Two physical tree objects are equivalent when they "
"share the same tree hash"
),
},
},
]
@@ -16,6 +16,12 @@ development workflows. Built-in types are **unnamespaced** (no
|-------------------|----------|------------------|--------------------------------|
| ``git-checkout`` | physical | ``git_worktree`` | Git repository checkout |
| ``fs-directory`` | physical | ``copy_on_write``| Filesystem directory |
| ``file`` | virtual | ``none`` | Cross-layer file identity |
| ``directory`` | virtual | ``none`` | Cross-layer directory identity |
| ``commit`` | virtual | ``none`` | Cross-repo commit identity |
| ``branch`` | virtual | ``none`` | Cross-repo branch identity |
| ``tag`` | virtual | ``none`` | Cross-repo tag identity |
| ``tree`` | virtual | ``none`` | Cross-repo tree identity |
Custom types must follow the ``namespace/name`` pattern and are registered
via YAML configuration files.
+10
View File
@@ -138,6 +138,7 @@ def _resource_type_dict(spec: Any) -> dict[str, object]:
"child_types": spec.child_types,
"handler": spec.handler,
"capabilities": spec.capabilities,
"equivalence": getattr(spec, "equivalence", None),
}
return result
@@ -434,6 +435,15 @@ def _print_type_panel(spec: Any) -> None:
f"[bold]Child Types:[/bold] {', '.join(spec.child_types) or '(none)'}"
)
equivalence = getattr(spec, "equivalence", None)
if equivalence is not None:
criteria = equivalence.get("criteria", [])
equiv_desc = equivalence.get("description", "")
details += (
f"\n[bold]Equivalence Criteria:[/bold] {', '.join(criteria)}"
f"\n[bold]Equivalence Description:[/bold] {equiv_desc}"
)
console.print(Panel(details, title="Resource Type", expand=False))
@@ -4,11 +4,16 @@ Resource types define schema-level constraints for resource categories:
CLI arguments, classification (physical/virtual), DAG constraints,
sandbox strategy, capabilities, auto-discovery, and handler references.
Built-in types (``git-checkout``, ``fs-directory``, etc.) use simple
unnamespaced names. Custom types follow ``namespace/name`` format.
Built-in types use simple unnamespaced names (e.g. ``git-checkout``).
Custom types follow ``namespace/name`` (e.g. ``myorg/custom-db``).
Virtual types **must** include an ``equivalence`` configuration.
Based on ``docs/specification.md``, ``implementation_plan.md`` Task
B0.type-model, and ADR-004: Pydantic v2 Validation.
See Also:
- ``docs/reference/resource_types_builtin.md`` -- full built-in type catalogue
- [Resource][cleveragents.domain.models.core.resource.Resource]
"""
from __future__ import annotations
1
@@ -230,6 +235,13 @@ class ResourceTypeSpec(BaseModel):
# -- GCP / Azure (flat, hierarchy pending) --
"gcp",
"azure",
# Virtual core resource types (#329)
"branch",
"commit",
"directory",
"file",
"tag",
"tree",
}
)
@@ -368,12 +380,24 @@ class ResourceTypeSpec(BaseModel):
"Only built-in types can be unnamespaced."
)
# Virtual types require equivalence rules
if self.resource_kind == ResourceKind.VIRTUAL and self.equivalence is None:
raise ValueError(
f"Virtual resource type '{self.name}' requires an "
"'equivalence' configuration for deduplication."
)
# Virtual types require equivalence rules with non-empty criteria
if self.resource_kind == ResourceKind.VIRTUAL:
if self.equivalence is None:
raise ValueError(
f"Virtual resource type '{self.name}' requires an "
"'equivalence' configuration for deduplication."
)
if "criteria" not in self.equivalence:
raise ValueError(
f"Virtual resource type '{self.name}' requires "
"'equivalence.criteria' key for identity matching."
)
criteria = self.equivalence["criteria"]
if not isinstance(criteria, list) or len(criteria) == 0:
raise ValueError(
f"Virtual resource type '{self.name}' requires non-empty "
"'equivalence.criteria' list for identity matching."
)
# ADR-042 rule 3: no cycles (self-inheritance)
if self.inherits is not None and self.inherits == self.name:
@@ -486,7 +510,10 @@ class ResourceTypeSpec(BaseModel):
if self.auto_discovery is not None:
result["auto_discovery"] = dict(self.auto_discovery)
if self.equivalence is not None:
result["equivalence"] = dict(self.equivalence)
result["equivalence"] = {
k: list(v) if isinstance(v, list) else v
for k, v in self.equivalence.items()
}
if self.handler is not None:
result["handler"] = self.handler