Files
CoreRasurae 8fa9e7652d
CI / quality (pull_request) Successful in 42s
CI / lint (pull_request) Successful in 46s
CI / security (pull_request) Successful in 47s
CI / build (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 1m4s
CI / integration_tests (pull_request) Successful in 1m9s
CI / unit_tests (pull_request) Successful in 3m43s
CI / coverage (pull_request) Successful in 3m42s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 59s
CI / typecheck (push) Successful in 58s
CI / quality (push) Successful in 1m5s
CI / security (push) Successful in 1m9s
CI / build (push) Successful in 51s
CI / integration_tests (push) Successful in 1m40s
CI / unit_tests (push) Successful in 3m53s
CI / coverage (push) Successful in 3m35s
CI / status-check (push) Successful in 3s
feat(registry): implement Canonicalizer with NFC normalization and SHA-1 hashing
Create Canonicalizer class implementing Package Registry Standard v1.0.0 §6:
- NFC normalization of all string values via unicodedata.normalize
- Lexicographic key sorting for deterministic output
- Lifecycle field stripping (version, release_date)
- RFC-8785 canonical JSON serialization (no whitespace, sorted keys)
- SHA-1 content-addressed hashing via compute_package_id()
- Internal reference resolution with configurable resolver callback

10 integration test cases covering:
- Complex content canonicalization with lifecycle stripping
- Deterministic output across multiple runs
- Key sorting enforcement
- Unicode NFC normalization
- PackageId computation for all 8 package types
- Different content produces different output

ISSUES CLOSED: #25
2026-06-10 11:33:50 +01:00

269 lines
11 KiB
Python

"""Robot Framework keyword library for Canonicalizer integration tests."""
from __future__ import annotations
import json
from typing import Any
from cleveractors.registry.canonical import Canonicalizer
from cleveractors.registry.types import PackageType
ROBOT_LIBRARY_SCOPE = "TEST SUITE" # pragma: no cover - integration test library
class CanonicalizerLib: # pragma: no cover - integration test library
"""Keyword library for Canonicalizer Robot Framework integration tests."""
def __init__(self) -> None:
self._canonicalizer: Canonicalizer = Canonicalizer()
self._result: str = ""
self._content: dict[str, Any] = {}
self._package_id_string: str = ""
self._package_id_string_2: str = ""
self._canonical_output_2: str = ""
self._content_original: dict[str, Any] = {}
def create_canonicalizer(self) -> None:
"""Create a fresh Canonicalizer instance for a test case."""
self._canonicalizer = Canonicalizer()
def set_content_from_yaml_dict(self, yaml_string: str) -> None:
"""Parse a YAML string into the internal content dict."""
import yaml
self._content = yaml.safe_load(yaml_string)
def set_complex_content(self) -> None:
"""Set content to a complex nested dict with lifecycle fields."""
self._content = {
"name": "complex-actor",
"description": "An actor with many properties",
"type": "llm",
"model": "gpt-4",
"temperature": 0.7,
"max_tokens": 4096,
"version": "2.1.0",
"release_date": "2026-06-07",
"metadata": {
"author": "Luis Mendes",
"tags": ["ai", "nlp", "conversation"],
"license": "MIT",
"version": "1.0.0",
},
"parameters": {
"top_p": 0.9,
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
},
}
def set_unicode_content(self) -> None:
"""Set content containing accented Unicode strings for NFC testing."""
self._content = {
"name": "unicode-test",
"description": "café résumé naïve garçon voilà",
"labels": ["café", "résumé", "naïve"],
"nested": {"key": "garçon voilà"},
}
def set_content_with_unsorted_keys(self) -> None:
"""Set content with intentionally unsorted keys for sorting tests."""
from collections import OrderedDict
self._content = OrderedDict(
[
("zulu", "last"),
("alpha", "first"),
("mike", "middle"),
("version", "should-be-stripped"),
]
)
def canonicalize_content(self) -> None:
"""Canonicalize the current content and store the result."""
self._result = self._canonicalizer.canonicalize(self._content)
def canonicalize_content_twice(self) -> None:
"""Canonicalize content twice to enable determinism assertions."""
first = self._canonicalizer.canonicalize(self._content)
second = self._canonicalizer.canonicalize(self._content)
self._result = first
self._canonical_output_2 = second
def compute_package_id_for_type(self, pkg_type: str) -> None:
"""Compute a PackageId for the current content by type name."""
pt = PackageType[pkg_type.upper()]
self._package_id_string = self._canonicalizer.compute_package_id(
self._content, pt
).id_string
def compute_package_id_twice_same_type(self, pkg_type: str) -> None:
"""Compute PackageId twice with same type to verify determinism."""
pt = PackageType[pkg_type.upper()]
pid1 = self._canonicalizer.compute_package_id(self._content, pt)
pid2 = self._canonicalizer.compute_package_id(self._content, pt)
self._package_id_string = str(pid1)
self._package_id_string_2 = str(pid2)
def compute_package_ids_for_types(self, type1: str, type2: str) -> None:
"""Compute PackageIds for two different package types."""
pt1 = PackageType[type1.upper()]
pt2 = PackageType[type2.upper()]
self._package_id_string = self._canonicalizer.compute_package_id(
self._content, pt1
).id_string
self._package_id_string_2 = self._canonicalizer.compute_package_id(
self._content, pt2
).id_string
def content_differs_by_one_field(self) -> None:
"""Save original content then modify the current content."""
self._content_original = dict(self._content)
self._content["modified"] = True
def canonicalize_both_contents(self) -> None:
"""Canonicalize both original and modified content for comparison."""
self._result = self._canonicalizer.canonicalize(self._content_original)
self._canonical_output_2 = self._canonicalizer.canonicalize(self._content)
# ── Assertions ──────────────────────────────────────────────────────
def result_is_valid_json(self) -> None:
"""Assert the canonicalization result parses as valid JSON dict."""
parsed = json.loads(self._result)
assert isinstance(parsed, dict), (
f"Expected dict from JSON, got {type(parsed).__name__}"
)
def result_has_key(self, key: str) -> None:
"""Assert the result JSON contains the given top-level key."""
parsed = json.loads(self._result)
assert key in parsed, f"Key {key!r} not found in result"
def result_does_not_have_key(self, key: str) -> None:
"""Assert the result JSON does NOT contain the given top-level key."""
parsed = json.loads(self._result)
assert key not in parsed, f"Key {key!r} should not be in result"
def result_keys_are_sorted(self) -> None:
"""Assert top-level keys in the result are lexicographically sorted."""
parsed = json.loads(self._result)
keys = list(parsed.keys())
assert keys == sorted(keys), f"Keys not sorted: {keys}"
def result_keys_are_sorted_recursively(self) -> None:
"""Assert all keys at every nesting level are lexicographically sorted."""
parsed = json.loads(self._result)
def _check_sorted(obj: Any, path: str) -> None:
if isinstance(obj, dict):
keys = list(obj.keys())
assert keys == sorted(keys), f"Keys not sorted at {path}: {keys}"
for k, v in obj.items():
_check_sorted(v, f"{path}.{k}")
elif isinstance(obj, list):
for i, item in enumerate(obj):
_check_sorted(item, f"{path}[{i}]")
_check_sorted(parsed, "root")
def canonicalization_is_deterministic(self) -> None:
"""Assert two canonicalization calls produced identical output."""
assert self._result == self._canonical_output_2, (
f"Canonicalization not deterministic:\n{self._result!r}\n!=\n{self._canonical_output_2!r}"
)
def canonicalization_produces_different_output(self) -> None:
"""Assert canonicalization of different content produces different output."""
assert self._result != self._canonical_output_2, (
"Different content should produce different canonical output"
)
def package_id_starts_with(self, prefix: str) -> None:
"""Assert the PackageId string starts with the expected type prefix."""
assert self._package_id_string.startswith(prefix), (
f"PackageId {self._package_id_string!r} should start with {prefix!r}"
)
def package_ids_are_identical(self) -> None:
"""Assert two computed PackageIds are identical."""
assert self._package_id_string == self._package_id_string_2, (
f"PackageIds should be identical: {self._package_id_string} != {self._package_id_string_2}"
)
def package_ids_are_different(self) -> None:
"""Assert two computed PackageIds differ."""
assert self._package_id_string != self._package_id_string_2, (
f"PackageIds should differ: {self._package_id_string} == {self._package_id_string_2}"
)
def package_id_is_40_hex_chars(self) -> None:
"""Assert the PackageId SHA-1 portion is 40 lowercase hex characters."""
sha1 = self._package_id_string.rsplit("_", 1)[-1]
assert len(sha1) == 40, f"SHA1 should be 40 chars: {len(sha1)}"
assert all(c in "0123456789abcdef" for c in sha1), (
f"SHA1 should be hex: {sha1!r}"
)
def result_contains_nfc_normalized_text(self, expected: str) -> None:
"""Assert the expected NFC-normalized text exists in the result and all strings are NFC."""
import unicodedata
parsed = json.loads(self._result)
def _collect_strings(obj: Any) -> list[str]:
strings: list[str] = []
if isinstance(obj, str):
strings.append(obj)
elif isinstance(obj, dict):
for v in obj.values():
strings.extend(_collect_strings(v))
elif isinstance(obj, list):
for item in obj:
strings.extend(_collect_strings(item))
return strings
all_strings = _collect_strings(parsed)
nfc_expected = unicodedata.normalize("NFC", expected)
assert nfc_expected in all_strings, (
f"Expected text {nfc_expected!r} not found in strings: {all_strings!r}"
)
for s in all_strings:
assert s == unicodedata.normalize("NFC", s), (
f"String not NFC-normalized: {s!r}"
)
def result_does_not_contain_lifecycle_field(self, field: str) -> None:
"""Assert the given lifecycle field is not present at any nesting level."""
parsed = json.loads(self._result)
def _has_field(obj: Any, target: str) -> bool:
if isinstance(obj, dict):
if target in obj:
return True
return any(_has_field(v, target) for v in obj.values())
if isinstance(obj, list):
return any(_has_field(item, target) for item in obj)
return False
assert not _has_field(parsed, field), (
f"Lifecycle field {field!r} found in canonical output"
)
def result_has_string_value(self, key: str, expected: str) -> None:
"""Assert the result contains the given key with the expected string value."""
parsed = json.loads(self._result)
def _find(obj: Any, target: str) -> Any:
if isinstance(obj, dict):
if target in obj:
return obj[target]
for v in obj.values():
found = _find(v, target)
if found is not None:
return found
return None
actual = _find(parsed, key)
assert actual == expected, f"Key {key!r}: expected {expected!r}, got {actual!r}"