2026-10-08 00:24:43 +02:00
|
|
|
import json
|
|
|
|
|
import socket
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from mas_conformance import address_fixtures, property_fixtures
|
|
|
|
|
from mas_conformance.runner import check_address_fixture, run_parser_corpus
|
|
|
|
|
from mas_core.generator import generate_local_part, resource_local_part
|
|
|
|
|
from mas_core.model import RequiredProfile
|
|
|
|
|
from mas_core.parser import (
|
|
|
|
|
MasSyntaxError,
|
|
|
|
|
parse_address,
|
|
|
|
|
parse_local_part,
|
2026-10-18 18:16:59 +02:00
|
|
|
parse_received_address,
|
2026-10-08 00:24:43 +02:00
|
|
|
validate_profile,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
FIXTURES = address_fixtures() + property_fixtures()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_vendored_corpus_matches_spec_bundle():
|
2026-10-19 13:35:10 +02:00
|
|
|
spec = json.loads((ROOT / "mas-v0.2-spec/conformance/addresses.json").read_text())
|
2026-10-08 00:24:43 +02:00
|
|
|
assert spec == address_fixtures()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_vendored_schema_matches_spec_bundle():
|
|
|
|
|
a = (ROOT / "mas-v0.1-spec/schema/mas-statute-v0.1.schema.json").read_bytes()
|
|
|
|
|
b = (ROOT / "mas_core/schema/mas-statute-v0.1.schema.json").read_bytes()
|
|
|
|
|
assert a == b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("fixture", FIXTURES, ids=[f["id"] for f in FIXTURES])
|
|
|
|
|
def test_fixture(fixture):
|
|
|
|
|
assert check_address_fixture(fixture) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_whole_corpus_passes():
|
|
|
|
|
assert run_parser_corpus().ok
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def no_network(monkeypatch):
|
|
|
|
|
def deny(*a, **k):
|
|
|
|
|
raise AssertionError("parser attempted network I/O")
|
|
|
|
|
monkeypatch.setattr(socket, "socket", deny)
|
|
|
|
|
monkeypatch.setattr(socket, "create_connection", deny)
|
|
|
|
|
monkeypatch.setattr(socket, "getaddrinfo", deny)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parser_performs_no_io(no_network, monkeypatch):
|
|
|
|
|
monkeypatch.setattr("builtins.open", lambda *a, **k: (_ for _ in ()).throw(AssertionError("file I/O")))
|
|
|
|
|
for f in FIXTURES:
|
|
|
|
|
try:
|
|
|
|
|
parse_local_part(f["local_part"])
|
|
|
|
|
except MasSyntaxError:
|
|
|
|
|
pass
|
|
|
|
|
parse_address("anna.mueller@acme.coulomb.social")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_maximal_parser_reports_required_profile():
|
|
|
|
|
p = parse_local_part("ticket!callback")
|
|
|
|
|
assert p.required_profile is RequiredProfile.P2
|
|
|
|
|
assert not validate_profile(p, "p1")
|
|
|
|
|
assert validate_profile(p, "P2")
|
|
|
|
|
assert parse_local_part("ticket=PR42+comments").required_profile is RequiredProfile.P1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_combined_form_model():
|
|
|
|
|
p = parse_local_part("ticket=PR42+comments!callback")
|
|
|
|
|
assert p.target.resource_type == "ticket" and p.target.resource_id == "PR42"
|
|
|
|
|
assert p.facet.value == "comments" and p.interaction.mode == "callback"
|
|
|
|
|
assert p.required_profile is RequiredProfile.P2
|
|
|
|
|
assert not p.generator_allowed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_kind_inference_from_punctuation():
|
|
|
|
|
for lp in ("bernd.worsch", "research-companion", "invoice_ingest"):
|
|
|
|
|
d = parse_local_part(lp).to_dict()
|
|
|
|
|
assert d["target_kind"] == "opaque"
|
|
|
|
|
assert not {"kind", "person", "actor", "function"} & set(d)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_facet_loss_degrades_to_base():
|
|
|
|
|
p = parse_local_part("ticket=GS-41872+comments")
|
|
|
|
|
assert p.base == "ticket=GS-41872"
|
|
|
|
|
stripped = p.without_facet()
|
|
|
|
|
assert stripped.raw == "ticket=GS-41872" and stripped.target == p.target
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_address_domain():
|
|
|
|
|
a = parse_address("ticket=GS-41872@Great-Service.Example")
|
|
|
|
|
assert a.domain == "great-service.example"
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
parse_address("no-domain")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_generator_rules():
|
|
|
|
|
assert generate_local_part("ticket", interaction="callback") == "ticket!callback"
|
|
|
|
|
assert resource_local_part("ticket", "GS-41872") == "ticket=GS-41872"
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
generate_local_part("ticket=PR42", facet="comments", interaction="callback")
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
generate_local_part("ticket", facet="a+b")
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
generate_local_part("ticket", facet="")
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
resource_local_part("ticket", "x" * 60)
|
|
|
|
|
for f in FIXTURES:
|
|
|
|
|
if f["valid"] and f.get("generator_allowed") is False:
|
|
|
|
|
p = parse_local_part(f["local_part"])
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
generate_local_part(str(p.target), facet=p.facet.value, interaction=p.interaction.mode)
|
2026-10-18 18:16:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("raw, expected", [
|
|
|
|
|
('"ticket=PR42"@example.com', "ticket=PR42"), # SCP-1: unnecessary quotes
|
|
|
|
|
('"ticket!callback"@example.com', "ticket!callback"),
|
|
|
|
|
("ticket=PR42@example.com", "ticket=PR42"),
|
|
|
|
|
])
|
|
|
|
|
def test_received_quoted_local_part_is_equivalent(raw, expected):
|
|
|
|
|
assert parse_received_address(raw).local.raw == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("raw", ['"a\\"b"@example.com', '"a b"@example.com', '"x@y"@example.com'])
|
|
|
|
|
def test_received_quoted_non_mas_stays_invalid(raw):
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
parse_received_address(raw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_canonical_parser_still_rejects_quotes():
|
|
|
|
|
with pytest.raises(MasSyntaxError):
|
|
|
|
|
parse_address('"ticket=PR42"@example.com')
|
2026-10-18 22:19:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_vendored_v02_schema_matches_draft_bundle():
|
|
|
|
|
a = (ROOT / "mas-v0.2-spec/schema/mas-statute-v0.2.schema.json").read_bytes()
|
|
|
|
|
b = (ROOT / "mas_core/schema/mas-statute-v0.2.schema.json").read_bytes()
|
|
|
|
|
assert a == b
|
2026-10-19 09:31:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_vendored_v02_statute_aware_corpus_matches_draft_bundle():
|
|
|
|
|
a = (ROOT / "mas-v0.2-spec/conformance/statute-aware.json").read_bytes()
|
|
|
|
|
b = (ROOT / "mas_conformance/statute_aware.json").read_bytes()
|
|
|
|
|
assert a == b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_v02_statute_aware_corpus():
|
|
|
|
|
from mas_conformance.runner import run_statute_aware_corpus
|
|
|
|
|
rep = run_statute_aware_corpus()
|
|
|
|
|
assert rep.ok, rep.failures
|
MAS-WP-0009 T01: independent JS implementation (impl/js), differential test, SCP-9
impl/js (Node, zero deps, written from the specs): parser, v0.2
statute-aware rules, signing, outside master. Passes addresses.json,
statute-aware.json and auth.json unchanged; its master passes the 11
slave-setup mode flows against the Python email side (ExternalMaster).
Differential testing vs mas_core found two unstated stand-in rules, now
SCP-9 in the v0.2 draft with fixtures si-011..013, dt-109; ABNF comments
no longer say v0.1. Runner: external masters, step errors fail the flow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
2026-10-19 13:22:22 +02:00
|
|
|
assert rep.passed == 33
|