story1.3
This commit is contained in:
@@ -4,6 +4,14 @@ Defines all custom exceptions used by the methodologies/plugin system.
|
||||
Following architecture-defined patterns from Error-Handling-Patterns.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass # No additional type imports needed
|
||||
|
||||
|
||||
class AutoClaudeError(Exception):
|
||||
"""Base exception for all Auto Claude errors.
|
||||
@@ -33,9 +41,26 @@ class ManifestValidationError(PluginError):
|
||||
- Field values have incorrect types
|
||||
- Schema validation fails
|
||||
- Version constraints cannot be satisfied
|
||||
|
||||
Attributes:
|
||||
path: Path to the invalid manifest file
|
||||
errors: List of validation error messages
|
||||
"""
|
||||
|
||||
pass
|
||||
def __init__(self, path: Path | str, errors: list[str]) -> None:
|
||||
"""Initialize with manifest path and error details.
|
||||
|
||||
Args:
|
||||
path: Path to the invalid manifest file
|
||||
errors: List of validation error messages
|
||||
"""
|
||||
self.path: Path = path if isinstance(path, Path) else Path(path)
|
||||
self.errors: list[str] = errors
|
||||
if errors:
|
||||
error_summary = "\n - " + "\n - ".join(errors)
|
||||
else:
|
||||
error_summary = " (no details)"
|
||||
super().__init__(f"Invalid manifest at {path}:{error_summary}")
|
||||
|
||||
|
||||
class PluginLoadError(PluginError):
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
"""Manifest loader and dataclasses for methodology plugins.
|
||||
|
||||
This module provides the manifest loading functionality with JSON Schema validation.
|
||||
It defines dataclasses representing the manifest structure and validates
|
||||
manifest.yaml files against a schema before creating typed objects.
|
||||
|
||||
Architecture Source: architecture.md#Manifest-Schema
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import jsonschema
|
||||
import yaml
|
||||
|
||||
from apps.backend.methodologies.exceptions import ManifestValidationError
|
||||
|
||||
|
||||
def _is_valid_uri(uri: str) -> bool:
|
||||
"""Check if a string is a valid HTTP/HTTPS URI.
|
||||
|
||||
For manifest metadata fields (homepage, repository), we only accept
|
||||
HTTP and HTTPS schemes as these are the standard web URLs.
|
||||
|
||||
Args:
|
||||
uri: String to validate as URI
|
||||
|
||||
Returns:
|
||||
True if valid HTTP/HTTPS URI, False otherwise
|
||||
"""
|
||||
try:
|
||||
result = urlparse(uri)
|
||||
# Must have http/https scheme and netloc
|
||||
return result.scheme in ("http", "https") and bool(result.netloc)
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
# Create a format checker with URI validation
|
||||
_format_checker = jsonschema.FormatChecker()
|
||||
|
||||
|
||||
@_format_checker.checks("uri", raises=jsonschema.FormatError)
|
||||
def _check_uri(instance: str) -> bool:
|
||||
"""Custom URI format checker for jsonschema."""
|
||||
if not isinstance(instance, str):
|
||||
return True # Non-strings handled by type validation
|
||||
if not _is_valid_uri(instance):
|
||||
raise jsonschema.FormatError(f"'{instance}' is not a valid URI")
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseDefinition:
|
||||
"""Definition of a methodology phase from manifest.yaml.
|
||||
|
||||
Represents a phase entry in the manifest's phases list.
|
||||
Each phase defines a distinct stage in the methodology pipeline.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for the phase
|
||||
name: Human-readable name for display
|
||||
description: Optional detailed description of the phase
|
||||
artifacts: List of artifact IDs produced by this phase
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
artifacts: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckpointDefinition:
|
||||
"""Definition of a checkpoint from manifest.yaml.
|
||||
|
||||
Represents a checkpoint entry for Semi-Auto pause points.
|
||||
Checkpoints define where the methodology pauses for user review.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for the checkpoint
|
||||
phase: ID of the phase this checkpoint follows
|
||||
description: What the user should review at this checkpoint
|
||||
"""
|
||||
|
||||
id: str
|
||||
phase: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArtifactDefinition:
|
||||
"""Definition of an artifact from manifest.yaml.
|
||||
|
||||
Represents an artifact entry that defines methodology outputs.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for the artifact
|
||||
name: Human-readable name for display
|
||||
type: Type of artifact (markdown, json, yaml, etc.)
|
||||
path: File path string relative to spec directory (e.g., "spec.md")
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
type: str = "markdown"
|
||||
path: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MethodologyManifest:
|
||||
"""Complete manifest for a methodology plugin.
|
||||
|
||||
Contains all configuration from a methodology's manifest.yaml file.
|
||||
This dataclass is created after successful schema validation.
|
||||
|
||||
Note: PhaseDefinition represents the static manifest structure, while
|
||||
Phase (from protocols.py) represents runtime state with status tracking.
|
||||
Conversion between these types occurs during plugin loading (Story 1.4).
|
||||
|
||||
Attributes:
|
||||
name: Unique identifier for the methodology (required)
|
||||
version: Semantic version string (required)
|
||||
entry_point: Module.ClassName for the runner (required)
|
||||
phases: List of phase definitions (required)
|
||||
description: Human-readable description (optional)
|
||||
author: Author name or organization (optional)
|
||||
checkpoints: List of checkpoint definitions (optional)
|
||||
artifacts: List of artifact definitions (optional)
|
||||
min_auto_claude_version: Minimum Auto Claude version required (optional)
|
||||
runtime: Runtime requirement e.g. "python3.12" (optional)
|
||||
complexity_levels: Supported complexity levels (optional)
|
||||
execution_modes: Supported execution modes (optional)
|
||||
license: License identifier e.g. "MIT" (optional)
|
||||
homepage: Project homepage URL (optional)
|
||||
repository: Source repository URL (optional)
|
||||
keywords: List of keywords for discovery (optional)
|
||||
"""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
entry_point: str
|
||||
phases: list[PhaseDefinition]
|
||||
description: str = ""
|
||||
author: str = ""
|
||||
checkpoints: list[CheckpointDefinition] = field(default_factory=list)
|
||||
artifacts: list[ArtifactDefinition] = field(default_factory=list)
|
||||
min_auto_claude_version: str = ""
|
||||
runtime: str = ""
|
||||
complexity_levels: list[str] = field(default_factory=list)
|
||||
execution_modes: list[str] = field(default_factory=list)
|
||||
license: str = ""
|
||||
homepage: str = ""
|
||||
repository: str = ""
|
||||
keywords: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _parse_phases(phases_data: list[dict[str, Any]]) -> list[PhaseDefinition]:
|
||||
"""Parse phase definitions from manifest data.
|
||||
|
||||
Args:
|
||||
phases_data: List of phase dictionaries from YAML
|
||||
|
||||
Returns:
|
||||
List of PhaseDefinition dataclass instances
|
||||
"""
|
||||
return [
|
||||
PhaseDefinition(
|
||||
id=phase["id"],
|
||||
name=phase["name"],
|
||||
description=phase.get("description", ""),
|
||||
artifacts=phase.get("artifacts", []),
|
||||
)
|
||||
for phase in phases_data
|
||||
]
|
||||
|
||||
|
||||
def _parse_checkpoints(
|
||||
checkpoints_data: list[dict[str, Any]] | None,
|
||||
) -> list[CheckpointDefinition]:
|
||||
"""Parse checkpoint definitions from manifest data.
|
||||
|
||||
Args:
|
||||
checkpoints_data: List of checkpoint dictionaries from YAML, or None
|
||||
|
||||
Returns:
|
||||
List of CheckpointDefinition dataclass instances
|
||||
"""
|
||||
if not checkpoints_data:
|
||||
return []
|
||||
return [
|
||||
CheckpointDefinition(
|
||||
id=cp["id"],
|
||||
phase=cp["phase"],
|
||||
description=cp.get("description", ""),
|
||||
)
|
||||
for cp in checkpoints_data
|
||||
]
|
||||
|
||||
|
||||
def _parse_artifacts(
|
||||
artifacts_data: list[dict[str, Any]] | None,
|
||||
) -> list[ArtifactDefinition]:
|
||||
"""Parse artifact definitions from manifest data.
|
||||
|
||||
Args:
|
||||
artifacts_data: List of artifact dictionaries from YAML, or None
|
||||
|
||||
Returns:
|
||||
List of ArtifactDefinition dataclass instances
|
||||
"""
|
||||
if not artifacts_data:
|
||||
return []
|
||||
return [
|
||||
ArtifactDefinition(
|
||||
id=art["id"],
|
||||
name=art["name"],
|
||||
type=art.get("type", "markdown"),
|
||||
path=art.get("path", ""),
|
||||
)
|
||||
for art in artifacts_data
|
||||
]
|
||||
|
||||
|
||||
def _validate_duplicate_ids(
|
||||
items: list[dict[str, Any]] | None,
|
||||
item_type: str,
|
||||
) -> list[str]:
|
||||
"""Check for duplicate IDs in a list of items.
|
||||
|
||||
Args:
|
||||
items: List of item dictionaries with 'id' fields
|
||||
item_type: Type name for error messages (e.g., "phase", "checkpoint")
|
||||
|
||||
Returns:
|
||||
List of error messages for duplicate IDs
|
||||
"""
|
||||
if not items:
|
||||
return []
|
||||
|
||||
# Skip if items is not a list (schema validation will catch this)
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
|
||||
errors: list[str] = []
|
||||
seen_ids: set[str] = set()
|
||||
|
||||
for item in items:
|
||||
# Skip if item is not a dict (schema validation will catch this)
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
item_id = item.get("id")
|
||||
if item_id:
|
||||
if item_id in seen_ids:
|
||||
errors.append(f"Duplicate {item_type} ID: '{item_id}'")
|
||||
else:
|
||||
seen_ids.add(item_id)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_checkpoint_references(
|
||||
checkpoints: list[dict[str, Any]] | None,
|
||||
phases: list[dict[str, Any]],
|
||||
) -> list[str]:
|
||||
"""Validate that checkpoint phase references exist.
|
||||
|
||||
Args:
|
||||
checkpoints: List of checkpoint dictionaries
|
||||
phases: List of phase dictionaries
|
||||
|
||||
Returns:
|
||||
List of error messages for invalid references
|
||||
"""
|
||||
if not checkpoints:
|
||||
return []
|
||||
|
||||
errors: list[str] = []
|
||||
phase_ids = {phase.get("id") for phase in phases if phase.get("id")}
|
||||
|
||||
for checkpoint in checkpoints:
|
||||
checkpoint_id = checkpoint.get("id", "unknown")
|
||||
phase_ref = checkpoint.get("phase")
|
||||
if phase_ref and phase_ref not in phase_ids:
|
||||
errors.append(
|
||||
f"Checkpoint '{checkpoint_id}' references non-existent phase: '{phase_ref}'"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_manifest(manifest_data: dict[str, Any], path: Path) -> list[str]:
|
||||
"""Validate manifest data against JSON Schema and semantic rules.
|
||||
|
||||
Performs:
|
||||
1. JSON Schema validation (structure, types, formats)
|
||||
2. Duplicate ID validation (phases, checkpoints, artifacts)
|
||||
3. Cross-reference validation (checkpoint → phase references)
|
||||
|
||||
Args:
|
||||
manifest_data: Parsed YAML data dictionary
|
||||
path: Path to the manifest file (for error messages)
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty if valid)
|
||||
"""
|
||||
# Load the JSON Schema
|
||||
schema_path = Path(__file__).parent / "schemas" / "manifest.schema.json"
|
||||
if not schema_path.exists():
|
||||
return [f"Schema file not found: {schema_path}"]
|
||||
|
||||
try:
|
||||
with open(schema_path, encoding="utf-8") as f:
|
||||
schema = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
return [f"Invalid schema JSON: {e}"]
|
||||
|
||||
# Collect all validation errors
|
||||
errors: list[str] = []
|
||||
|
||||
# 1. JSON Schema validation with format checking (for URI validation)
|
||||
validator = jsonschema.Draft7Validator(schema, format_checker=_format_checker)
|
||||
|
||||
for error in validator.iter_errors(manifest_data):
|
||||
# Format error message with path and description
|
||||
if error.path:
|
||||
field_path = ".".join(str(p) for p in error.path)
|
||||
errors.append(f"{field_path}: {error.message}")
|
||||
else:
|
||||
errors.append(error.message)
|
||||
|
||||
# 2. Duplicate ID validation
|
||||
phases = manifest_data.get("phases", [])
|
||||
checkpoints = manifest_data.get("checkpoints")
|
||||
artifacts = manifest_data.get("artifacts")
|
||||
|
||||
errors.extend(_validate_duplicate_ids(phases, "phase"))
|
||||
errors.extend(_validate_duplicate_ids(checkpoints, "checkpoint"))
|
||||
errors.extend(_validate_duplicate_ids(artifacts, "artifact"))
|
||||
|
||||
# 3. Cross-reference validation
|
||||
errors.extend(_validate_checkpoint_references(checkpoints, phases))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def load_manifest(path: Path) -> MethodologyManifest:
|
||||
"""Load and validate a methodology manifest.
|
||||
|
||||
Reads a manifest.yaml file, validates it against the JSON Schema,
|
||||
and returns a MethodologyManifest dataclass with all fields populated.
|
||||
|
||||
Args:
|
||||
path: Path to the manifest.yaml file
|
||||
|
||||
Returns:
|
||||
MethodologyManifest dataclass with validated data
|
||||
|
||||
Raises:
|
||||
ManifestValidationError: If manifest is missing, malformed, or invalid
|
||||
FileNotFoundError: If the manifest file doesn't exist
|
||||
"""
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Manifest file not found: {path}")
|
||||
|
||||
# Read and parse YAML
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
manifest_data = yaml.safe_load(f)
|
||||
except yaml.YAMLError as e:
|
||||
raise ManifestValidationError(path, [f"Invalid YAML: {e}"]) from e
|
||||
|
||||
if not manifest_data or not isinstance(manifest_data, dict):
|
||||
raise ManifestValidationError(path, ["Manifest must be a YAML object"])
|
||||
|
||||
# Validate against JSON Schema
|
||||
errors = _validate_manifest(manifest_data, path)
|
||||
if errors:
|
||||
raise ManifestValidationError(path, errors)
|
||||
|
||||
# Parse and create dataclass
|
||||
return MethodologyManifest(
|
||||
name=manifest_data["name"],
|
||||
version=manifest_data["version"],
|
||||
entry_point=manifest_data["entry_point"],
|
||||
phases=_parse_phases(manifest_data["phases"]),
|
||||
description=manifest_data.get("description", ""),
|
||||
author=manifest_data.get("author", ""),
|
||||
checkpoints=_parse_checkpoints(manifest_data.get("checkpoints")),
|
||||
artifacts=_parse_artifacts(manifest_data.get("artifacts")),
|
||||
min_auto_claude_version=manifest_data.get("min_auto_claude_version", ""),
|
||||
runtime=manifest_data.get("runtime", ""),
|
||||
complexity_levels=manifest_data.get("complexity_levels", []),
|
||||
execution_modes=manifest_data.get("execution_modes", []),
|
||||
license=manifest_data.get("license", ""),
|
||||
homepage=manifest_data.get("homepage", ""),
|
||||
repository=manifest_data.get("repository", ""),
|
||||
keywords=manifest_data.get("keywords", []),
|
||||
)
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Methodology Manifest",
|
||||
"description": "Schema for methodology plugin manifest.yaml files",
|
||||
"type": "object",
|
||||
"required": ["name", "version", "entry_point", "phases"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the methodology plugin (kebab-case recommended)",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Semantic version string (e.g., '1.0.0')",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$"
|
||||
},
|
||||
"entry_point": {
|
||||
"type": "string",
|
||||
"description": "Module.ClassName path to the MethodologyRunner implementation",
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)+$"
|
||||
},
|
||||
"phases": {
|
||||
"type": "array",
|
||||
"description": "List of phase definitions for the methodology pipeline",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/definitions/phase"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Human-readable description of the methodology"
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"description": "Author name or organization"
|
||||
},
|
||||
"checkpoints": {
|
||||
"type": "array",
|
||||
"description": "List of checkpoint definitions for Semi-Auto pause points",
|
||||
"items": {
|
||||
"$ref": "#/definitions/checkpoint"
|
||||
}
|
||||
},
|
||||
"artifacts": {
|
||||
"type": "array",
|
||||
"description": "List of artifact definitions for methodology outputs",
|
||||
"items": {
|
||||
"$ref": "#/definitions/artifact"
|
||||
}
|
||||
},
|
||||
"min_auto_claude_version": {
|
||||
"type": "string",
|
||||
"description": "Minimum Auto Claude version required (optional, for future compatibility checks)",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$"
|
||||
},
|
||||
"runtime": {
|
||||
"type": "string",
|
||||
"description": "Runtime requirement (optional, e.g., 'python3.12')",
|
||||
"pattern": "^[a-z][a-z0-9._-]*$"
|
||||
},
|
||||
"complexity_levels": {
|
||||
"type": "array",
|
||||
"description": "Supported complexity levels (optional)",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["quick", "standard", "complex"]
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"execution_modes": {
|
||||
"type": "array",
|
||||
"description": "Supported execution modes (optional)",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["full_auto", "semi_auto"]
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"license": {
|
||||
"type": "string",
|
||||
"description": "License identifier (optional, e.g., 'MIT', 'Apache-2.0')"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"description": "Project homepage URL (optional)",
|
||||
"format": "uri"
|
||||
},
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"description": "Source repository URL (optional)",
|
||||
"format": "uri"
|
||||
},
|
||||
"keywords": {
|
||||
"type": "array",
|
||||
"description": "Keywords for discovery (optional)",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"phase": {
|
||||
"type": "object",
|
||||
"description": "A phase definition in the methodology pipeline",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the phase",
|
||||
"pattern": "^[a-z][a-z0-9_-]*$",
|
||||
"minLength": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable name for display",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Detailed description of what this phase does"
|
||||
},
|
||||
"artifacts": {
|
||||
"type": "array",
|
||||
"description": "List of artifact IDs produced by this phase",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"checkpoint": {
|
||||
"type": "object",
|
||||
"description": "A checkpoint definition for Semi-Auto pause points",
|
||||
"required": ["id", "phase"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the checkpoint",
|
||||
"pattern": "^[a-z][a-z0-9_-]*$",
|
||||
"minLength": 1
|
||||
},
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"description": "ID of the phase this checkpoint follows"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "What the user should review at this checkpoint"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"artifact": {
|
||||
"type": "object",
|
||||
"description": "An artifact definition for methodology outputs",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the artifact",
|
||||
"pattern": "^[a-z][a-z0-9_-]*$",
|
||||
"minLength": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable name for display",
|
||||
"minLength": 1
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Type of artifact content",
|
||||
"enum": ["markdown", "json", "yaml", "text", "html"]
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the artifact file relative to spec directory"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,11 @@ google-generativeai>=0.8.0
|
||||
# Pydantic for structured output schemas
|
||||
pydantic>=2.0.0
|
||||
|
||||
# YAML parsing for plugin manifests and registry
|
||||
pyyaml>=6.0.0
|
||||
|
||||
# JSON Schema validation for manifest.yaml
|
||||
jsonschema>=4.0.0
|
||||
|
||||
# Error tracking (optional - requires SENTRY_DSN environment variable)
|
||||
sentry-sdk>=2.0.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -279,6 +279,7 @@ class TestExceptionHierarchy:
|
||||
|
||||
def test_exceptions_are_instantiable_with_message(self):
|
||||
"""Test that all exceptions can be instantiated with a message."""
|
||||
from pathlib import Path
|
||||
from apps.backend.methodologies.exceptions import (
|
||||
PluginError,
|
||||
ManifestValidationError,
|
||||
@@ -291,8 +292,13 @@ class TestExceptionHierarchy:
|
||||
e1 = PluginError(msg)
|
||||
assert str(e1) == msg
|
||||
|
||||
e2 = ManifestValidationError(msg)
|
||||
assert str(e2) == msg
|
||||
# ManifestValidationError now requires path and errors list
|
||||
test_path = Path("/test/manifest.yaml")
|
||||
e2 = ManifestValidationError(test_path, [msg])
|
||||
assert msg in str(e2)
|
||||
assert str(test_path) in str(e2)
|
||||
assert e2.path == test_path
|
||||
assert e2.errors == [msg]
|
||||
|
||||
e3 = PluginLoadError(msg)
|
||||
assert str(e3) == msg
|
||||
@@ -533,7 +539,8 @@ class TestNegativeAndEdgeCases:
|
||||
"""Test negative scenarios and edge cases."""
|
||||
|
||||
def test_exception_with_no_message(self):
|
||||
"""Test exceptions can be instantiated without a message."""
|
||||
"""Test exceptions can be instantiated without a message (except ManifestValidationError)."""
|
||||
from pathlib import Path
|
||||
from apps.backend.methodologies.exceptions import (
|
||||
PluginError,
|
||||
ManifestValidationError,
|
||||
@@ -544,8 +551,10 @@ class TestNegativeAndEdgeCases:
|
||||
e1 = PluginError()
|
||||
assert str(e1) == ""
|
||||
|
||||
e2 = ManifestValidationError()
|
||||
assert str(e2) == ""
|
||||
# ManifestValidationError requires path and errors - test with empty errors
|
||||
test_path = Path("/test/manifest.yaml")
|
||||
e2 = ManifestValidationError(test_path, [])
|
||||
assert str(test_path) in str(e2)
|
||||
|
||||
e3 = PluginLoadError()
|
||||
assert str(e3) == ""
|
||||
@@ -562,24 +571,27 @@ class TestNegativeAndEdgeCases:
|
||||
|
||||
def test_exception_can_be_raised_and_caught(self):
|
||||
"""Test exceptions can be raised and caught properly."""
|
||||
from pathlib import Path
|
||||
from apps.backend.methodologies.exceptions import (
|
||||
AutoClaudeError,
|
||||
PluginError,
|
||||
ManifestValidationError,
|
||||
)
|
||||
|
||||
test_path = Path("/test/manifest.yaml")
|
||||
|
||||
# Catch specific exception
|
||||
with pytest.raises(ManifestValidationError) as exc_info:
|
||||
raise ManifestValidationError("Invalid manifest")
|
||||
raise ManifestValidationError(test_path, ["Invalid manifest"])
|
||||
assert "Invalid manifest" in str(exc_info.value)
|
||||
|
||||
# Catch parent exception
|
||||
with pytest.raises(PluginError):
|
||||
raise ManifestValidationError("Caught as PluginError")
|
||||
raise ManifestValidationError(test_path, ["Caught as PluginError"])
|
||||
|
||||
# Catch grandparent exception
|
||||
with pytest.raises(AutoClaudeError):
|
||||
raise ManifestValidationError("Caught as AutoClaudeError")
|
||||
raise ManifestValidationError(test_path, ["Caught as AutoClaudeError"])
|
||||
|
||||
def test_phase_result_with_failure(self):
|
||||
"""Test PhaseResult can represent failure state."""
|
||||
|
||||
Reference in New Issue
Block a user