diff --git a/schemas/aep/evidence-envelope.schema.json b/schemas/aep/evidence-envelope.schema.json index 227648e..5e8112a 100644 --- a/schemas/aep/evidence-envelope.schema.json +++ b/schemas/aep/evidence-envelope.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://wasmagent.dev/schemas/aep/evidence-envelope.schema.json", "title": "EvidenceEnvelope", - "description": "Shared base fields for all AEP evidence records \u2014 schema_version, trace_id, created_at_ms, and optional signature. Every evidence type $refs this schema for its envelope fields.", + "description": "Shared base fields for all AEP evidence records — schema_version, trace_id, created_at_ms, and optional signature. Every evidence type $refs this schema for its envelope fields.", "type": "object", "required": [ "schema_version", @@ -23,27 +23,18 @@ }, "signature": { "type": "object", - "description": "Optional tamper-evident signature over the record content", "required": [ "alg", "key_id", "sig" ], "properties": { - "alg": { - "type": "string", - "description": "Signature algorithm, e.g. ed25519" - }, - "key_id": { - "type": "string", - "description": "Identifier of the signing key" - }, - "sig": { - "type": "string", - "description": "Base64-encoded signature bytes" - } - }, - "additionalProperties": false + "alg": { "type": "string" }, + "key_id": { "type": "string" }, + "sig": { "type": "string" }, + "bundle": { "type": "object" }, + "transparency_log_ref": { "type": "string" } + } } } } diff --git a/src/wasmagent_protocol/__init__.py b/src/wasmagent_protocol/__init__.py index c67435a..6c8d41d 100644 --- a/src/wasmagent_protocol/__init__.py +++ b/src/wasmagent_protocol/__init__.py @@ -24,10 +24,17 @@ def _read(rel_path: str) -> str: # rel_path is e.g. "index.json" or "aep/aep-record.schema.json" - resource = resources.files(_SCHEMAS_PKG) - for part in rel_path.split("/"): - resource = resource / part - return resource.read_text(encoding="utf-8") + try: + resource = resources.files(_SCHEMAS_PKG) + for part in rel_path.split("/"): + resource = resource / part + return resource.read_text(encoding="utf-8") + except (ModuleNotFoundError, TypeError, FileNotFoundError): + schemas_dir = Path(__file__).resolve().parent.parent.parent / "schemas" + p = schemas_dir + for part in rel_path.split("/"): + p = p / part + return p.read_text(encoding="utf-8") INDEX: dict[str, Any] = json.loads(_read("index.json")) @@ -69,8 +76,15 @@ def schema_path(schema_id: str) -> Path: raise KeyError( f"unknown schema id {schema_id!r}; known: {', '.join(_PATHS)}" ) from None - resource = resources.files(_SCHEMAS_PKG) - for part in rel.split("/"): - resource = resource / part - with resources.as_file(resource) as p: - return Path(p) + try: + resource = resources.files(_SCHEMAS_PKG) + for part in rel.split("/"): + resource = resource / part + with resources.as_file(resource) as p: + return Path(p) + except (ModuleNotFoundError, TypeError, FileNotFoundError): + schemas_dir = Path(__file__).resolve().parent.parent.parent / "schemas" + p = schemas_dir + for part in rel.split("/"): + p = p / part + return p diff --git a/tests/test_conformance.py b/tests/test_conformance.py new file mode 100644 index 0000000..f0c15a3 --- /dev/null +++ b/tests/test_conformance.py @@ -0,0 +1,53 @@ +"""Pytest entry for the conformance harness. + +`tests/conformance.py` is a runnable script (`python3 tests/conformance.py`) +with a ``main()`` but no ``test_*`` functions, so ``pytest tests/`` collects +nothing from it directly. This module surfaces that harness as a single pytest +test so the bot/CI pytest gate actually exercises the schema + fixture +invariants — the same checks documented in CLAUDE.md. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +# tests/ is not a package; make conformance.py importable whether pytest runs +# from the repo root or collects this file directly. +_HERE = Path(__file__).resolve().parent +if str(_HERE) not in sys.path: + sys.path.insert(0, str(_HERE)) + +import conformance # noqa: E402 + + +def test_conformance_harness() -> None: + """Every registered schema is well-formed, $refs resolve, and fixtures + pass/fail as expected. Mirrors ``python3 tests/conformance.py``'s exit code. + """ + conformance.errors.clear() + rc = conformance.main() + assert rc == 0, ( + "conformance harness reported failures:\n" + + "\n".join(f" - {e}" for e in conformance.errors) + ) + + +def test_evidence_envelope_schema() -> None: + """Verify evidence-envelope schema registration, canonical $id, and signature shape.""" + import wasmagent_protocol + + assert "evidence-envelope" in wasmagent_protocol.schema_ids() + envelope = wasmagent_protocol.get_schema("evidence-envelope") + assert envelope["$id"] == "https://wasmagent.dev/schemas/aep/evidence-envelope.schema.json" + assert envelope["title"] == "EvidenceEnvelope" + assert set(envelope["required"]) == {"schema_version", "created_at_ms"} + + # Signature field properties must match aep-record signature fields + aep_record = wasmagent_protocol.get_schema("aep-record") + env_sig = envelope["properties"]["signature"] + aep_sig = aep_record["properties"]["signature"] + + assert set(env_sig["required"]) == set(aep_sig["required"]) + assert set(env_sig["properties"].keys()) == set(aep_sig["properties"].keys()) + assert env_sig["properties"]["bundle"] == aep_sig["properties"]["bundle"] +