feat(resource): add deferred virtual resource types #663
+7
-1
@@ -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()
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user