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
103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
"""Differential test: mas_core against the independent JS implementation (MAS-WP-0009).
|
|
|
|
Generated local-parts go through both. Every disagreement is a spec finding
|
|
or a bug. Known, recorded divergences are listed in KNOWN with their reason.
|
|
"""
|
|
import json
|
|
import random
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from mas_conformance import statute_aware_fixtures
|
|
from mas_core.generator import CannotExpress, for_destination
|
|
from mas_core.parser import MasSyntaxError, parse_address, parse_for_profile
|
|
from mas_core.policy import resolve
|
|
from mas_core.statute import build_statute
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BATCH = ROOT / "impl/js/src/batch.mjs"
|
|
pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not available")
|
|
|
|
TOKENS = ["ticket", "Ticket", "invoice", "support", "case", "gs-1", "GS-1", "x", "a.b", "--", "---",
|
|
"=", "==", "+", "!", "comments", "Comments", "callback", "chat", ".", "%2B", "_", "-", "$",
|
|
'"', "ï", "RE=2026", "PR42", "q"]
|
|
TRANSFORMS = ["resource-alternate", "interaction-alternate", "lowercase", "drop-facet", "drop-interaction"]
|
|
|
|
|
|
def corpus(n, seed=20261019):
|
|
rnd = random.Random(seed)
|
|
out = set()
|
|
while len(out) < n:
|
|
out.add("".join(rnd.choice(TOKENS) for _ in range(rnd.randint(1, 6))))
|
|
return sorted(out)
|
|
|
|
|
|
def js(statutes, cases):
|
|
proc = subprocess.run(["node", str(BATCH)], input=json.dumps({"statutes": statutes, "cases": cases}),
|
|
capture_output=True, text=True, check=True)
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def py_parse(local, profile):
|
|
try:
|
|
return {"ok": True, "parsed": parse_for_profile(local, profile).to_fixture_dict()}
|
|
except MasSyntaxError:
|
|
return {"ok": False}
|
|
|
|
|
|
def py_interpret(local, st):
|
|
try:
|
|
sem = resolve(parse_address(f"{local}@{st.namespace}"), st)
|
|
except MasSyntaxError:
|
|
return {"ok": False}
|
|
d = sem.to_dict()
|
|
return {"ok": True, "canonical": d["canonical_local_part"], "stand_ins": d["stand_ins"],
|
|
"key": sem.equivalence_key}
|
|
|
|
|
|
def py_dest(local, st, needs, lossy):
|
|
try:
|
|
return {"ok": True, "text": for_destination(local, st, needs, allow_lossy=lossy).local_part}
|
|
except (CannotExpress, MasSyntaxError):
|
|
return {"ok": False}
|
|
|
|
|
|
def strip(r):
|
|
return {k: v for k, v in r.items() if k != "error"}
|
|
|
|
|
|
DOCS = statute_aware_fixtures()["statutes"]
|
|
STATUTES = {k: build_statute(d, d["namespace"]) for k, d in DOCS.items()}
|
|
LOCALS = corpus(1500)
|
|
|
|
|
|
def disagreements(cases, py):
|
|
got = js(DOCS, cases)
|
|
return [(c, strip(g), p) for c, g, p in zip(cases, got, (py(c) for c in cases)) if strip(g) != p]
|
|
|
|
|
|
def test_parser_agrees():
|
|
cases = [{"op": "parse", "local": lp, "profile": prof} for lp in LOCALS for prof in ("p1", "p2")]
|
|
bad = disagreements(cases, lambda c: py_parse(c["local"], c["profile"]))
|
|
assert bad == [], bad[:10]
|
|
|
|
|
|
def test_interpretation_and_equivalence_agree():
|
|
cases = [{"op": "interpret", "local": lp, "statute": s} for lp in LOCALS for s in DOCS]
|
|
bad = disagreements(cases, lambda c: py_interpret(c["local"], STATUTES[c["statute"]]))
|
|
assert bad == [], bad[:10]
|
|
|
|
|
|
def test_destination_transforms_agree():
|
|
rnd = random.Random(7)
|
|
valid = [lp for lp in LOCALS if py_parse(lp, "p2")["ok"]]
|
|
cases = []
|
|
for lp in valid:
|
|
for s in DOCS:
|
|
needs = rnd.sample(TRANSFORMS, rnd.randint(0, 3))
|
|
cases.append({"op": "dest", "local": lp, "statute": s, "needs": needs, "lossy": rnd.random() < 0.5})
|
|
bad = disagreements(cases, lambda c: py_dest(c["local"], STATUTES[c["statute"]], c["needs"], c["lossy"]))
|
|
assert bad == [], bad[:10]
|