feat(resource): add virtual core resource types #661
@@ -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()
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
|
||||
"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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user
P0:
child_typeshere referencegit-tree-entryandgit-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.