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
This commit is contained in:
parent
ecb1ba47f6
commit
a841ceec73
23 changed files with 938 additions and 49 deletions
103
tests/test_differential.py
Normal file
103
tests/test_differential.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""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]
|
||||
24
tests/test_js_impl.py
Normal file
24
tests/test_js_impl.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""The independent JS implementation (impl/js, MAS-WP-0009), where node is available."""
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not available")
|
||||
|
||||
|
||||
def test_js_passes_the_bundles_unchanged():
|
||||
# addresses.json, statute-aware.json and auth.json, read from the bundles in place
|
||||
proc = subprocess.run(["node", "--test"], cwd=ROOT / "impl/js", capture_output=True, text=True)
|
||||
assert proc.returncode == 0, proc.stdout[-2000:] + proc.stderr[-2000:]
|
||||
assert "# fail 0" in proc.stdout or "ℹ fail 0" in proc.stdout
|
||||
|
||||
|
||||
def test_js_outside_master_passes_the_slave_flows():
|
||||
from mas_modes.external import ExternalMaster
|
||||
from mas_modes.runner import run_mode_corpus
|
||||
rep = run_mode_corpus(master_factory=ExternalMaster.js, roles=("slave",))
|
||||
assert rep.ok, rep.failures
|
||||
assert rep.passed == 11
|
||||
|
|
@ -150,4 +150,4 @@ 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
|
||||
assert rep.passed == 29
|
||||
assert rep.passed == 33
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue