From fc5ea50b3ac762b5d68f68192be5a72213c6770f Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:03:45 +0800 Subject: [PATCH 01/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20cli=20package?= =?UTF-8?q?=20=5F=5Finit=5F=5F=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 agent_assembly/cli/__init__.py diff --git a/agent_assembly/cli/__init__.py b/agent_assembly/cli/__init__.py new file mode 100644 index 0000000..429ec93 --- /dev/null +++ b/agent_assembly/cli/__init__.py @@ -0,0 +1 @@ +"""CLI tools for Agent Assembly SDK.""" From 4bbef1e6699daace01338d7e1e1d9f7440e79ca7 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:03:55 +0800 Subject: [PATCH 02/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20adapter=5Fvali?= =?UTF-8?q?dator=20module=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 agent_assembly/cli/adapter_validator.py diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py new file mode 100644 index 0000000..14bc993 --- /dev/null +++ b/agent_assembly/cli/adapter_validator.py @@ -0,0 +1,12 @@ +"""Adapter contract validation logic for community adapters.""" + +from __future__ import annotations + +import inspect +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor From 71207b6205d0e78f8936118cd681b0f45190290d Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:04:02 +0800 Subject: [PATCH 03/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20output=20modul?= =?UTF-8?q?e=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/output.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 agent_assembly/cli/output.py diff --git a/agent_assembly/cli/output.py b/agent_assembly/cli/output.py new file mode 100644 index 0000000..c09f811 --- /dev/null +++ b/agent_assembly/cli/output.py @@ -0,0 +1,8 @@ +"""Pass/fail output formatting for adapter validation results.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from agent_assembly.cli.adapter_validator import AdapterValidationResult From 01112a7bd39565bb94ff73e46f3c509e24edcc7e Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:04:10 +0800 Subject: [PATCH 04/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20main=20module?= =?UTF-8?q?=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/main.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 agent_assembly/cli/main.py diff --git a/agent_assembly/cli/main.py b/agent_assembly/cli/main.py new file mode 100644 index 0000000..71ff695 --- /dev/null +++ b/agent_assembly/cli/main.py @@ -0,0 +1,6 @@ +"""CLI entry point for Agent Assembly SDK tools.""" + +from __future__ import annotations + +import argparse +import sys From 1e1913204a86df63793f81b0f4eca5e662772a54 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:04:24 +0800 Subject: [PATCH 05/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20AdapterValidat?= =?UTF-8?q?ionResult=20dataclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 14bc993..cd870d0 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -10,3 +10,12 @@ pass from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor + + +@dataclass(frozen=True, slots=True) +class AdapterValidationResult: + """Result of a single adapter contract check.""" + + check_name: str + passed: bool + message: str From 42e9faae3eec50ba0c5690d719ac395f0122da8a Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:04:43 +0800 Subject: [PATCH 06/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fcheck=5Finh?= =?UTF-8?q?erits=5Fframework=5Fadapter=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index cd870d0..6872b3a 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -19,3 +19,18 @@ class AdapterValidationResult: check_name: str passed: bool message: str + + +def _check_inherits_framework_adapter(cls: type) -> AdapterValidationResult: + """Check that the class inherits from FrameworkAdapter.""" + if issubclass(cls, FrameworkAdapter): + return AdapterValidationResult( + check_name="inherits_framework_adapter", + passed=True, + message="Class inherits from FrameworkAdapter.", + ) + return AdapterValidationResult( + check_name="inherits_framework_adapter", + passed=False, + message=f"Class {cls.__name__} does not inherit from FrameworkAdapter.", + ) From 94f5d1ab653d01bd1eccf308b339f9ed946537b8 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:04:57 +0800 Subject: [PATCH 07/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fcheck=5Fabs?= =?UTF-8?q?tract=5Fmethods=5Fimplemented=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 6872b3a..3490fd1 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -34,3 +34,30 @@ def _check_inherits_framework_adapter(cls: type) -> AdapterValidationResult: passed=False, message=f"Class {cls.__name__} does not inherit from FrameworkAdapter.", ) + + +_REQUIRED_ABSTRACT_METHODS = frozenset( + { + "get_framework_name", + "get_supported_versions", + "register_hooks", + "unregister_hooks", + } +) + + +def _check_abstract_methods_implemented(cls: type) -> AdapterValidationResult: + """Check that all 4 required abstract methods are concretely implemented.""" + remaining = getattr(cls, "__abstractmethods__", frozenset()) + missing = _REQUIRED_ABSTRACT_METHODS & remaining + if not missing: + return AdapterValidationResult( + check_name="abstract_methods_implemented", + passed=True, + message="All required abstract methods are implemented.", + ) + return AdapterValidationResult( + check_name="abstract_methods_implemented", + passed=False, + message=f"Missing implementations: {', '.join(sorted(missing))}.", + ) From aae6fe1c05b0fae30b90b264b047b158e2d284f7 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:05:12 +0800 Subject: [PATCH 08/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fcheck=5Ffra?= =?UTF-8?q?mework=5Fname=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 3490fd1..7027f33 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -61,3 +61,26 @@ def _check_abstract_methods_implemented(cls: type) -> AdapterValidationResult: passed=False, message=f"Missing implementations: {', '.join(sorted(missing))}.", ) + + +def _check_framework_name(instance: FrameworkAdapter) -> AdapterValidationResult: + """Check that get_framework_name() returns a non-empty string.""" + try: + name = instance.get_framework_name() + except Exception as exc: + return AdapterValidationResult( + check_name="framework_name", + passed=False, + message=f"get_framework_name() raised {type(exc).__name__}: {exc}", + ) + if isinstance(name, str) and name.strip(): + return AdapterValidationResult( + check_name="framework_name", + passed=True, + message=f"Framework name: '{name}'.", + ) + return AdapterValidationResult( + check_name="framework_name", + passed=False, + message="get_framework_name() must return a non-empty string.", + ) From 9804fb5564d3b94a32b22f7d3da8885ffe227704 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:05:25 +0800 Subject: [PATCH 09/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fcheck=5Fsup?= =?UTF-8?q?ported=5Fversions=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 7027f33..4297669 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -84,3 +84,33 @@ def _check_framework_name(instance: FrameworkAdapter) -> AdapterValidationResult passed=False, message="get_framework_name() must return a non-empty string.", ) + + +def _check_supported_versions(instance: FrameworkAdapter) -> AdapterValidationResult: + """Check that get_supported_versions() returns a non-empty list of strings.""" + try: + versions = instance.get_supported_versions() + except Exception as exc: + return AdapterValidationResult( + check_name="supported_versions", + passed=False, + message=f"get_supported_versions() raised {type(exc).__name__}: {exc}", + ) + if not isinstance(versions, list) or not versions: + return AdapterValidationResult( + check_name="supported_versions", + passed=False, + message="get_supported_versions() must return a non-empty list.", + ) + for i, v in enumerate(versions): + if not isinstance(v, str) or not v.strip(): + return AdapterValidationResult( + check_name="supported_versions", + passed=False, + message=f"Version at index {i} must be a non-empty string.", + ) + return AdapterValidationResult( + check_name="supported_versions", + passed=True, + message=f"Supported versions: {versions}.", + ) From 4842284047ab07cbb2af0cdad0b3c9d71f6ce192 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:05:41 +0800 Subject: [PATCH 10/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fcheck=5Freg?= =?UTF-8?q?ister=5Fhooks=5Fsignature=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 4297669..d23adca 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -114,3 +114,31 @@ def _check_supported_versions(instance: FrameworkAdapter) -> AdapterValidationRe passed=True, message=f"Supported versions: {versions}.", ) + + +def _check_register_hooks_signature(cls: type) -> AdapterValidationResult: + """Check that register_hooks accepts a GovernanceInterceptor argument.""" + sig = inspect.signature(cls.register_hooks) + params = [p for name, p in sig.parameters.items() if name != "self"] + if not params: + return AdapterValidationResult( + check_name="register_hooks_signature", + passed=False, + message="register_hooks() must accept an interceptor argument.", + ) + first_param = params[0] + annotation = first_param.annotation + if annotation is inspect.Parameter.empty or annotation is GovernanceInterceptor: + return AdapterValidationResult( + check_name="register_hooks_signature", + passed=True, + message="register_hooks() accepts an interceptor argument.", + ) + return AdapterValidationResult( + check_name="register_hooks_signature", + passed=False, + message=( + f"register_hooks() first parameter annotated as {annotation}, " + f"expected GovernanceInterceptor." + ), + ) From c12f1706993d44179df000c2f76d6a73fb62d49d Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:05:59 +0800 Subject: [PATCH 11/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fcheck=5Funr?= =?UTF-8?q?egister=5Fhooks=5Fidempotent=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index d23adca..ecea568 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -142,3 +142,26 @@ def _check_register_hooks_signature(cls: type) -> AdapterValidationResult: f"expected GovernanceInterceptor." ), ) + + +def _check_unregister_hooks_idempotent( + instance: FrameworkAdapter, +) -> AdapterValidationResult: + """Check that calling unregister_hooks() twice does not raise.""" + try: + instance.unregister_hooks() + instance.unregister_hooks() + except Exception as exc: + return AdapterValidationResult( + check_name="unregister_hooks_idempotent", + passed=False, + message=( + f"unregister_hooks() is not idempotent: " + f"second call raised {type(exc).__name__}: {exc}" + ), + ) + return AdapterValidationResult( + check_name="unregister_hooks_idempotent", + passed=True, + message="unregister_hooks() is idempotent (two calls without error).", + ) From 0015cdba39564767e70b8f908fac7d50b65339d3 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:06:24 +0800 Subject: [PATCH 12/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fcheck=5Fent?= =?UTF-8?q?ry=5Fpoint=5Fmetadata=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index ecea568..6c7a69d 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -3,7 +3,9 @@ from __future__ import annotations import inspect +import tomllib from dataclasses import dataclass +from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -165,3 +167,61 @@ def _check_unregister_hooks_idempotent( passed=True, message="unregister_hooks() is idempotent (two calls without error).", ) + + +def _check_entry_point_metadata( + cls: type, path_or_module: str +) -> AdapterValidationResult: + """Check entry point metadata in pyproject.toml if present at the given path.""" + search_path = Path(path_or_module) + if search_path.is_file(): + search_path = search_path.parent + + pyproject_path = search_path / "pyproject.toml" + if not pyproject_path.is_file(): + return AdapterValidationResult( + check_name="entry_point_metadata", + passed=True, + message="No pyproject.toml found; skipping entry point check.", + ) + + try: + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + except Exception as exc: + return AdapterValidationResult( + check_name="entry_point_metadata", + passed=False, + message=f"Failed to parse pyproject.toml: {exc}", + ) + + entry_points = ( + data.get("project", {}).get("entry-points", {}).get("agent_assembly.adapters", {}) + ) + if not entry_points: + return AdapterValidationResult( + check_name="entry_point_metadata", + passed=False, + message=( + "pyproject.toml missing [project.entry-points.\"agent_assembly.adapters\"] " + "section." + ), + ) + + class_qualname = f"{cls.__module__}:{cls.__qualname__}" + for ep_name, ep_value in entry_points.items(): + if ep_value == class_qualname: + return AdapterValidationResult( + check_name="entry_point_metadata", + passed=True, + message=f"Entry point '{ep_name}' correctly references {class_qualname}.", + ) + + return AdapterValidationResult( + check_name="entry_point_metadata", + passed=False, + message=( + f"No entry point references {class_qualname}. " + f"Found: {entry_points}." + ), + ) From 0947fd257b618e42e018f7d57ad37e32450f91d5 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:06:47 +0800 Subject: [PATCH 13/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20validate=5Fada?= =?UTF-8?q?pter=20orchestrator=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 6c7a69d..6ce59d6 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -225,3 +225,26 @@ def _check_entry_point_metadata( f"Found: {entry_points}." ), ) + + +def validate_adapter( + cls: type, path_or_module: str +) -> list[AdapterValidationResult]: + """Run all contract checks against an adapter class and return results.""" + results: list[AdapterValidationResult] = [] + + results.append(_check_inherits_framework_adapter(cls)) + results.append(_check_abstract_methods_implemented(cls)) + + # Instance-level checks require a concrete class that can be instantiated + if any(not r.passed for r in results): + return results + + instance = cls() + results.append(_check_framework_name(instance)) + results.append(_check_supported_versions(instance)) + results.append(_check_register_hooks_signature(cls)) + results.append(_check_unregister_hooks_idempotent(instance)) + results.append(_check_entry_point_metadata(cls, path_or_module)) + + return results From 65e5b41898b729f71c9d860e9538e8509721b27e Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:07:18 +0800 Subject: [PATCH 14/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20load=5Fadapter?= =?UTF-8?q?=5Fclass=5Ffrom=5Fmodule=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 6ce59d6..85a5846 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -2,6 +2,8 @@ from __future__ import annotations +import importlib +import importlib.util import inspect import tomllib from dataclasses import dataclass @@ -248,3 +250,27 @@ def validate_adapter( results.append(_check_entry_point_metadata(cls, path_or_module)) return results + + +def _find_adapter_class_in_module(module: object) -> type | None: + """Scan a module for the first FrameworkAdapter subclass.""" + for _name, obj in inspect.getmembers(module, inspect.isclass): + if obj is not FrameworkAdapter and issubclass(obj, FrameworkAdapter): + return obj + return None + + +def load_adapter_class_from_module(module_name: str) -> type: + """Load an adapter class from a dotted module name. + + Raises: + ImportError: If the module cannot be imported. + ValueError: If no FrameworkAdapter subclass is found in the module. + """ + module = importlib.import_module(module_name) + cls = _find_adapter_class_in_module(module) + if cls is None: + raise ValueError( + f"No FrameworkAdapter subclass found in module '{module_name}'." + ) + return cls From b0a4e8fd1d4620bbe564d80d31f0911b000a6a68 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:07:37 +0800 Subject: [PATCH 15/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20load=5Fadapter?= =?UTF-8?q?=5Fclass=5Ffrom=5Fpath=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 85a5846..49c2c7e 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -274,3 +274,30 @@ def load_adapter_class_from_module(module_name: str) -> type: f"No FrameworkAdapter subclass found in module '{module_name}'." ) return cls + + +def load_adapter_class_from_path(file_path: str) -> type: + """Load an adapter class from a file system path. + + Raises: + FileNotFoundError: If the path does not exist. + ValueError: If no FrameworkAdapter subclass is found in the file. + """ + path = Path(file_path).resolve() + if not path.is_file(): + raise FileNotFoundError(f"File not found: {path}") + + module_name = f"_aasm_validate_{path.stem}" + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise ValueError(f"Cannot create module spec from path: {path}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + cls = _find_adapter_class_in_module(module) + if cls is None: + raise ValueError( + f"No FrameworkAdapter subclass found in '{path}'." + ) + return cls From 21f824a12a4ada78095d2e4eb2cd0668f003be9f Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:07:53 +0800 Subject: [PATCH 16/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20load=5Fadapter?= =?UTF-8?q?=5Fclass=20dispatcher=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 49c2c7e..2ff6520 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -301,3 +301,11 @@ def load_adapter_class_from_path(file_path: str) -> type: f"No FrameworkAdapter subclass found in '{path}'." ) return cls + + +def load_adapter_class(path_or_module: str) -> type: + """Load an adapter class from either a file path or a dotted module name.""" + candidate = Path(path_or_module) + if candidate.exists(): + return load_adapter_class_from_path(path_or_module) + return load_adapter_class_from_module(path_or_module) From e9298c0181fb2630794130135bc4a31c5edf9f76 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:08:10 +0800 Subject: [PATCH 17/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20format=5Fresul?= =?UTF-8?q?ts=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/output.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/agent_assembly/cli/output.py b/agent_assembly/cli/output.py index c09f811..bdad3d5 100644 --- a/agent_assembly/cli/output.py +++ b/agent_assembly/cli/output.py @@ -6,3 +6,19 @@ if TYPE_CHECKING: from agent_assembly.cli.adapter_validator import AdapterValidationResult + + +def format_results(results: list[AdapterValidationResult]) -> str: + """Format validation results as human-readable PASS/FAIL lines.""" + lines: list[str] = [] + for result in results: + prefix = "PASS" if result.passed else "FAIL" + lines.append(f" [{prefix}] {result.check_name}: {result.message}") + + passed = sum(1 for r in results if r.passed) + failed = len(results) - passed + + lines.append("") + lines.append(f"Results: {passed} passed, {failed} failed, {len(results)} total") + + return "\n".join(lines) From 42ea7c36760a79f60eab07cb2ac0fb0f70633af8 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:08:33 +0800 Subject: [PATCH 18/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20argparse=20par?= =?UTF-8?q?ser=20with=20adapter=20subcommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/main.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/agent_assembly/cli/main.py b/agent_assembly/cli/main.py index 71ff695..fa51afc 100644 --- a/agent_assembly/cli/main.py +++ b/agent_assembly/cli/main.py @@ -4,3 +4,16 @@ import argparse import sys + + +def _build_parser() -> argparse.ArgumentParser: + """Build the top-level argument parser with the adapter subcommand.""" + parser = argparse.ArgumentParser( + prog="aasm", + description="Agent Assembly SDK command-line tools.", + ) + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + subparsers.add_parser("adapter", help="Adapter management commands") + + return parser From a24753153c1af8ad7845ee6626a4276844b2b2e1 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:08:47 +0800 Subject: [PATCH 19/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20validate=20sub?= =?UTF-8?q?command=20to=20adapter=20sub-parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/main.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/agent_assembly/cli/main.py b/agent_assembly/cli/main.py index fa51afc..0a9175a 100644 --- a/agent_assembly/cli/main.py +++ b/agent_assembly/cli/main.py @@ -14,6 +14,19 @@ def _build_parser() -> argparse.ArgumentParser: ) subparsers = parser.add_subparsers(dest="command", help="Available commands") - subparsers.add_parser("adapter", help="Adapter management commands") + adapter_parser = subparsers.add_parser( + "adapter", help="Adapter management commands" + ) + adapter_subparsers = adapter_parser.add_subparsers( + dest="adapter_command", help="Adapter subcommands" + ) + + validate_parser = adapter_subparsers.add_parser( + "validate", help="Validate a community adapter against the FrameworkAdapter contract" + ) + validate_parser.add_argument( + "path_or_module", + help="File path or dotted module name of the adapter to validate", + ) return parser From 8a9d75e291f3fda292d937f75a611802f20298e7 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:09:02 +0800 Subject: [PATCH 20/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20=5Fhandle=5Fad?= =?UTF-8?q?apter=5Fvalidate=20command=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/main.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/agent_assembly/cli/main.py b/agent_assembly/cli/main.py index 0a9175a..5e03af0 100644 --- a/agent_assembly/cli/main.py +++ b/agent_assembly/cli/main.py @@ -30,3 +30,21 @@ def _build_parser() -> argparse.ArgumentParser: ) return parser + + +def _handle_adapter_validate(args: argparse.Namespace) -> int: + """Run adapter validation and return an exit code.""" + from agent_assembly.cli.adapter_validator import load_adapter_class, validate_adapter + from agent_assembly.cli.output import format_results + + try: + cls = load_adapter_class(args.path_or_module) + except (ImportError, FileNotFoundError, ValueError) as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + results = validate_adapter(cls, args.path_or_module) + print(format_results(results)) + + all_passed = all(r.passed for r in results) + return 0 if all_passed else 1 From 7caf3e173460c1f0b5aba365fca41b471e184d1c Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:09:18 +0800 Subject: [PATCH 21/41] =?UTF-8?q?=E2=9C=A8=20(cli):=20Add=20main=20entry?= =?UTF-8?q?=20point=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/main.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/agent_assembly/cli/main.py b/agent_assembly/cli/main.py index 5e03af0..5729941 100644 --- a/agent_assembly/cli/main.py +++ b/agent_assembly/cli/main.py @@ -48,3 +48,15 @@ def _handle_adapter_validate(args: argparse.Namespace) -> int: all_passed = all(r.passed for r in results) return 0 if all_passed else 1 + + +def main() -> None: + """Parse CLI arguments and dispatch to the appropriate handler.""" + parser = _build_parser() + args = parser.parse_args() + + if args.command == "adapter" and getattr(args, "adapter_command", None) == "validate": + sys.exit(_handle_adapter_validate(args)) + else: + parser.print_help() + sys.exit(1) From 29995f27dabfdde309ece55cb12502d2ffd659b4 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:09:32 +0800 Subject: [PATCH 22/41] =?UTF-8?q?=F0=9F=94=A7=20(config):=20Register=20aas?= =?UTF-8?q?m=20CLI=20entry=20point=20in=20pyproject.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ba11b12..cfec966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,9 @@ dependencies = [ "typing-extensions>=4.0.0", ] +[project.scripts] +aasm = "agent_assembly.cli.main:main" + [project.urls] Homepage = "https://github.com/agent-assembly/python-sdk" Repository = "https://github.com/agent-assembly/python-sdk" From bb7bc4f10a6946041a1092a4594059ee210f66fb Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:09:49 +0800 Subject: [PATCH 23/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20test=20cli=20p?= =?UTF-8?q?ackage=20=5F=5Finit=5F=5F=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/unit/cli/__init__.py diff --git a/test/unit/cli/__init__.py b/test/unit/cli/__init__.py new file mode 100644 index 0000000..e69de29 From 9277552ed627dde3bd1ad933ff227a1a6037a244 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:10:06 +0800 Subject: [PATCH 24/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20test=20fixture?= =?UTF-8?q?s=20for=20valid=20and=20invalid=20adapters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/conftest.py | 107 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 test/unit/cli/conftest.py diff --git a/test/unit/cli/conftest.py b/test/unit/cli/conftest.py new file mode 100644 index 0000000..71bad07 --- /dev/null +++ b/test/unit/cli/conftest.py @@ -0,0 +1,107 @@ +"""Shared fixtures for CLI adapter validator tests.""" + +from __future__ import annotations + +import pytest + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor + + +class ValidAdapter(FrameworkAdapter): + """A fully valid adapter for testing.""" + + def get_framework_name(self) -> str: + return "test_framework" + + def get_supported_versions(self) -> list[str]: + return [">=1.0.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + pass + + def unregister_hooks(self) -> None: + pass + + +class EmptyNameAdapter(FrameworkAdapter): + """Adapter that returns an empty framework name.""" + + def get_framework_name(self) -> str: + return "" + + def get_supported_versions(self) -> list[str]: + return [">=1.0.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + pass + + def unregister_hooks(self) -> None: + pass + + +class EmptyVersionsAdapter(FrameworkAdapter): + """Adapter that returns an empty versions list.""" + + def get_framework_name(self) -> str: + return "test_framework" + + def get_supported_versions(self) -> list[str]: + return [] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + pass + + def unregister_hooks(self) -> None: + pass + + +class NonIdempotentAdapter(FrameworkAdapter): + """Adapter whose unregister_hooks raises on the second call.""" + + def __init__(self) -> None: + self._call_count = 0 + + def get_framework_name(self) -> str: + return "test_framework" + + def get_supported_versions(self) -> list[str]: + return [">=1.0.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + pass + + def unregister_hooks(self) -> None: + self._call_count += 1 + if self._call_count > 1: + raise RuntimeError("Already unregistered") + + +class NotAnAdapter: + """A class that does not inherit from FrameworkAdapter.""" + + pass + + +@pytest.fixture() +def valid_adapter_cls() -> type: + return ValidAdapter + + +@pytest.fixture() +def empty_name_adapter_cls() -> type: + return EmptyNameAdapter + + +@pytest.fixture() +def empty_versions_adapter_cls() -> type: + return EmptyVersionsAdapter + + +@pytest.fixture() +def non_idempotent_adapter_cls() -> type: + return NonIdempotentAdapter + + +@pytest.fixture() +def not_an_adapter_cls() -> type: + return NotAnAdapter From 7e58ca19758b122891c8bb430f5cdd21aedcb24c Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:10:53 +0800 Subject: [PATCH 25/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?AdapterValidationResult=20dataclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/unit/cli/test_adapter_validator.py diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py new file mode 100644 index 0000000..996ae9e --- /dev/null +++ b/test/unit/cli/test_adapter_validator.py @@ -0,0 +1,31 @@ +"""Unit tests for adapter validator logic.""" + +from __future__ import annotations + +from agent_assembly.cli.adapter_validator import AdapterValidationResult + + +class TestAdapterValidationResult: + """Tests for the AdapterValidationResult dataclass.""" + + def test_fields_stored(self) -> None: + result = AdapterValidationResult( + check_name="test_check", passed=True, message="ok" + ) + assert result.check_name == "test_check" + assert result.passed is True + assert result.message == "ok" + + def test_equality(self) -> None: + a = AdapterValidationResult(check_name="c", passed=True, message="m") + b = AdapterValidationResult(check_name="c", passed=True, message="m") + assert a == b + + def test_frozen(self) -> None: + import pytest + + result = AdapterValidationResult( + check_name="c", passed=True, message="m" + ) + with pytest.raises(AttributeError): + result.passed = False # type: ignore[misc] From 2852f15fb174feb451a5d18e99118a6cd000ec30 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:11:21 +0800 Subject: [PATCH 26/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?=5Fcheck=5Finherits=5Fframework=5Fadapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index 996ae9e..b655c89 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -2,7 +2,10 @@ from __future__ import annotations -from agent_assembly.cli.adapter_validator import AdapterValidationResult +from agent_assembly.cli.adapter_validator import ( + AdapterValidationResult, + _check_inherits_framework_adapter, +) class TestAdapterValidationResult: @@ -29,3 +32,17 @@ def test_frozen(self) -> None: ) with pytest.raises(AttributeError): result.passed = False # type: ignore[misc] + + +class TestCheckInheritsFrameworkAdapter: + """Tests for _check_inherits_framework_adapter.""" + + def test_valid_subclass_passes(self, valid_adapter_cls: type) -> None: + result = _check_inherits_framework_adapter(valid_adapter_cls) + assert result.passed is True + assert result.check_name == "inherits_framework_adapter" + + def test_non_subclass_fails(self, not_an_adapter_cls: type) -> None: + result = _check_inherits_framework_adapter(not_an_adapter_cls) + assert result.passed is False + assert "does not inherit" in result.message From b93e3f7b8f3b112a2efd8c374e5e437b22cd4521 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:12:22 +0800 Subject: [PATCH 27/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?=5Fcheck=5Fabstract=5Fmethods=5Fimplemented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index b655c89..d86ea55 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -4,8 +4,10 @@ from agent_assembly.cli.adapter_validator import ( AdapterValidationResult, + _check_abstract_methods_implemented, _check_inherits_framework_adapter, ) +from agent_assembly.adapters.base import FrameworkAdapter class TestAdapterValidationResult: @@ -46,3 +48,24 @@ def test_non_subclass_fails(self, not_an_adapter_cls: type) -> None: result = _check_inherits_framework_adapter(not_an_adapter_cls) assert result.passed is False assert "does not inherit" in result.message + + +class TestCheckAbstractMethodsImplemented: + """Tests for _check_abstract_methods_implemented.""" + + def test_all_methods_concrete_passes(self, valid_adapter_cls: type) -> None: + result = _check_abstract_methods_implemented(valid_adapter_cls) + assert result.passed is True + + def test_missing_method_fails(self) -> None: + class PartialAdapter(FrameworkAdapter): + def get_framework_name(self) -> str: + return "test" + + def get_supported_versions(self) -> list[str]: + return [">=1.0.0"] + + result = _check_abstract_methods_implemented(PartialAdapter) + assert result.passed is False + assert "register_hooks" in result.message + assert "unregister_hooks" in result.message From a32a297a8280e933d3ebe3f5195120ebbe512fe6 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:13:04 +0800 Subject: [PATCH 28/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?=5Fcheck=5Fframework=5Fname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index d86ea55..7c6fcb9 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -5,6 +5,7 @@ from agent_assembly.cli.adapter_validator import ( AdapterValidationResult, _check_abstract_methods_implemented, + _check_framework_name, _check_inherits_framework_adapter, ) from agent_assembly.adapters.base import FrameworkAdapter @@ -69,3 +70,27 @@ def get_supported_versions(self) -> list[str]: assert result.passed is False assert "register_hooks" in result.message assert "unregister_hooks" in result.message + + +class TestCheckFrameworkName: + """Tests for _check_framework_name.""" + + def test_non_empty_name_passes(self, valid_adapter_cls: type) -> None: + result = _check_framework_name(valid_adapter_cls()) + assert result.passed is True + assert "test_framework" in result.message + + def test_empty_name_fails(self, empty_name_adapter_cls: type) -> None: + result = _check_framework_name(empty_name_adapter_cls()) + assert result.passed is False + assert "non-empty string" in result.message + + def test_whitespace_name_fails(self) -> None: + from test.unit.cli.conftest import ValidAdapter + + class WhitespaceAdapter(ValidAdapter): + def get_framework_name(self) -> str: + return " " + + result = _check_framework_name(WhitespaceAdapter()) + assert result.passed is False From b0d2ff0fb5271c156e4bb906dc49bd4100c39abe Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:13:36 +0800 Subject: [PATCH 29/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?=5Fcheck=5Fsupported=5Fversions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index 7c6fcb9..851cfc7 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -7,6 +7,7 @@ _check_abstract_methods_implemented, _check_framework_name, _check_inherits_framework_adapter, + _check_supported_versions, ) from agent_assembly.adapters.base import FrameworkAdapter @@ -94,3 +95,27 @@ def get_framework_name(self) -> str: result = _check_framework_name(WhitespaceAdapter()) assert result.passed is False + + +class TestCheckSupportedVersions: + """Tests for _check_supported_versions.""" + + def test_valid_list_passes(self, valid_adapter_cls: type) -> None: + result = _check_supported_versions(valid_adapter_cls()) + assert result.passed is True + + def test_empty_list_fails(self, empty_versions_adapter_cls: type) -> None: + result = _check_supported_versions(empty_versions_adapter_cls()) + assert result.passed is False + assert "non-empty list" in result.message + + def test_empty_string_in_list_fails(self) -> None: + from test.unit.cli.conftest import ValidAdapter + + class EmptyStringVersionAdapter(ValidAdapter): + def get_supported_versions(self) -> list[str]: + return [">=1.0.0", ""] + + result = _check_supported_versions(EmptyStringVersionAdapter()) + assert result.passed is False + assert "index 1" in result.message From 9710508e29b09ca905623027c07873b07b48121e Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:14:38 +0800 Subject: [PATCH 30/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?=5Fcheck=5Fregister=5Fhooks=5Fsignature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix string annotation comparison when from __future__ import annotations is active. Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 7 ++++++- test/unit/cli/test_adapter_validator.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 2ff6520..71e1bbe 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -132,7 +132,12 @@ def _check_register_hooks_signature(cls: type) -> AdapterValidationResult: ) first_param = params[0] annotation = first_param.annotation - if annotation is inspect.Parameter.empty or annotation is GovernanceInterceptor: + acceptable = ( + annotation is inspect.Parameter.empty + or annotation is GovernanceInterceptor + or (isinstance(annotation, str) and "GovernanceInterceptor" in annotation) + ) + if acceptable: return AdapterValidationResult( check_name="register_hooks_signature", passed=True, diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index 851cfc7..67014a6 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -7,6 +7,7 @@ _check_abstract_methods_implemented, _check_framework_name, _check_inherits_framework_adapter, + _check_register_hooks_signature, _check_supported_versions, ) from agent_assembly.adapters.base import FrameworkAdapter @@ -119,3 +120,22 @@ def get_supported_versions(self) -> list[str]: result = _check_supported_versions(EmptyStringVersionAdapter()) assert result.passed is False assert "index 1" in result.message + + +class TestCheckRegisterHooksSignature: + """Tests for _check_register_hooks_signature.""" + + def test_correct_signature_passes(self, valid_adapter_cls: type) -> None: + result = _check_register_hooks_signature(valid_adapter_cls) + assert result.passed is True + + def test_missing_param_fails(self) -> None: + from test.unit.cli.conftest import ValidAdapter + + class NoParamAdapter(ValidAdapter): + def register_hooks(self) -> None: # type: ignore[override] + pass + + result = _check_register_hooks_signature(NoParamAdapter) + assert result.passed is False + assert "must accept" in result.message From 963bf3c37ead7215fc489f64a892f4559902f846 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:15:33 +0800 Subject: [PATCH 31/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?=5Fcheck=5Funregister=5Fhooks=5Fidempotent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index 67014a6..540ad9f 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -9,6 +9,7 @@ _check_inherits_framework_adapter, _check_register_hooks_signature, _check_supported_versions, + _check_unregister_hooks_idempotent, ) from agent_assembly.adapters.base import FrameworkAdapter @@ -139,3 +140,18 @@ def register_hooks(self) -> None: # type: ignore[override] result = _check_register_hooks_signature(NoParamAdapter) assert result.passed is False assert "must accept" in result.message + + +class TestCheckUnregisterHooksIdempotent: + """Tests for _check_unregister_hooks_idempotent.""" + + def test_double_call_no_raise_passes(self, valid_adapter_cls: type) -> None: + result = _check_unregister_hooks_idempotent(valid_adapter_cls()) + assert result.passed is True + + def test_raises_on_second_call_fails( + self, non_idempotent_adapter_cls: type + ) -> None: + result = _check_unregister_hooks_idempotent(non_idempotent_adapter_cls()) + assert result.passed is False + assert "not idempotent" in result.message From 9cdeaa2197af685e50b8bd9d7995a1e80f3e5f48 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:16:05 +0800 Subject: [PATCH 32/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?=5Fcheck=5Fentry=5Fpoint=5Fmetadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index 540ad9f..a130944 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -9,6 +9,7 @@ _check_inherits_framework_adapter, _check_register_hooks_signature, _check_supported_versions, + _check_entry_point_metadata, _check_unregister_hooks_idempotent, ) from agent_assembly.adapters.base import FrameworkAdapter @@ -155,3 +156,44 @@ def test_raises_on_second_call_fails( result = _check_unregister_hooks_idempotent(non_idempotent_adapter_cls()) assert result.passed is False assert "not idempotent" in result.message + + +class TestCheckEntryPointMetadata: + """Tests for _check_entry_point_metadata.""" + + def test_valid_pyproject_passes( + self, valid_adapter_cls: type, tmp_path: object + ) -> None: + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + pyproject = tmp_path / "pyproject.toml" + qualname = f"{valid_adapter_cls.__module__}:{valid_adapter_cls.__qualname__}" + pyproject.write_text( + f'[project.entry-points."agent_assembly.adapters"]\n' + f'test_framework = "{qualname}"\n' + ) + result = _check_entry_point_metadata(valid_adapter_cls, str(tmp_path)) + assert result.passed is True + + def test_missing_entry_point_fails( + self, valid_adapter_cls: type, tmp_path: object + ) -> None: + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text("[project]\nname = 'test'\n") + result = _check_entry_point_metadata(valid_adapter_cls, str(tmp_path)) + assert result.passed is False + assert "missing" in result.message + + def test_no_pyproject_skips( + self, valid_adapter_cls: type, tmp_path: object + ) -> None: + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + result = _check_entry_point_metadata(valid_adapter_cls, str(tmp_path)) + assert result.passed is True + assert "skipping" in result.message.lower() From 29abbab5ebce8d1a58c1271095b53e153b836b38 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:17:06 +0800 Subject: [PATCH 33/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?validate=5Fadapter=20orchestrator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_adapter_validator.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index a130944..43a3a6d 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -11,6 +11,7 @@ _check_supported_versions, _check_entry_point_metadata, _check_unregister_hooks_idempotent, + validate_adapter, ) from agent_assembly.adapters.base import FrameworkAdapter @@ -197,3 +198,32 @@ def test_no_pyproject_skips( result = _check_entry_point_metadata(valid_adapter_cls, str(tmp_path)) assert result.passed is True assert "skipping" in result.message.lower() + + +class TestValidateAdapter: + """Tests for validate_adapter orchestrator.""" + + def test_all_pass_for_valid_adapter(self, valid_adapter_cls: type) -> None: + results = validate_adapter(valid_adapter_cls, "test.module") + assert all(r.passed for r in results) + + def test_mixed_fail_for_empty_name( + self, empty_name_adapter_cls: type + ) -> None: + results = validate_adapter(empty_name_adapter_cls, "test.module") + failed = [r for r in results if not r.passed] + assert len(failed) >= 1 + assert any(r.check_name == "framework_name" for r in failed) + + def test_short_circuits_on_inheritance_failure( + self, not_an_adapter_cls: type + ) -> None: + results = validate_adapter(not_an_adapter_cls, "test.module") + assert len(results) == 2 + assert not results[0].passed + + def test_result_count_for_valid_adapter( + self, valid_adapter_cls: type + ) -> None: + results = validate_adapter(valid_adapter_cls, "test.module") + assert len(results) == 7 From b4dd5e09e32d7774be5db00c84f388ac2fc2c922 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:18:00 +0800 Subject: [PATCH 34/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?load=5Fadapter=5Fclass=5Ffrom=5Fmodule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_loader.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/unit/cli/test_loader.py diff --git a/test/unit/cli/test_loader.py b/test/unit/cli/test_loader.py new file mode 100644 index 0000000..fd0fb76 --- /dev/null +++ b/test/unit/cli/test_loader.py @@ -0,0 +1,27 @@ +"""Unit tests for adapter class loader functions.""" + +from __future__ import annotations + +import pytest + +from agent_assembly.cli.adapter_validator import load_adapter_class_from_module + + +class TestLoadAdapterClassFromModule: + """Tests for load_adapter_class_from_module.""" + + def test_valid_module(self) -> None: + cls = load_adapter_class_from_module( + "agent_assembly.adapters.langchain.adapter" + ) + from agent_assembly.adapters.base import FrameworkAdapter + + assert issubclass(cls, FrameworkAdapter) + + def test_invalid_module_raises(self) -> None: + with pytest.raises(ImportError): + load_adapter_class_from_module("nonexistent.module.path") + + def test_module_with_no_adapter_raises(self) -> None: + with pytest.raises(ValueError, match="No FrameworkAdapter subclass"): + load_adapter_class_from_module("agent_assembly.exceptions") From c3594be1d9d2ad2a50c4c60a3777454b5efc93d8 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:18:32 +0800 Subject: [PATCH 35/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?load=5Fadapter=5Fclass=5Ffrom=5Fpath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_loader.py | 45 +++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test/unit/cli/test_loader.py b/test/unit/cli/test_loader.py index fd0fb76..e89d31f 100644 --- a/test/unit/cli/test_loader.py +++ b/test/unit/cli/test_loader.py @@ -4,7 +4,10 @@ import pytest -from agent_assembly.cli.adapter_validator import load_adapter_class_from_module +from agent_assembly.cli.adapter_validator import ( + load_adapter_class_from_module, + load_adapter_class_from_path, +) class TestLoadAdapterClassFromModule: @@ -25,3 +28,43 @@ def test_invalid_module_raises(self) -> None: def test_module_with_no_adapter_raises(self) -> None: with pytest.raises(ValueError, match="No FrameworkAdapter subclass"): load_adapter_class_from_module("agent_assembly.exceptions") + + +class TestLoadAdapterClassFromPath: + """Tests for load_adapter_class_from_path.""" + + def test_valid_file_path(self, tmp_path: object) -> None: + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + adapter_file = tmp_path / "my_adapter.py" + adapter_file.write_text( + "from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor\n" + "\n" + "class MyAdapter(FrameworkAdapter):\n" + " def get_framework_name(self) -> str:\n" + " return 'my_framework'\n" + " def get_supported_versions(self) -> list[str]:\n" + " return ['>=1.0.0']\n" + " def register_hooks(self, interceptor: GovernanceInterceptor) -> None:\n" + " pass\n" + " def unregister_hooks(self) -> None:\n" + " pass\n" + ) + cls = load_adapter_class_from_path(str(adapter_file)) + from agent_assembly.adapters.base import FrameworkAdapter + + assert issubclass(cls, FrameworkAdapter) + + def test_invalid_path_raises(self) -> None: + with pytest.raises(FileNotFoundError): + load_adapter_class_from_path("/nonexistent/path/adapter.py") + + def test_file_with_no_adapter_raises(self, tmp_path: object) -> None: + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + empty_file = tmp_path / "empty.py" + empty_file.write_text("x = 1\n") + with pytest.raises(ValueError, match="No FrameworkAdapter subclass"): + load_adapter_class_from_path(str(empty_file)) From 236acac14880c18415ff828743ea05081479abb4 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:24:02 +0800 Subject: [PATCH 36/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?load=5Fadapter=5Fclass=20dispatcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_loader.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/unit/cli/test_loader.py b/test/unit/cli/test_loader.py index e89d31f..34457f6 100644 --- a/test/unit/cli/test_loader.py +++ b/test/unit/cli/test_loader.py @@ -5,6 +5,7 @@ import pytest from agent_assembly.cli.adapter_validator import ( + load_adapter_class, load_adapter_class_from_module, load_adapter_class_from_path, ) @@ -68,3 +69,36 @@ def test_file_with_no_adapter_raises(self, tmp_path: object) -> None: empty_file.write_text("x = 1\n") with pytest.raises(ValueError, match="No FrameworkAdapter subclass"): load_adapter_class_from_path(str(empty_file)) + + +class TestLoadAdapterClass: + """Tests for load_adapter_class dispatcher.""" + + def test_dispatches_to_path_for_existing_file( + self, tmp_path: object + ) -> None: + import pathlib + + assert isinstance(tmp_path, pathlib.Path) + adapter_file = tmp_path / "my_adapter.py" + adapter_file.write_text( + "from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor\n" + "\n" + "class MyAdapter(FrameworkAdapter):\n" + " def get_framework_name(self) -> str:\n" + " return 'my_framework'\n" + " def get_supported_versions(self) -> list[str]:\n" + " return ['>=1.0.0']\n" + " def register_hooks(self, interceptor: GovernanceInterceptor) -> None:\n" + " pass\n" + " def unregister_hooks(self) -> None:\n" + " pass\n" + ) + cls = load_adapter_class(str(adapter_file)) + assert cls.__name__ == "MyAdapter" + + def test_dispatches_to_module_for_dotted_name(self) -> None: + cls = load_adapter_class("agent_assembly.adapters.langchain.adapter") + from agent_assembly.adapters.base import FrameworkAdapter + + assert issubclass(cls, FrameworkAdapter) From 5379acd7eb9cc86d968d32015ab7c03c6150b772 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:24:24 +0800 Subject: [PATCH 37/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?format=5Fresults=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_output.py | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/unit/cli/test_output.py diff --git a/test/unit/cli/test_output.py b/test/unit/cli/test_output.py new file mode 100644 index 0000000..9d90289 --- /dev/null +++ b/test/unit/cli/test_output.py @@ -0,0 +1,39 @@ +"""Unit tests for output formatting.""" + +from __future__ import annotations + +from agent_assembly.cli.adapter_validator import AdapterValidationResult +from agent_assembly.cli.output import format_results + + +class TestFormatResults: + """Tests for format_results.""" + + def test_all_pass_output(self) -> None: + results = [ + AdapterValidationResult(check_name="check_a", passed=True, message="ok"), + AdapterValidationResult(check_name="check_b", passed=True, message="ok"), + ] + output = format_results(results) + assert "[PASS]" in output + assert "[FAIL]" not in output + assert "2 passed, 0 failed" in output + + def test_mixed_output(self) -> None: + results = [ + AdapterValidationResult(check_name="check_a", passed=True, message="ok"), + AdapterValidationResult( + check_name="check_b", passed=False, message="bad" + ), + ] + output = format_results(results) + assert "[PASS]" in output + assert "[FAIL]" in output + assert "1 passed, 1 failed" in output + + def test_pass_fail_prefix_format(self) -> None: + results = [ + AdapterValidationResult(check_name="my_check", passed=True, message="m"), + ] + output = format_results(results) + assert " [PASS] my_check: m" in output From 88a13a7b141a9ba28265eae9e5a5652390d8d865 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:25:30 +0800 Subject: [PATCH 38/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?CLI=20main=20exit=20code=200=20on=20valid=20adapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_cli_main.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/unit/cli/test_cli_main.py diff --git a/test/unit/cli/test_cli_main.py b/test/unit/cli/test_cli_main.py new file mode 100644 index 0000000..cc27760 --- /dev/null +++ b/test/unit/cli/test_cli_main.py @@ -0,0 +1,22 @@ +"""Unit tests for CLI main entry point.""" + +from __future__ import annotations + +from unittest import mock + +import pytest + +from agent_assembly.cli.main import main + + +class TestCliMainExitCodeZero: + """Tests for CLI main returning exit code 0.""" + + def test_valid_adapter_exits_zero(self) -> None: + with mock.patch( + "sys.argv", + ["aasm", "adapter", "validate", "agent_assembly.adapters.langchain.adapter"], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 From fcb8f929b3a354dacd8b4a22cba4d316ac0c320c Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:26:00 +0800 Subject: [PATCH 39/41] =?UTF-8?q?=E2=9C=85=20(cli):=20Add=20tests=20for=20?= =?UTF-8?q?CLI=20main=20exit=20code=201=20on=20invalid=20adapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test/unit/cli/test_cli_main.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/unit/cli/test_cli_main.py b/test/unit/cli/test_cli_main.py index cc27760..bcd5df9 100644 --- a/test/unit/cli/test_cli_main.py +++ b/test/unit/cli/test_cli_main.py @@ -20,3 +20,22 @@ def test_valid_adapter_exits_zero(self) -> None: with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 0 + + +class TestCliMainExitCodeOne: + """Tests for CLI main returning exit code 1.""" + + def test_nonexistent_module_exits_one(self) -> None: + with mock.patch( + "sys.argv", + ["aasm", "adapter", "validate", "nonexistent.module"], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 + + def test_no_command_exits_one(self) -> None: + with mock.patch("sys.argv", ["aasm"]): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 From 5f69bd47457706f204c93a18803101862c770610 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:28:11 +0800 Subject: [PATCH 40/41] =?UTF-8?q?=F0=9F=9A=A8=20(cli):=20Fix=20import=20or?= =?UTF-8?q?dering=20and=20mypy=20type=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 8 +++++--- test/unit/cli/test_adapter_validator.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 71e1bbe..6c9c892 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -5,11 +5,12 @@ import importlib import importlib.util import inspect -import tomllib from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING +import tomllib + if TYPE_CHECKING: pass @@ -52,7 +53,7 @@ def _check_inherits_framework_adapter(cls: type) -> AdapterValidationResult: def _check_abstract_methods_implemented(cls: type) -> AdapterValidationResult: """Check that all 4 required abstract methods are concretely implemented.""" - remaining = getattr(cls, "__abstractmethods__", frozenset()) + remaining: frozenset[str] = getattr(cls, "__abstractmethods__", frozenset()) missing = _REQUIRED_ABSTRACT_METHODS & remaining if not missing: return AdapterValidationResult( @@ -122,7 +123,8 @@ def _check_supported_versions(instance: FrameworkAdapter) -> AdapterValidationRe def _check_register_hooks_signature(cls: type) -> AdapterValidationResult: """Check that register_hooks accepts a GovernanceInterceptor argument.""" - sig = inspect.signature(cls.register_hooks) + register_hooks = getattr(cls, "register_hooks") + sig = inspect.signature(register_hooks) params = [p for name, p in sig.parameters.items() if name != "self"] if not params: return AdapterValidationResult( diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index 43a3a6d..16c0595 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -2,18 +2,18 @@ from __future__ import annotations +from agent_assembly.adapters.base import FrameworkAdapter from agent_assembly.cli.adapter_validator import ( AdapterValidationResult, _check_abstract_methods_implemented, + _check_entry_point_metadata, _check_framework_name, _check_inherits_framework_adapter, _check_register_hooks_signature, _check_supported_versions, - _check_entry_point_metadata, _check_unregister_hooks_idempotent, validate_adapter, ) -from agent_assembly.adapters.base import FrameworkAdapter class TestAdapterValidationResult: From a02347a351e373649e3f9c2f156f99147abed16c Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 18:28:45 +0800 Subject: [PATCH 41/41] =?UTF-8?q?=F0=9F=9A=A8=20(cli):=20Apply=20pre-commi?= =?UTF-8?q?t=20formatting=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent_assembly/cli/adapter_validator.py | 43 ++++++------------------- agent_assembly/cli/main.py | 13 ++++---- test/unit/cli/conftest.py | 2 -- test/unit/cli/test_adapter_validator.py | 41 ++++++----------------- test/unit/cli/test_loader.py | 8 ++--- test/unit/cli/test_output.py | 4 +-- 6 files changed, 29 insertions(+), 82 deletions(-) diff --git a/agent_assembly/cli/adapter_validator.py b/agent_assembly/cli/adapter_validator.py index 6c9c892..3ca1174 100644 --- a/agent_assembly/cli/adapter_validator.py +++ b/agent_assembly/cli/adapter_validator.py @@ -5,12 +5,11 @@ import importlib import importlib.util import inspect +import tomllib from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING -import tomllib - if TYPE_CHECKING: pass @@ -148,10 +147,7 @@ def _check_register_hooks_signature(cls: type) -> AdapterValidationResult: return AdapterValidationResult( check_name="register_hooks_signature", passed=False, - message=( - f"register_hooks() first parameter annotated as {annotation}, " - f"expected GovernanceInterceptor." - ), + message=(f"register_hooks() first parameter annotated as {annotation}, " f"expected GovernanceInterceptor."), ) @@ -166,10 +162,7 @@ def _check_unregister_hooks_idempotent( return AdapterValidationResult( check_name="unregister_hooks_idempotent", passed=False, - message=( - f"unregister_hooks() is not idempotent: " - f"second call raised {type(exc).__name__}: {exc}" - ), + message=(f"unregister_hooks() is not idempotent: " f"second call raised {type(exc).__name__}: {exc}"), ) return AdapterValidationResult( check_name="unregister_hooks_idempotent", @@ -178,9 +171,7 @@ def _check_unregister_hooks_idempotent( ) -def _check_entry_point_metadata( - cls: type, path_or_module: str -) -> AdapterValidationResult: +def _check_entry_point_metadata(cls: type, path_or_module: str) -> AdapterValidationResult: """Check entry point metadata in pyproject.toml if present at the given path.""" search_path = Path(path_or_module) if search_path.is_file(): @@ -204,17 +195,12 @@ def _check_entry_point_metadata( message=f"Failed to parse pyproject.toml: {exc}", ) - entry_points = ( - data.get("project", {}).get("entry-points", {}).get("agent_assembly.adapters", {}) - ) + entry_points = data.get("project", {}).get("entry-points", {}).get("agent_assembly.adapters", {}) if not entry_points: return AdapterValidationResult( check_name="entry_point_metadata", passed=False, - message=( - "pyproject.toml missing [project.entry-points.\"agent_assembly.adapters\"] " - "section." - ), + message=('pyproject.toml missing [project.entry-points."agent_assembly.adapters"] ' "section."), ) class_qualname = f"{cls.__module__}:{cls.__qualname__}" @@ -229,16 +215,11 @@ def _check_entry_point_metadata( return AdapterValidationResult( check_name="entry_point_metadata", passed=False, - message=( - f"No entry point references {class_qualname}. " - f"Found: {entry_points}." - ), + message=(f"No entry point references {class_qualname}. " f"Found: {entry_points}."), ) -def validate_adapter( - cls: type, path_or_module: str -) -> list[AdapterValidationResult]: +def validate_adapter(cls: type, path_or_module: str) -> list[AdapterValidationResult]: """Run all contract checks against an adapter class and return results.""" results: list[AdapterValidationResult] = [] @@ -277,9 +258,7 @@ def load_adapter_class_from_module(module_name: str) -> type: module = importlib.import_module(module_name) cls = _find_adapter_class_in_module(module) if cls is None: - raise ValueError( - f"No FrameworkAdapter subclass found in module '{module_name}'." - ) + raise ValueError(f"No FrameworkAdapter subclass found in module '{module_name}'.") return cls @@ -304,9 +283,7 @@ def load_adapter_class_from_path(file_path: str) -> type: cls = _find_adapter_class_in_module(module) if cls is None: - raise ValueError( - f"No FrameworkAdapter subclass found in '{path}'." - ) + raise ValueError(f"No FrameworkAdapter subclass found in '{path}'.") return cls diff --git a/agent_assembly/cli/main.py b/agent_assembly/cli/main.py index 5729941..2e2ac08 100644 --- a/agent_assembly/cli/main.py +++ b/agent_assembly/cli/main.py @@ -14,12 +14,8 @@ def _build_parser() -> argparse.ArgumentParser: ) subparsers = parser.add_subparsers(dest="command", help="Available commands") - adapter_parser = subparsers.add_parser( - "adapter", help="Adapter management commands" - ) - adapter_subparsers = adapter_parser.add_subparsers( - dest="adapter_command", help="Adapter subcommands" - ) + adapter_parser = subparsers.add_parser("adapter", help="Adapter management commands") + adapter_subparsers = adapter_parser.add_subparsers(dest="adapter_command", help="Adapter subcommands") validate_parser = adapter_subparsers.add_parser( "validate", help="Validate a community adapter against the FrameworkAdapter contract" @@ -34,7 +30,10 @@ def _build_parser() -> argparse.ArgumentParser: def _handle_adapter_validate(args: argparse.Namespace) -> int: """Run adapter validation and return an exit code.""" - from agent_assembly.cli.adapter_validator import load_adapter_class, validate_adapter + from agent_assembly.cli.adapter_validator import ( + load_adapter_class, + validate_adapter, + ) from agent_assembly.cli.output import format_results try: diff --git a/test/unit/cli/conftest.py b/test/unit/cli/conftest.py index 71bad07..7660b78 100644 --- a/test/unit/cli/conftest.py +++ b/test/unit/cli/conftest.py @@ -79,8 +79,6 @@ def unregister_hooks(self) -> None: class NotAnAdapter: """A class that does not inherit from FrameworkAdapter.""" - pass - @pytest.fixture() def valid_adapter_cls() -> type: diff --git a/test/unit/cli/test_adapter_validator.py b/test/unit/cli/test_adapter_validator.py index 16c0595..ecb596f 100644 --- a/test/unit/cli/test_adapter_validator.py +++ b/test/unit/cli/test_adapter_validator.py @@ -20,9 +20,7 @@ class TestAdapterValidationResult: """Tests for the AdapterValidationResult dataclass.""" def test_fields_stored(self) -> None: - result = AdapterValidationResult( - check_name="test_check", passed=True, message="ok" - ) + result = AdapterValidationResult(check_name="test_check", passed=True, message="ok") assert result.check_name == "test_check" assert result.passed is True assert result.message == "ok" @@ -35,9 +33,7 @@ def test_equality(self) -> None: def test_frozen(self) -> None: import pytest - result = AdapterValidationResult( - check_name="c", passed=True, message="m" - ) + result = AdapterValidationResult(check_name="c", passed=True, message="m") with pytest.raises(AttributeError): result.passed = False # type: ignore[misc] @@ -151,9 +147,7 @@ def test_double_call_no_raise_passes(self, valid_adapter_cls: type) -> None: result = _check_unregister_hooks_idempotent(valid_adapter_cls()) assert result.passed is True - def test_raises_on_second_call_fails( - self, non_idempotent_adapter_cls: type - ) -> None: + def test_raises_on_second_call_fails(self, non_idempotent_adapter_cls: type) -> None: result = _check_unregister_hooks_idempotent(non_idempotent_adapter_cls()) assert result.passed is False assert "not idempotent" in result.message @@ -162,24 +156,17 @@ def test_raises_on_second_call_fails( class TestCheckEntryPointMetadata: """Tests for _check_entry_point_metadata.""" - def test_valid_pyproject_passes( - self, valid_adapter_cls: type, tmp_path: object - ) -> None: + def test_valid_pyproject_passes(self, valid_adapter_cls: type, tmp_path: object) -> None: import pathlib assert isinstance(tmp_path, pathlib.Path) pyproject = tmp_path / "pyproject.toml" qualname = f"{valid_adapter_cls.__module__}:{valid_adapter_cls.__qualname__}" - pyproject.write_text( - f'[project.entry-points."agent_assembly.adapters"]\n' - f'test_framework = "{qualname}"\n' - ) + pyproject.write_text(f'[project.entry-points."agent_assembly.adapters"]\n' f'test_framework = "{qualname}"\n') result = _check_entry_point_metadata(valid_adapter_cls, str(tmp_path)) assert result.passed is True - def test_missing_entry_point_fails( - self, valid_adapter_cls: type, tmp_path: object - ) -> None: + def test_missing_entry_point_fails(self, valid_adapter_cls: type, tmp_path: object) -> None: import pathlib assert isinstance(tmp_path, pathlib.Path) @@ -189,9 +176,7 @@ def test_missing_entry_point_fails( assert result.passed is False assert "missing" in result.message - def test_no_pyproject_skips( - self, valid_adapter_cls: type, tmp_path: object - ) -> None: + def test_no_pyproject_skips(self, valid_adapter_cls: type, tmp_path: object) -> None: import pathlib assert isinstance(tmp_path, pathlib.Path) @@ -207,23 +192,17 @@ def test_all_pass_for_valid_adapter(self, valid_adapter_cls: type) -> None: results = validate_adapter(valid_adapter_cls, "test.module") assert all(r.passed for r in results) - def test_mixed_fail_for_empty_name( - self, empty_name_adapter_cls: type - ) -> None: + def test_mixed_fail_for_empty_name(self, empty_name_adapter_cls: type) -> None: results = validate_adapter(empty_name_adapter_cls, "test.module") failed = [r for r in results if not r.passed] assert len(failed) >= 1 assert any(r.check_name == "framework_name" for r in failed) - def test_short_circuits_on_inheritance_failure( - self, not_an_adapter_cls: type - ) -> None: + def test_short_circuits_on_inheritance_failure(self, not_an_adapter_cls: type) -> None: results = validate_adapter(not_an_adapter_cls, "test.module") assert len(results) == 2 assert not results[0].passed - def test_result_count_for_valid_adapter( - self, valid_adapter_cls: type - ) -> None: + def test_result_count_for_valid_adapter(self, valid_adapter_cls: type) -> None: results = validate_adapter(valid_adapter_cls, "test.module") assert len(results) == 7 diff --git a/test/unit/cli/test_loader.py b/test/unit/cli/test_loader.py index 34457f6..de22781 100644 --- a/test/unit/cli/test_loader.py +++ b/test/unit/cli/test_loader.py @@ -15,9 +15,7 @@ class TestLoadAdapterClassFromModule: """Tests for load_adapter_class_from_module.""" def test_valid_module(self) -> None: - cls = load_adapter_class_from_module( - "agent_assembly.adapters.langchain.adapter" - ) + cls = load_adapter_class_from_module("agent_assembly.adapters.langchain.adapter") from agent_assembly.adapters.base import FrameworkAdapter assert issubclass(cls, FrameworkAdapter) @@ -74,9 +72,7 @@ def test_file_with_no_adapter_raises(self, tmp_path: object) -> None: class TestLoadAdapterClass: """Tests for load_adapter_class dispatcher.""" - def test_dispatches_to_path_for_existing_file( - self, tmp_path: object - ) -> None: + def test_dispatches_to_path_for_existing_file(self, tmp_path: object) -> None: import pathlib assert isinstance(tmp_path, pathlib.Path) diff --git a/test/unit/cli/test_output.py b/test/unit/cli/test_output.py index 9d90289..8f847f8 100644 --- a/test/unit/cli/test_output.py +++ b/test/unit/cli/test_output.py @@ -22,9 +22,7 @@ def test_all_pass_output(self) -> None: def test_mixed_output(self) -> None: results = [ AdapterValidationResult(check_name="check_a", passed=True, message="ok"), - AdapterValidationResult( - check_name="check_b", passed=False, message="bad" - ), + AdapterValidationResult(check_name="check_b", passed=False, message="bad"), ] output = format_results(results) assert "[PASS]" in output