feat(resource): add deferred virtual resource types #663

Merged
hamza.khyari merged 1 commits from feature/post-resource-types-virtual into master 2026-03-18 02:40:32 +00:00
16 changed files with 1534 additions and 16 deletions
+7 -1
View File
@@ -2,6 +2,12 @@
## Unreleased
- Added built-in deferred virtual resource types: `remote`, `submodule`, and
`symlink` with equivalence metadata rules for cross-repo and cross-layer
identity tracking. Registry bootstrap includes deferred virtual types but
hides them from `resource add` scaffolding (user_addable: false). Includes
YAML configurations, Behave BDD tests (47 scenarios), Robot Framework
integration tests, ASV benchmarks, and reference documentation. (#331)
- Fixed `plan execute` CLI failing with "Plan is not in an executable state
(current: strategize/queued)" after strategize completed successfully.
Root cause: `_get_plan_executor()` created a second `PlanLifecycleService`
@@ -174,7 +180,7 @@
insertions. Includes OWL/Turtle ontology files, ProvenanceInfo model with 2
required fields (source_resource, source_path) and 3 defaulted fields
(source_range, valid_from, is_current), build_detail_level_map/resolve_detail_level
utilities, and full Behave BDD tests (78 scenarios, 200 steps). (#576)
utilities, and full Behave BDD tests (78 scenarios, 200 steps). (#576)
- Added `RepoIndexingService` for repository file indexing with incremental
refresh, extension-based language detection, SHA-256 content hashing, and
token estimation. Supports policy enforcement via include/exclude globs,
@@ -0,0 +1,116 @@
"""ASV benchmarks for deferred virtual resource type loading (#331).
Benchmarks cover YAML schema loading, domain model creation via
``ResourceTypeSpec.from_config``, and ``as_cli_dict`` rendering for the
three deferred virtual types: remote, submodule, symlink.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
_EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" / "resource-types"
_REMOTE_YAML = _EXAMPLES_DIR / "remote.yaml"
_SUBMODULE_YAML = _EXAMPLES_DIR / "submodule.yaml"
_SYMLINK_YAML = _EXAMPLES_DIR / "symlink.yaml"
class TimeDeferredVirtualSchemaLoad:
"""Benchmark deferred virtual resource type YAML schema loading."""
def setup(self) -> None:
"""Set up schema class and file paths."""
from cleveragents.resource.schema import ResourceTypeConfigSchema
self.schema_cls = ResourceTypeConfigSchema
self.remote_path = _REMOTE_YAML
self.submodule_path = _SUBMODULE_YAML
self.symlink_path = _SYMLINK_YAML
def time_load_remote_schema(self) -> None:
"""Benchmark loading remote.yaml via schema loader."""
self.schema_cls.from_yaml_file(self.remote_path)
def time_load_submodule_schema(self) -> None:
"""Benchmark loading submodule.yaml via schema loader."""
self.schema_cls.from_yaml_file(self.submodule_path)
def time_load_symlink_schema(self) -> None:
"""Benchmark loading symlink.yaml via schema loader."""
self.schema_cls.from_yaml_file(self.symlink_path)
def time_load_all_deferred_virtual_schemas(self) -> None:
"""Benchmark loading all three deferred virtual type schemas."""
self.schema_cls.from_yaml_file(self.remote_path)
self.schema_cls.from_yaml_file(self.submodule_path)
self.schema_cls.from_yaml_file(self.symlink_path)
class TimeDeferredVirtualDomainModelLoad:
"""Benchmark deferred virtual resource type domain model creation."""
def setup(self) -> None:
"""Set up ResourceTypeSpec and parsed configs."""
import yaml
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
self.spec_cls = ResourceTypeSpec
with open(_REMOTE_YAML) as f:
self.remote_config: dict[str, Any] = yaml.safe_load(f)
with open(_SUBMODULE_YAML) as f:
self.submodule_config: dict[str, Any] = yaml.safe_load(f)
with open(_SYMLINK_YAML) as f:
self.symlink_config: dict[str, Any] = yaml.safe_load(f)
def time_remote_from_config(self) -> None:
"""Benchmark creating remote ResourceTypeSpec from config."""
self.spec_cls.from_config(self.remote_config)
def time_submodule_from_config(self) -> None:
"""Benchmark creating submodule ResourceTypeSpec from config."""
self.spec_cls.from_config(self.submodule_config)
def time_symlink_from_config(self) -> None:
"""Benchmark creating symlink ResourceTypeSpec from config."""
self.spec_cls.from_config(self.symlink_config)
def time_all_deferred_virtual_from_config(self) -> None:
"""Benchmark creating all three deferred virtual specs."""
self.spec_cls.from_config(self.remote_config)
self.spec_cls.from_config(self.submodule_config)
self.spec_cls.from_config(self.symlink_config)
class TimeDeferredVirtualCliDict:
"""Benchmark as_cli_dict rendering for deferred virtual types."""
def setup(self) -> None:
"""Set up pre-built ResourceTypeSpec instances."""
import yaml
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
with open(_REMOTE_YAML) as f:
remote_cfg: dict[str, Any] = yaml.safe_load(f)
with open(_SUBMODULE_YAML) as f:
submodule_cfg: dict[str, Any] = yaml.safe_load(f)
with open(_SYMLINK_YAML) as f:
symlink_cfg: dict[str, Any] = yaml.safe_load(f)
self.remote_spec = ResourceTypeSpec.from_config(remote_cfg)
self.submodule_spec = ResourceTypeSpec.from_config(submodule_cfg)
self.symlink_spec = ResourceTypeSpec.from_config(symlink_cfg)
def time_remote_cli_dict(self) -> None:
"""Benchmark remote as_cli_dict rendering."""
self.remote_spec.as_cli_dict()
def time_submodule_cli_dict(self) -> None:
"""Benchmark submodule as_cli_dict rendering."""
self.submodule_spec.as_cli_dict()
def time_symlink_cli_dict(self) -> None:
"""Benchmark symlink as_cli_dict rendering."""
self.symlink_spec.as_cli_dict()
+125
View File
@@ -296,3 +296,128 @@ across different commits or repos that have the same tree hash.
| Criterion | Description |
|---|---|
| `tree_hash` | Git tree object SHA |
## Deferred Virtual Resource Types
Virtual resource types represent abstract identities that link equivalent
physical resources across repositories and layers. Virtual types are never
user-addable, have no sandbox strategy, and are created automatically by
the system when equivalent physical resources are detected.
Based on `docs/specification.md` ~line 23705.
---
### remote
Cross-repo remote identity. Links `git-remote` resources across different
repos that point to the same URL.
| Field | Value |
|---|---|
| **resource_kind** | virtual |
| **sandbox_strategy** | none |
| **user_addable** | false |
| **handler** | none |
#### CLI Arguments
None (system-managed only).
#### Parent / Child Types
- **Parent types**: none
- **Child types**: `git-remote`
#### Equivalence Metadata
| Field | Value |
|---|---|
| **criteria** | `url` |
| **description** | Two git-remote resources share a remote parent when they point to the same normalized URL. |
#### Capabilities
| Capability | Enabled |
|---|---|
| read | no |
| write | no |
| sandbox | no |
| checkpoint | no |
---
### submodule
Cross-repo submodule identity. Links `git-submodule` resources across
different repos with the same submodule URL and path.
| Field | Value |
|---|---|
| **resource_kind** | virtual |
| **sandbox_strategy** | none |
| **user_addable** | false |
| **handler** | none |
#### CLI Arguments
None (system-managed only).
#### Parent / Child Types
- **Parent types**: none
- **Child types**: `git-submodule`
#### Equivalence Metadata
| Field | Value |
|---|---|
| **criteria** | `url`, `path` |
| **description** | Two git-submodule resources share a submodule parent when they have the same submodule URL and path. |
#### Capabilities
| Capability | Enabled |
|---|---|
| read | no |
| write | no |
| sandbox | no |
| checkpoint | no |
---
### symlink
Cross-layer symlink identity. Links `fs-symlink` and `git-tree-entry`
(mode 120000) resources with the same name and target.
| Field | Value |
|---|---|
| **resource_kind** | virtual |
| **sandbox_strategy** | none |
| **user_addable** | false |
| **handler** | none |
#### CLI Arguments
None (system-managed only).
#### Parent / Child Types
- **Parent types**: none
- **Child types**: `fs-symlink`, `git-tree-entry`
#### Equivalence Metadata
| Field | Value |
|---|---|
| **criteria** | `name`, `target_path` |
| **description** | Two physical symlink resources share a symlink parent when they have the same name and target path. |
#### Capabilities
| Capability | Enabled |
|---|---|
| read | no |
| write | no |
| sandbox | no |
| checkpoint | no |
+20
View File
@@ -0,0 +1,20 @@
# Spec reference: docs/specification.md ~lines 23705-23719, 24619-24631
schema_version: "1.0"
name: remote
description: "Cross-repo remote identity. Links git-remote resources across different repos that point to the same URL."
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["git-remote"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria: ["url"]
description: "Two git-remote resources share a remote parent when they point to the same normalized URL."
handler: null
+20
View File
@@ -0,0 +1,20 @@
# Spec reference: docs/specification.md ~lines 23705-23719, 24619-24631
schema_version: "1.0"
name: submodule
description: "Cross-repo submodule identity. Links git-submodule resources across different repos with the same submodule URL and path."
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["git-submodule"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria: ["url", "path"]
description: "Two git-submodule resources share a submodule parent when they have the same submodule URL and path."
handler: null
+20
View File
@@ -0,0 +1,20 @@
# Spec reference: docs/specification.md ~lines 23705-23719, 24619-24631
schema_version: "1.0"
name: symlink
description: "Cross-layer symlink identity. Links fs-symlink and git-tree-entry (mode 120000) resources with the same name and target."
resource_kind: virtual
sandbox_strategy: none
user_addable: false
built_in: true
cli_args: []
parent_types: []
child_types: ["fs-symlink", "git-tree-entry"]
capabilities:
read: false
write: false
sandbox: false
checkpoint: false
equivalence:
criteria: ["name", "target_path"]
description: "Two physical symlink resources share a symlink parent when they have the same name and target path."
handler: null
@@ -0,0 +1,344 @@
Feature: Deferred Virtual Resource Types
As a CleverAgents developer
I want built-in virtual resource types (remote, submodule, symlink)
So that equivalence tracking can link physical resources across repos and layers
# Existence and loading
Scenario: remote YAML validates for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should be valid
And the virt_deferred schema name should be "remote"
Scenario: submodule YAML validates for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should be valid
And the virt_deferred schema name should be "submodule"
Scenario: symlink YAML validates for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should be valid
And the virt_deferred schema name should be "symlink"
# ── Resource kind ─────────────────────────────────────────
Scenario: remote is a virtual resource type for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema resource_kind should be "virtual"
Scenario: submodule is a virtual resource type for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema resource_kind should be "virtual"
Scenario: symlink is a virtual resource type for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema resource_kind should be "virtual"
# ── Not user-addable ──────────────────────────────────────
Scenario: remote is not user-addable for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema user_addable should be false
Scenario: submodule is not user-addable for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema user_addable should be false
Scenario: symlink is not user-addable for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema user_addable should be false
# ── Built-in and unnamespaced ─────────────────────────────
Scenario: remote is built-in and unnamespaced for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema built_in should be true
And the virt_deferred schema name should not contain "/"
Scenario: submodule is built-in and unnamespaced for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema built_in should be true
And the virt_deferred schema name should not contain "/"
Scenario: symlink is built-in and unnamespaced for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema built_in should be true
And the virt_deferred schema name should not contain "/"
# ── Sandbox strategy ──────────────────────────────────────
Scenario: remote has no sandbox strategy for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema sandbox_strategy should be "none"
Scenario: submodule has no sandbox strategy for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema sandbox_strategy should be "none"
Scenario: symlink has no sandbox strategy for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema sandbox_strategy should be "none"
# ── Capabilities (all false for virtual) ──────────────────
Scenario: remote has all capabilities false for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred capabilities should have read false
And the virt_deferred capabilities should have write false
And the virt_deferred capabilities should have sandbox false
And the virt_deferred capabilities should have checkpoint false
Scenario: submodule has all capabilities false for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred capabilities should have read false
And the virt_deferred capabilities should have write false
And the virt_deferred capabilities should have sandbox false
And the virt_deferred capabilities should have checkpoint false
Scenario: symlink has all capabilities false for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred capabilities should have read false
And the virt_deferred capabilities should have write false
And the virt_deferred capabilities should have sandbox false
And the virt_deferred capabilities should have checkpoint false
# ── Equivalence metadata ──────────────────────────────────
Scenario: remote has equivalence metadata for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema equivalence should be present
And the virt_deferred equivalence criteria should contain "url"
Scenario: submodule has equivalence metadata for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema equivalence should be present
And the virt_deferred equivalence criteria should contain "url"
And the virt_deferred equivalence criteria should contain "path"
Scenario: symlink has equivalence metadata for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema equivalence should be present
And the virt_deferred equivalence criteria should contain "name"
And the virt_deferred equivalence criteria should contain "target_path"
# ── Domain model loading ──────────────────────────────────
Scenario: remote YAML loads as domain model for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML as a ResourceTypeSpec for virt_deferred
Then the virt_deferred domain model should be valid
And the virt_deferred domain model name should be "remote"
And the virt_deferred domain model resource_kind should be "virtual"
Scenario: submodule YAML loads as domain model for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML as a ResourceTypeSpec for virt_deferred
Then the virt_deferred domain model should be valid
And the virt_deferred domain model name should be "submodule"
And the virt_deferred domain model resource_kind should be "virtual"
Scenario: symlink YAML loads as domain model for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML as a ResourceTypeSpec for virt_deferred
Then the virt_deferred domain model should be valid
And the virt_deferred domain model name should be "symlink"
And the virt_deferred domain model resource_kind should be "virtual"
# ── Child types ───────────────────────────────────────────
Scenario: remote allows git-remote children for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema child_types should contain "git-remote"
Scenario: submodule allows git-submodule children for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema child_types should contain "git-submodule"
Scenario: symlink allows fs-symlink children for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema child_types should contain "fs-symlink"
Scenario: symlink allows git-tree-entry children for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema child_types should contain "git-tree-entry"
# ── No parent types ───────────────────────────────────────
Scenario: remote has no parent types for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should have empty parent_types
Scenario: submodule has no parent types for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should have empty parent_types
Scenario: symlink has no parent types for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should have empty parent_types
# ── No CLI args ───────────────────────────────────────────
Scenario: remote has no cli_args for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should have 0 cli_args
Scenario: submodule has no cli_args for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should have 0 cli_args
Scenario: symlink has no cli_args for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema should have 0 cli_args
# ── No handler ────────────────────────────────────────────
Scenario: remote has no handler for virt_deferred
Given the built-in remote YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema handler should be none
Scenario: submodule has no handler for virt_deferred
Given the built-in submodule YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema handler should be none
Scenario: symlink has no handler for virt_deferred
Given the built-in symlink YAML file for virt_deferred
When I load the YAML via schema for virt_deferred
Then the virt_deferred schema handler should be none
# ── Registry bootstrap includes virtual types ─────────────
Scenario: builtin types list includes deferred virtual types for virt_deferred
Given the registry builtin types list for virt_deferred
Then the registry should contain type "remote" for virt_deferred
And the registry should contain type "submodule" for virt_deferred
And the registry should contain type "symlink" for virt_deferred
Scenario: deferred virtual types in registry are not user-addable for virt_deferred
Given the registry builtin types list for virt_deferred
Then the registry type "remote" should not be user_addable for virt_deferred
And the registry type "submodule" should not be user_addable for virt_deferred
And the registry type "symlink" should not be user_addable for virt_deferred
Scenario: deferred virtual types in registry are virtual for virt_deferred
Given the registry builtin types list for virt_deferred
Then the registry type "remote" should be virtual for virt_deferred
And the registry type "submodule" should be virtual for virt_deferred
And the registry type "symlink" should be virtual for virt_deferred
Scenario: deferred virtual types in registry have equivalence for virt_deferred
Given the registry builtin types list for virt_deferred
Then the registry type "remote" should have equivalence for virt_deferred
And the registry type "submodule" should have equivalence for virt_deferred
And the registry type "symlink" should have equivalence for virt_deferred
# ── BUILTIN_NAMES includes virtual types ──────────────────
Scenario: BUILTIN_NAMES includes deferred virtual type names for virt_deferred
Given the ResourceTypeSpec BUILTIN_NAMES set for virt_deferred
Then BUILTIN_NAMES should contain "remote" for virt_deferred
And BUILTIN_NAMES should contain "submodule" for virt_deferred
And BUILTIN_NAMES should contain "symlink" for virt_deferred
# ── DB roundtrip via bootstrap ─────────────────────────────
Scenario: virtual types survive DB roundtrip after bootstrap for virt_deferred
Given a virt_deferred fresh in-memory resource registry with bootstrap
When I query the virt_deferred registry for type "remote"
Then the virt_deferred db type "remote" should exist
And the virt_deferred db type "remote" should have kind "virtual"
And the virt_deferred db type "remote" should not be user_addable
And the virt_deferred db type "remote" should have equivalence with criteria "url"
When I query the virt_deferred registry for type "submodule"
Then the virt_deferred db type "submodule" should exist
And the virt_deferred db type "submodule" should have kind "virtual"
And the virt_deferred db type "submodule" should not be user_addable
And the virt_deferred db type "submodule" should have equivalence with criteria "url,path"
When I query the virt_deferred registry for type "symlink"
Then the virt_deferred db type "symlink" should exist
And the virt_deferred db type "symlink" should have kind "virtual"
And the virt_deferred db type "symlink" should not be user_addable
And the virt_deferred db type "symlink" should have equivalence with criteria "name,target_path"
Scenario: virtual types appear in list_types after bootstrap for virt_deferred
Given a virt_deferred fresh in-memory resource registry with bootstrap
When I list all virt_deferred registry types
Then the virt_deferred type list should include "remote"
And the virt_deferred type list should include "submodule"
And the virt_deferred type list should include "symlink"
# ── Negative tests ─────────────────────────────────────────
Scenario: virtual type without equivalence fails validation for virt_deferred
When I create a virt_deferred virtual type config without equivalence
Then the virt_deferred config creation should raise a validation error
Scenario: from_config rejects missing name for virt_deferred
When I create a virt_deferred config without name
Then the virt_deferred config creation should raise a ValueError
Scenario: from_config rejects missing resource_kind for virt_deferred
When I create a virt_deferred config without resource_kind
Then the virt_deferred config creation should raise a ValueError
# ── Manual add rejection (F-001 / F-003) ───────────────────
Scenario: manual register_resource rejects remote as not user-addable
Given a virt_deferred fresh in-memory resource registry with bootstrap
When I attempt to manually register a resource of type "remote" for virt_deferred
Then the virt_deferred manual registration should be rejected
And the virt_deferred rejection message should mention "not user-addable"
Scenario: manual register_resource rejects submodule as not user-addable
Given a virt_deferred fresh in-memory resource registry with bootstrap
When I attempt to manually register a resource of type "submodule" for virt_deferred
Then the virt_deferred manual registration should be rejected
And the virt_deferred rejection message should mention "not user-addable"
Scenario: manual register_resource rejects symlink as not user-addable
Given a virt_deferred fresh in-memory resource registry with bootstrap
When I attempt to manually register a resource of type "symlink" for virt_deferred
Then the virt_deferred manual registration should be rejected
And the virt_deferred rejection message should mention "not user-addable"
# ── Equivalence criteria validation (F-004) ────────────────
Scenario: equivalence with non-string criteria element fails validation
When I create a virt_deferred virtual type config with criteria containing 123
Then the virt_deferred config creation should raise a validation error
Scenario: equivalence with empty-string criteria element fails validation
When I create a virt_deferred virtual type config with criteria containing ""
Then the virt_deferred config creation should raise a validation error
@@ -256,13 +256,20 @@ def step_spec_with_equivalence(context: Any) -> None:
"criteria": ["content_hash", "location"],
"description": "Hash-based equivalence comparison",
}
# Virtual types require equivalence, so this is consistent
# Virtual types require equivalence and all-false capabilities
context.cov_spec_eq = ResourceTypeSpec(
name="testns/virt-type",
description="Virtual type with equivalence",
resource_kind=ResourceKind.VIRTUAL,
sandbox_strategy=SandboxStrategy.NONE,
user_addable=False,
handler=None,
capabilities={
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
equivalence=context.equivalence_data,
built_in=False,
)
@@ -364,7 +371,7 @@ def step_db_row_with_equivalence(context: Any) -> None:
allowed_child_types_json=None,
auto_discover_json=None,
capabilities_json=json.dumps(
{"read": True, "write": True, "sandbox": True, "checkpoint": False}
{"read": False, "write": False, "sandbox": False, "checkpoint": False}
),
equivalence_json=json.dumps(context.eq_original),
source="test",
@@ -0,0 +1,543 @@
"""Step definitions for resource_type_deferred_virtual.feature.
Tests for deferred virtual resource types: remote, submodule, symlink (#331).
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
from behave import given, then, when
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
from cleveragents.resource.schema import ResourceTypeConfigSchema
_EXAMPLES_DIR = (
Path(__file__).resolve().parent.parent.parent / "examples" / "resource-types"
)
# ── Given steps ──────────────────────────────────────────────
@given("the built-in remote YAML file for virt_deferred")
def step_builtin_remote_yaml_virt_deferred(context: object) -> None:
"""Load the remote YAML file path."""
context.virt_deferred_yaml_path = _EXAMPLES_DIR / "remote.yaml" # type: ignore[attr-defined]
@given("the built-in submodule YAML file for virt_deferred")
def step_builtin_submodule_yaml_virt_deferred(context: object) -> None:
"""Load the submodule YAML file path."""
context.virt_deferred_yaml_path = _EXAMPLES_DIR / "submodule.yaml" # type: ignore[attr-defined]
@given("the built-in symlink YAML file for virt_deferred")
def step_builtin_symlink_yaml_virt_deferred(context: object) -> None:
"""Load the symlink YAML file path."""
context.virt_deferred_yaml_path = _EXAMPLES_DIR / "symlink.yaml" # type: ignore[attr-defined]
@given("the registry builtin types list for virt_deferred")
def step_registry_builtin_types_virt_deferred(context: object) -> None:
"""Load the BUILTIN_TYPES list from the registry data module."""
from cleveragents.application.services._resource_registry_data import (
BUILTIN_TYPES,
)
context.virt_deferred_builtin_types = { # type: ignore[attr-defined]
t["name"]: t for t in BUILTIN_TYPES
}
@given("the ResourceTypeSpec BUILTIN_NAMES set for virt_deferred")
def step_builtin_names_set_virt_deferred(context: object) -> None:
"""Load the BUILTIN_NAMES frozenset."""
context.virt_deferred_builtin_names = ResourceTypeSpec.BUILTIN_NAMES # type: ignore[attr-defined]
# ── When steps ───────────────────────────────────────────────
@when("I load the YAML via schema for virt_deferred")
def step_load_yaml_schema_virt_deferred(context: object) -> None:
"""Load the YAML file via the schema loader."""
path: Path = context.virt_deferred_yaml_path # type: ignore[attr-defined]
context.virt_deferred_schema = ResourceTypeConfigSchema.from_yaml_file(path) # type: ignore[attr-defined]
@when("I load the YAML as a ResourceTypeSpec for virt_deferred")
def step_load_yaml_domain_virt_deferred(context: object) -> None:
"""Load the YAML file as a domain model."""
path: Path = context.virt_deferred_yaml_path # type: ignore[attr-defined]
raw: dict[str, Any] = yaml.safe_load(path.read_text(encoding="utf-8"))
context.virt_deferred_domain = ResourceTypeSpec.from_config(raw) # type: ignore[attr-defined]
# ── Then steps: schema validation ────────────────────────────
@then("the virt_deferred schema should be valid")
def step_schema_valid_virt_deferred(context: object) -> None:
"""Assert the schema loaded successfully with required fields populated."""
schema = context.virt_deferred_schema # type: ignore[attr-defined]
assert schema is not None, "Schema should not be None"
assert hasattr(schema, "name") and schema.name, "Schema must have a non-empty name"
assert hasattr(schema, "resource_kind") and schema.resource_kind, (
"Schema must have a resource_kind"
)
assert hasattr(schema, "sandbox_strategy"), "Schema must have sandbox_strategy"
@then('the virt_deferred schema name should be "{name}"')
def step_schema_name_virt_deferred(context: object, name: str) -> None:
"""Assert the schema name matches."""
actual = context.virt_deferred_schema.name # type: ignore[attr-defined]
assert actual == name, f"Expected schema name '{name}', got '{actual}'"
@then('the virt_deferred schema resource_kind should be "{kind}"')
def step_schema_kind_virt_deferred(context: object, kind: str) -> None:
"""Assert the resource kind matches."""
actual = context.virt_deferred_schema.resource_kind # type: ignore[attr-defined]
assert actual == kind, f"Expected resource_kind '{kind}', got '{actual}'"
@then("the virt_deferred schema user_addable should be false")
def step_schema_not_user_addable_virt_deferred(context: object) -> None:
"""Assert the type is not user-addable."""
actual = context.virt_deferred_schema.user_addable # type: ignore[attr-defined]
assert actual is False, f"Expected user_addable False, got {actual}"
@then("the virt_deferred schema built_in should be true")
def step_schema_builtin_virt_deferred(context: object) -> None:
"""Assert the type is built-in."""
actual = context.virt_deferred_schema.built_in # type: ignore[attr-defined]
assert actual is True, f"Expected built_in True, got {actual}"
@then('the virt_deferred schema name should not contain "/"')
def step_schema_no_slash_virt_deferred(context: object) -> None:
"""Assert the name is unnamespaced."""
name = context.virt_deferred_schema.name # type: ignore[attr-defined]
assert "/" not in name, f"Built-in name '{name}' should not contain '/'"
@then('the virt_deferred schema sandbox_strategy should be "{strategy}"')
def step_schema_sandbox_virt_deferred(context: object, strategy: str) -> None:
"""Assert the sandbox strategy matches."""
actual = context.virt_deferred_schema.sandbox_strategy # type: ignore[attr-defined]
assert actual == strategy, f"Expected sandbox_strategy '{strategy}', got '{actual}'"
# ── Then steps: capabilities ─────────────────────────────────
@then("the virt_deferred capabilities should have read {value}")
def step_caps_read_virt_deferred(context: object, value: str) -> None:
"""Assert the read capability."""
expected = value.lower() == "true"
actual = context.virt_deferred_schema.capabilities["read"] # type: ignore[attr-defined]
assert actual == expected, f"Expected read={expected}, got {actual}"
@then("the virt_deferred capabilities should have write {value}")
def step_caps_write_virt_deferred(context: object, value: str) -> None:
"""Assert the write capability."""
expected = value.lower() == "true"
actual = context.virt_deferred_schema.capabilities["write"] # type: ignore[attr-defined]
assert actual == expected, f"Expected write={expected}, got {actual}"
@then("the virt_deferred capabilities should have sandbox {value}")
def step_caps_sandbox_virt_deferred(context: object, value: str) -> None:
"""Assert the sandbox capability."""
expected = value.lower() == "true"
actual = context.virt_deferred_schema.capabilities["sandbox"] # type: ignore[attr-defined]
assert actual == expected, f"Expected sandbox={expected}, got {actual}"
@then("the virt_deferred capabilities should have checkpoint {value}")
def step_caps_checkpoint_virt_deferred(context: object, value: str) -> None:
"""Assert the checkpoint capability."""
expected = value.lower() == "true"
actual = context.virt_deferred_schema.capabilities["checkpoint"] # type: ignore[attr-defined]
assert actual == expected, f"Expected checkpoint={expected}, got {actual}"
# ── Then steps: equivalence metadata ─────────────────────────
@then("the virt_deferred schema equivalence should be present")
def step_equivalence_present_virt_deferred(context: object) -> None:
"""Assert equivalence metadata is present."""
equiv = context.virt_deferred_schema.equivalence # type: ignore[attr-defined]
assert equiv is not None, "Equivalence metadata should not be None"
assert "criteria" in equiv, (
f"Equivalence must contain 'criteria', got keys: {list(equiv)}"
)
@then('the virt_deferred equivalence criteria should contain "{field}"')
def step_equivalence_criteria_virt_deferred(context: object, field: str) -> None:
"""Assert the equivalence criteria contains the given field."""
equiv = context.virt_deferred_schema.equivalence # type: ignore[attr-defined]
assert field in equiv["criteria"], (
f"Expected '{field}' in criteria {equiv['criteria']}"
)
# ── Then steps: domain model ────────────────────────────────
@then("the virt_deferred domain model should be valid")
def step_domain_valid_virt_deferred(context: object) -> None:
"""Assert the domain model loaded successfully with required fields."""
domain = context.virt_deferred_domain # type: ignore[attr-defined]
assert domain is not None, "Domain model should not be None"
assert domain.name, "Domain model must have a non-empty name"
assert domain.resource_kind is not None, "Domain model must have a resource_kind"
assert domain.equivalence is not None, (
f"Virtual domain model '{domain.name}' must have equivalence"
)
@then('the virt_deferred domain model name should be "{name}"')
def step_domain_name_virt_deferred(context: object, name: str) -> None:
"""Assert the domain model name matches."""
actual = context.virt_deferred_domain.name # type: ignore[attr-defined]
assert actual == name, f"Expected domain name '{name}', got '{actual}'"
@then('the virt_deferred domain model resource_kind should be "{kind}"')
def step_domain_kind_virt_deferred(context: object, kind: str) -> None:
"""Assert the domain model resource kind matches."""
actual = context.virt_deferred_domain.resource_kind # type: ignore[attr-defined]
assert str(actual) == kind or actual.value == kind, (
f"Expected resource_kind '{kind}', got '{actual}'"
)
# ── Then steps: child/parent types ──────────────────────────
@then('the virt_deferred schema child_types should contain "{child}"')
def step_schema_child_virt_deferred(context: object, child: str) -> None:
"""Assert the schema child_types contains the given type."""
types = context.virt_deferred_schema.child_types # type: ignore[attr-defined]
assert child in types, f"Expected '{child}' in child_types {types}"
@then("the virt_deferred schema should have empty parent_types")
def step_schema_no_parents_virt_deferred(context: object) -> None:
"""Assert the schema has no parent types."""
parents = context.virt_deferred_schema.parent_types # type: ignore[attr-defined]
assert len(parents) == 0, f"Expected empty parent_types, got {parents}"
@then("the virt_deferred schema should have {count:d} cli_args")
def step_schema_cli_args_count_virt_deferred(context: object, count: int) -> None:
"""Assert the schema has the expected number of CLI args."""
actual = len(context.virt_deferred_schema.cli_args) # type: ignore[attr-defined]
assert actual == count, f"Expected {count} cli_args, got {actual}"
@then("the virt_deferred schema handler should be none")
def step_schema_handler_none_virt_deferred(context: object) -> None:
"""Assert the schema has no handler."""
handler = context.virt_deferred_schema.handler # type: ignore[attr-defined]
assert handler is None, f"Expected handler None, got '{handler}'"
# ── Then steps: registry ─────────────────────────────────────
@then('the registry should contain type "{name}" for virt_deferred')
def step_registry_contains_virt_deferred(context: object, name: str) -> None:
"""Assert the registry contains the named type."""
types: dict[str, Any] = context.virt_deferred_builtin_types # type: ignore[attr-defined]
assert name in types, f"Type '{name}' not found in _BUILTIN_TYPES"
@then('the registry type "{name}" should not be user_addable for virt_deferred')
def step_registry_not_user_addable_virt_deferred(context: object, name: str) -> None:
"""Assert the named type in the registry is not user-addable."""
types: dict[str, Any] = context.virt_deferred_builtin_types # type: ignore[attr-defined]
assert types[name]["user_addable"] is False, (
f"Type '{name}' should not be user_addable"
)
@then('the registry type "{name}" should be virtual for virt_deferred')
def step_registry_virtual_virt_deferred(context: object, name: str) -> None:
"""Assert the named type in the registry is virtual."""
types: dict[str, Any] = context.virt_deferred_builtin_types # type: ignore[attr-defined]
assert types[name]["resource_kind"] == "virtual", f"Type '{name}' should be virtual"
@then('the registry type "{name}" should have equivalence for virt_deferred')
def step_registry_equivalence_virt_deferred(context: object, name: str) -> None:
"""Assert the named type in the registry has equivalence metadata."""
types: dict[str, Any] = context.virt_deferred_builtin_types # type: ignore[attr-defined]
assert types[name].get("equivalence") is not None, (
f"Type '{name}' should have equivalence metadata"
)
# ── Then steps: BUILTIN_NAMES ───────────────────────────────
@then('BUILTIN_NAMES should contain "{name}" for virt_deferred')
def step_builtin_names_contains_virt_deferred(context: object, name: str) -> None:
"""Assert BUILTIN_NAMES contains the given name."""
names: frozenset[str] = context.virt_deferred_builtin_names # type: ignore[attr-defined]
assert name in names, f"'{name}' not in BUILTIN_NAMES: {sorted(names)}"
# ── Given/When/Then steps: DB roundtrip via bootstrap ────────
@given("a virt_deferred fresh in-memory resource registry with bootstrap")
def step_fresh_registry_virt_deferred(context: object) -> None:
"""Create an in-memory SQLite DB, bootstrap all 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:", echo=False)
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine, expire_on_commit=False)
context.virt_deferred_service = ResourceRegistryService( # type: ignore[attr-defined]
session_factory=factory,
)
@when('I query the virt_deferred registry for type "{name}"')
def step_query_registry_virt_deferred(context: object, name: str) -> None:
"""Query the bootstrapped registry for a specific type."""
from cleveragents.core.exceptions import NotFoundError
service = context.virt_deferred_service # type: ignore[attr-defined]
try:
context.virt_deferred_db_spec = service.show_type(name) # type: ignore[attr-defined]
context.virt_deferred_db_found = True # type: ignore[attr-defined]
except NotFoundError:
context.virt_deferred_db_spec = None # type: ignore[attr-defined]
context.virt_deferred_db_found = False # type: ignore[attr-defined]
@when("I list all virt_deferred registry types")
def step_list_types_virt_deferred(context: object) -> None:
"""List all types from the bootstrapped registry."""
service = context.virt_deferred_service # type: ignore[attr-defined]
context.virt_deferred_type_list = [ # type: ignore[attr-defined]
t.name for t in service.list_types()
]
@then('the virt_deferred db type "{name}" should exist')
def step_db_type_exists_virt_deferred(context: object, name: str) -> None:
"""Assert the type was found in the DB."""
assert context.virt_deferred_db_found is True, ( # type: ignore[attr-defined]
f"Type '{name}' not found after bootstrap"
)
@then('the virt_deferred db type "{name}" should have kind "{kind}"')
def step_db_type_kind_virt_deferred(context: object, name: str, kind: str) -> None:
"""Assert the DB type has the expected resource kind."""
spec = context.virt_deferred_db_spec # type: ignore[attr-defined]
actual = (
str(spec.resource_kind.value)
if hasattr(spec.resource_kind, "value")
else str(spec.resource_kind)
)
assert actual == kind, f"Expected '{name}' kind '{kind}', got '{actual}'"
@then('the virt_deferred db type "{name}" should not be user_addable')
def step_db_type_not_user_addable_virt_deferred(context: object, name: str) -> None:
"""Assert the DB type is not user-addable."""
spec = context.virt_deferred_db_spec # type: ignore[attr-defined]
assert spec.user_addable is False, (
f"Type '{name}' should not be user_addable after roundtrip"
)
@then(
'the virt_deferred db type "{name}" should have equivalence with criteria "{criteria_csv}"'
)
def step_db_type_equivalence_virt_deferred(
context: object,
name: str,
criteria_csv: str,
) -> None:
"""Assert the DB type has equivalence with expected criteria after roundtrip."""
spec = context.virt_deferred_db_spec # type: ignore[attr-defined]
assert spec.equivalence is not None, (
f"Type '{name}' should have equivalence after roundtrip"
)
expected = [c.strip() for c in criteria_csv.split(",")]
actual = spec.equivalence.get("criteria", [])
assert actual == expected, (
f"Type '{name}' equivalence criteria: expected {expected}, got {actual}"
)
@then('the virt_deferred type list should include "{name}"')
def step_type_list_includes_virt_deferred(context: object, name: str) -> None:
"""Assert the type name appears in the list_types result."""
names: list[str] = context.virt_deferred_type_list # type: ignore[attr-defined]
assert name in names, f"'{name}' not in list_types result: {names}"
# ── When/Then steps: negative tests ─────────────────────────
@when("I create a virt_deferred virtual type config without equivalence")
def step_create_virtual_no_equiv_virt_deferred(context: object) -> None:
"""Attempt to create a virtual ResourceTypeSpec without equivalence."""
from pydantic import ValidationError
context.virt_deferred_error = None # type: ignore[attr-defined]
try:
ResourceTypeSpec.from_config(
{
"name": "test-virtual-no-equiv",
"resource_kind": "virtual",
"sandbox_strategy": "none",
"built_in": True,
}
)
except (ValueError, ValidationError) as exc:
context.virt_deferred_error = exc # type: ignore[attr-defined]
@when("I create a virt_deferred config without name")
def step_create_no_name_virt_deferred(context: object) -> None:
"""Attempt to create a ResourceTypeSpec without name."""
context.virt_deferred_error = None # type: ignore[attr-defined]
try:
ResourceTypeSpec.from_config(
{
"resource_kind": "virtual",
"sandbox_strategy": "none",
}
)
except ValueError as exc:
context.virt_deferred_error = exc # type: ignore[attr-defined]
@when("I create a virt_deferred config without resource_kind")
def step_create_no_kind_virt_deferred(context: object) -> None:
"""Attempt to create a ResourceTypeSpec without resource_kind."""
context.virt_deferred_error = None # type: ignore[attr-defined]
try:
ResourceTypeSpec.from_config(
{
"name": "test-missing-kind",
"sandbox_strategy": "none",
}
)
except ValueError as exc:
context.virt_deferred_error = exc # type: ignore[attr-defined]
@then("the virt_deferred config creation should raise a validation error")
def step_should_raise_validation_virt_deferred(context: object) -> None:
"""Assert that a validation error was raised."""
err = context.virt_deferred_error # type: ignore[attr-defined]
assert err is not None, "Expected a validation error but none was raised"
@then("the virt_deferred config creation should raise a ValueError")
def step_should_raise_value_error_virt_deferred(context: object) -> None:
"""Assert that a ValueError was raised."""
err = context.virt_deferred_error # type: ignore[attr-defined]
assert err is not None, "Expected a ValueError but none was raised"
assert isinstance(err, ValueError), (
f"Expected ValueError, got {type(err).__name__}: {err}"
)
# ── When/Then steps: manual add rejection (F-001 / F-003) ───
@when(
'I attempt to manually register a resource of type "{type_name}" for virt_deferred'
)
def step_attempt_manual_register_virt_deferred(context: object, type_name: str) -> None:
"""Attempt to register a resource instance of a non-user-addable type."""
service = context.virt_deferred_service # type: ignore[attr-defined]
context.virt_deferred_register_error = None # type: ignore[attr-defined]
try:
service.register_resource(
type_name=type_name,
name=f"test-manual-{type_name}",
)
except Exception as exc:
context.virt_deferred_register_error = exc # type: ignore[attr-defined]
@then("the virt_deferred manual registration should be rejected")
def step_manual_registration_rejected_virt_deferred(context: object) -> None:
"""Assert that manual registration was rejected."""
err = context.virt_deferred_register_error # type: ignore[attr-defined]
assert err is not None, "Expected registration to be rejected but it succeeded"
@then('the virt_deferred rejection message should mention "{text}"')
def step_rejection_message_mentions_virt_deferred(context: object, text: str) -> None:
"""Assert the rejection error message contains expected text."""
err = context.virt_deferred_register_error # type: ignore[attr-defined]
assert text in str(err), f"Expected '{text}' in error message, got: {err}"
# ── When steps: criteria validation (F-004) ──────────────────
@when("I create a virt_deferred virtual type config with criteria containing 123")
def step_create_criteria_non_string_virt_deferred(context: object) -> None:
"""Attempt to create a virtual type with non-string criteria element."""
from pydantic import ValidationError
context.virt_deferred_error = None # type: ignore[attr-defined]
try:
ResourceTypeSpec.from_config(
{
"name": "test-bad-criteria-int",
"resource_kind": "virtual",
"sandbox_strategy": "none",
"built_in": True,
"equivalence": {"criteria": [123], "description": "bad"},
}
)
except (ValueError, ValidationError) as exc:
context.virt_deferred_error = exc # type: ignore[attr-defined]
@when('I create a virt_deferred virtual type config with criteria containing ""')
def step_create_criteria_empty_string_virt_deferred(context: object) -> None:
"""Attempt to create a virtual type with empty-string criteria element."""
from pydantic import ValidationError
context.virt_deferred_error = None # type: ignore[attr-defined]
try:
ResourceTypeSpec.from_config(
{
"name": "test-bad-criteria-empty",
"resource_kind": "virtual",
"sandbox_strategy": "none",
"built_in": True,
"equivalence": {"criteria": [""], "description": "bad"},
}
)
except (ValueError, ValidationError) as exc:
context.virt_deferred_error = exc # type: ignore[attr-defined]
+15 -1
View File
@@ -44,7 +44,21 @@ 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)
if kind == "virtual":
config = _base_config(
resource_kind=kind,
sandbox_strategy="none",
)
config["user_addable"] = False
config["handler"] = None
config["capabilities"] = {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
}
else:
config = _base_config(resource_kind=kind)
config["equivalence"] = {
"criteria": ["content_hash"],
"description": "Test equivalence",
+134
View File
@@ -0,0 +1,134 @@
*** Settings ***
Documentation Deferred Virtual Resource Type Tests (#331)
Library Process
Library OperatingSystem
*** Variables ***
${PYTHON} python
${REMOTE_YAML} ${CURDIR}/../examples/resource-types/remote.yaml
${SUBMODULE_YAML} ${CURDIR}/../examples/resource-types/submodule.yaml
${SYMLINK_YAML} ${CURDIR}/../examples/resource-types/symlink.yaml
*** Test Cases ***
Load Remote Virtual Type YAML And Validate
[Documentation] Load remote.yaml via schema loader and assert key fields
${script}= Catenate SEPARATOR=\n
... from cleveragents.resource.schema import ResourceTypeConfigSchema
... schema = ResourceTypeConfigSchema.from_yaml_file("${REMOTE_YAML}")
... assert schema.name == "remote", f"name: {schema.name}"
... assert schema.resource_kind == "virtual", f"kind: {schema.resource_kind}"
... assert schema.sandbox_strategy == "none", f"strategy: {schema.sandbox_strategy}"
... assert schema.built_in is True, f"built_in: {schema.built_in}"
... assert schema.user_addable is False, f"user_addable: {schema.user_addable}"
... assert len(schema.cli_args) == 0, f"cli_args: {len(schema.cli_args)}"
... assert schema.equivalence is not None, "equivalence missing"
... assert "url" in schema.equivalence["criteria"]
... assert "git-remote" in schema.child_types
... print("remote deferred virtual YAML validated successfully")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 remote load failed: ${result.stderr}
Should Contain ${result.stdout} remote deferred virtual YAML validated successfully
Load Submodule Virtual Type YAML And Validate
[Documentation] Load submodule.yaml via schema loader and assert key fields
${script}= Catenate SEPARATOR=\n
... from cleveragents.resource.schema import ResourceTypeConfigSchema
... schema = ResourceTypeConfigSchema.from_yaml_file("${SUBMODULE_YAML}")
... assert schema.name == "submodule", f"name: {schema.name}"
... assert schema.resource_kind == "virtual", f"kind: {schema.resource_kind}"
... assert schema.sandbox_strategy == "none", f"strategy: {schema.sandbox_strategy}"
... assert schema.built_in is True, f"built_in: {schema.built_in}"
... assert schema.user_addable is False, f"user_addable: {schema.user_addable}"
... assert len(schema.cli_args) == 0, f"cli_args: {len(schema.cli_args)}"
... assert schema.equivalence is not None, "equivalence missing"
... assert "url" in schema.equivalence["criteria"]
... assert "path" in schema.equivalence["criteria"]
... assert "git-submodule" in schema.child_types
... print("submodule deferred virtual YAML validated successfully")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 submodule load failed: ${result.stderr}
Should Contain ${result.stdout} submodule deferred virtual YAML validated successfully
Load Symlink Virtual Type YAML And Validate
[Documentation] Load symlink.yaml via schema loader and assert key fields
${script}= Catenate SEPARATOR=\n
... from cleveragents.resource.schema import ResourceTypeConfigSchema
... schema = ResourceTypeConfigSchema.from_yaml_file("${SYMLINK_YAML}")
... assert schema.name == "symlink", f"name: {schema.name}"
... assert schema.resource_kind == "virtual", f"kind: {schema.resource_kind}"
... assert schema.sandbox_strategy == "none", f"strategy: {schema.sandbox_strategy}"
... assert schema.built_in is True, f"built_in: {schema.built_in}"
... assert schema.user_addable is False, f"user_addable: {schema.user_addable}"
... assert len(schema.cli_args) == 0, f"cli_args: {len(schema.cli_args)}"
... assert schema.equivalence is not None, "equivalence missing"
... assert "name" in schema.equivalence["criteria"]
... assert "target_path" in schema.equivalence["criteria"]
... assert "fs-symlink" in schema.child_types
... assert "git-tree-entry" in schema.child_types
... print("symlink deferred virtual YAML validated successfully")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 symlink load failed: ${result.stderr}
Should Contain ${result.stdout} symlink deferred virtual YAML validated successfully
Load All Deferred Virtual YAMLs As Domain Models
[Documentation] Load all deferred virtual YAMLs as ResourceTypeSpec domain models
${script}= Catenate SEPARATOR=\n
... import yaml
... from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
... def check(path, expected_name, expected_criteria):
... ${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 == expected_name, f"name mismatch: {spec.name}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert str(spec.resource_kind) == "virtual", f"kind: {spec.resource_kind}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.equivalence is not None, "equivalence missing"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.equivalence["criteria"] == expected_criteria
... ${SPACE}${SPACE}${SPACE}${SPACE}cli_dict = spec.as_cli_dict()
... ${SPACE}${SPACE}${SPACE}${SPACE}assert "equivalence" in cli_dict, "missing equivalence in cli_dict"
... check("${REMOTE_YAML}", "remote", ["url"])
... check("${SUBMODULE_YAML}", "submodule", ["url", "path"])
... check("${SYMLINK_YAML}", "symlink", ["name", "target_path"])
... print("All deferred virtual 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 deferred virtual domain models validated successfully
Virtual Types Survive DB Bootstrap Roundtrip
[Documentation] Bootstrap in-memory DB and verify virtual types are retrievable
${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:", echo=False)
... Base.metadata.create_all(engine)
... factory = sessionmaker(bind=engine, expire_on_commit=False)
... svc = ResourceRegistryService(session_factory=factory)
... for name, expected_criteria in [("remote", ["url"]), ("submodule", ["url", "path"]), ("symlink", ["name", "target_path"])]:
... ${SPACE}${SPACE}${SPACE}${SPACE}spec = svc.show_type(name)
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec is not None, f"{name} not found after bootstrap"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert str(spec.resource_kind.value) == "virtual", f"{name} kind: {spec.resource_kind}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.user_addable is False, f"{name} user_addable: {spec.user_addable}"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.equivalence is not None, f"{name} missing equivalence"
... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.equivalence["criteria"] == expected_criteria, f"{name} criteria: {spec.equivalence}"
... names = [t.name for t in svc.list_types()]
... for vt in ["remote", "submodule", "symlink"]:
... ${SPACE}${SPACE}${SPACE}${SPACE}assert vt in names, f"{vt} not in list_types: {names}"
... print("All virtual types survived DB bootstrap roundtrip")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 DB roundtrip failed: ${result.stderr}
Should Contain ${result.stdout} All virtual types survived DB bootstrap roundtrip
Virtual Type Without Equivalence Fails Validation
[Documentation] Verify that creating a virtual type without equivalence raises an error
${script}= Catenate SEPARATOR=\n
... from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
... try:
... ${SPACE}${SPACE}${SPACE}${SPACE}ResourceTypeSpec.from_config({"name": "bad-virtual", "resource_kind": "virtual", "sandbox_strategy": "none", "built_in": True})
... ${SPACE}${SPACE}${SPACE}${SPACE}print("ERROR: no exception raised")
... except (ValueError, Exception) as e:
... ${SPACE}${SPACE}${SPACE}${SPACE}assert "equivalence" in str(e).lower(), f"Unexpected error: {e}"
... ${SPACE}${SPACE}${SPACE}${SPACE}print("Correctly rejected virtual type without equivalence")
${result}= Run Process ${PYTHON} -c ${script}
Should Be Equal As Integers ${result.rc} 0 Validation test failed: ${result.stderr}
Should Contain ${result.stdout} Correctly rejected virtual type without equivalence
@@ -20,6 +20,9 @@ from typing import Any
from cleveragents.application.services._resource_registry_virtual import (
VIRTUAL_CORE_TYPES,
)
from cleveragents.application.services._resource_registry_virtual_deferred import (
VIRTUAL_DEFERRED_TYPES,
)
from cleveragents.domain.models.core.resource import PhysVirt, Resource
from cleveragents.domain.models.core.resource_type import (
ResourceKind,
2
@@ -782,6 +785,8 @@ BUILTIN_TYPES: list[dict[str, Any]] = [
*_AWS_TYPES,
*_GCP_AZURE_TYPES,
*DATABASE_TYPE_DEFS,
# Deferred virtual types -- _resource_registry_virtual_deferred (#331)
*VIRTUAL_DEFERRED_TYPES,
]
@@ -0,0 +1,115 @@
"""Deferred virtual resource type definitions for the Resource Registry.
Contains the built-in deferred virtual type entries (remote, submodule,
symlink) that are appended to ``BUILTIN_TYPES`` in
``_resource_registry_data``. Extracted to keep each module under the
500-line CONTRIBUTING limit and for consistency with
``_resource_registry_virtual`` (#329).
.. 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_DEFERRED_TYPES"]
# === Deferred Virtual Resource Types (#331) ===
#
# These types represent abstract identities for concepts that have
# physical counterparts introduced by #662 (deferred physical types).
# Each defines equivalence criteria used by the equivalence tracking
# service (#334) to auto-link physical resources.
VIRTUAL_DEFERRED_TYPES: list[dict[str, Any]] = [
{
"name": "remote",
"description": (
"Cross-repo remote identity. Links git-remote resources "
"across different repos that point to the same URL."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["git-remote"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["url"],
"description": (
"Two git-remote resources share a remote parent "
"when they point to the same normalized URL."
),
},
},
{
"name": "submodule",
"description": (
"Cross-repo submodule identity. Links git-submodule resources "
"across different repos with the same submodule URL and path."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["git-submodule"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["url", "path"],
"description": (
"Two git-submodule resources share a submodule parent "
"when they have the same submodule URL and path."
),
},
},
{
"name": "symlink",
"description": (
"Cross-layer symlink identity. Links fs-symlink and "
"git-tree-entry (mode 120000) resources with the same "
"name and target."
),
"resource_kind": "virtual",
"sandbox_strategy": "none",
"user_addable": False,
"built_in": True,
"cli_args": [],
"parent_types": [],
"child_types": ["fs-symlink", "git-tree-entry"],
"handler": None,
"capabilities": {
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
},
"equivalence": {
"criteria": ["name", "target_path"],
"description": (
"Two physical symlink resources share a symlink parent "
"when they have the same name and target path."
),
},
},
]
@@ -22,6 +22,9 @@ development workflows. Built-in types are **unnamespaced** (no
| ``branch`` | virtual | ``none`` | Cross-repo branch identity |
| ``tag`` | virtual | ``none`` | Cross-repo tag identity |
| ``tree`` | virtual | ``none`` | Cross-repo tree identity |
| ``remote`` | virtual | ``none`` | Cross-repo remote identity |
| ``submodule`` | virtual | ``none`` | Cross-repo submodule identity |
| ``symlink`` | virtual | ``none`` | Cross-layer symlink identity |
Custom types must follow the ``namespace/name`` pattern and are registered
via YAML configuration files.
@@ -29,7 +32,6 @@ via YAML configuration files.
Based on:
- ``docs/specification.md`` -- Resource Registry
- ``implementation_plan.md`` -- Task B0.services
- ADR-003: Dependency Injection
- ADR-007: Repository Pattern
@@ -96,10 +98,10 @@ class ResourceRegistryService(ResourceInstanceMixin, ResourceDagMixin):
"""Initialize the resource registry service.
Automatically seeds built-in resource types (``fs-directory``,
``git-checkout``, etc.) on construction so they are available
immediately after ``agents init``. The bootstrap is idempotent
and gracefully skipped when the database tables do not yet exist
(e.g. before migrations have run).
``git-checkout``, virtual deferred types, etc.) on construction
so they are available immediately after ``agents init``. The
bootstrap is idempotent and gracefully skipped when the database
tables do not yet exist (e.g. before migrations have run).
Args:
session_factory: Callable returning a SQLAlchemy Session.
@@ -242,6 +242,10 @@ class ResourceTypeSpec(BaseModel):
"file",
"tag",
"tree",
# Deferred virtual resource types (#331)
"remote",
"submodule",
"symlink",
}
)
1
@@ -380,7 +384,9 @@ class ResourceTypeSpec(BaseModel):
"Only built-in types can be unnamespaced."
)
# Virtual types require equivalence rules with non-empty criteria
# Virtual types require equivalence rules with criteria and have
# strict constraints: no sandbox, not user-addable, no handler,
# all capabilities false.
if self.resource_kind == ResourceKind.VIRTUAL:
if self.equivalence is None:
raise ValueError(
@@ -389,14 +395,48 @@ class ResourceTypeSpec(BaseModel):
)
if "criteria" not in self.equivalence:
raise ValueError(
f"Virtual resource type '{self.name}' requires "
"'equivalence.criteria' key for identity matching."
f"Virtual resource type '{self.name}' equivalence must "
"contain a 'criteria' list per spec."
)
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."
f"Virtual resource type '{self.name}' equivalence "
"'criteria' must be a non-empty list."
)
for i, item in enumerate(criteria):
if not isinstance(item, str):
raise ValueError(
f"Virtual resource type '{self.name}' equivalence "
f"criteria[{i}] must be a string, got {type(item).__name__}."
)
if not item.strip():
raise ValueError(
f"Virtual resource type '{self.name}' equivalence "
f"criteria[{i}] must be a non-empty string."
)
if self.user_addable:
raise ValueError(
f"Virtual resource type '{self.name}' must not be "
"user_addable (virtual types are system-managed)."
)
if self.sandbox_strategy != SandboxStrategy.NONE:
raise ValueError(
f"Virtual resource type '{self.name}' must have "
"sandbox_strategy 'none'."
)
if self.handler is not None:
raise ValueError(
f"Virtual resource type '{self.name}' must not have "
"a handler (virtual types have no physical backing)."
)
caps = self.capabilities
if caps and any(
caps.get(k) for k in ("read", "write", "sandbox", "checkpoint")
):
raise ValueError(
f"Virtual resource type '{self.name}' must have all "
"capabilities set to false."
)
# ADR-042 rule 3: no cycles (self-inheritance)
@@ -457,6 +497,13 @@ class ResourceTypeSpec(BaseModel):
capabilities=config.get(
"capabilities",
{
"read": False,
"write": False,
"sandbox": False,
"checkpoint": False,
}
if config.get("resource_kind") == "virtual"
else {
"read": True,
"write": True,
"sandbox": True,
+2 -2
View File
@@ -72,10 +72,10 @@ ResourceRegistryService # noqa: B018, F821
_build_resource_registry_service # noqa: B018, F821
_build_namespaced_project_repo # noqa: B018, F821
_build_project_resource_link_repo # noqa: B018, F821
_BUILTIN_TYPES # noqa: B018, F821
BUILTIN_TYPES # noqa: B018, F821
_spec_to_db # noqa: B018, F821
_db_to_spec # noqa: B018, F821
_db_resource_to_domain # noqa: B018, F821
db_resource_to_domain # noqa: B018, F821
resource_registry_service # noqa: B018, F821
namespaced_project_repo # noqa: B018, F821
project_resource_link_repo # noqa: B018, F821