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:
tegwick 2026-10-19 13:22:22 +02:00
parent ecb1ba47f6
commit a841ceec73
23 changed files with 938 additions and 49 deletions

103
tests/test_differential.py Normal file
View 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
View 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

View file

@ -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