Files
rune/tests/test_builder_operated.py

292 lines
13 KiB
Python

"""SCAFFOLD-TRANCHE-0013 §13E: real builder-operated external project.
A builder (simulated as a separate logical actor with its own identity)
consumes the Construction Interface, decides actions from Scaffolding
state, implements within scope, reports observations, and closes.
Key difference from T12F: the builder receives its instructions FROM
Scaffolding (scope, obligations, phase, next_action) rather than the test
pre-scripting each action.
"""
import json
import os
import textwrap
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = os.path.join(os.path.dirname(__file__), "..")
sys.path.insert(0, os.path.join(ROOT, "src"))
from rune import construction_interface as ci # noqa: E402
from rune import structural_store as ss # noqa: E402
@pytest.fixture
def setup(tmp_path):
store = str(tmp_path / ".rune" / "scaffold")
proj_dir = str(tmp_path / "practice-scheduler")
iface = ci.ConstructionInterface(
builder="orchestrator", project_store=store,
project_id="practice-scheduler-js")
iface.scaffold.init(purpose="governance authority")
doc = iface.scaffold.load()
ss.add_entity(doc, id="capability:csv_export", kind="capability",
identities={}, maturity="WIRED", evidence_refs=["seed"])
ss.add_entity(doc, id="contract:csv_export", kind="contract",
identities={}, evidence_refs=["seed"])
ss.add_relationship(doc, type="implements",
from_id="capability:csv_export",
to_id="contract:csv_export", evidence_refs=["seed"])
iface.scaffold.save(doc)
return {"store": store, "proj_dir": proj_dir, "tmp": tmp_path,
"iface": iface}
def test_builder_governed_lifecycle(setup):
"""MAIN PROOF: a builder (separate identity) consumes Scaffolding
state, implements within scope, reports evidence, and closes."""
# The BUILDER (separate identity) creates its own interface
builder = ci.ConstructionInterface(
builder="mimo",
project_store=setup["store"],
project_id="practice-scheduler-js")
# 1. SUBMIT: builder asks Scaffolding to classify
env = builder.submit_need(
objective="create a practice scheduler with create/list/complete",
constraints={"contracts": ["contract:csv_export"]},
affected_surface=["practice-scheduler-js/scheduler.js", "practice-scheduler-js/test_scheduler.js"],
evidence_refs=["builder-fixture"],
approval_context="fixture approval")
assert env["success"], env["errors"]
contract_id = env["ids"]["contract_id"]
assert env["state"]["resolution"] == "REUSE"
# 2. SCOPE: builder receives and respects allowed paths
scope_env = builder.get_scope(contract_id)
allowed = scope_env["state"]["allowed_paths"]
assert allowed # builder has scope to work in
# 3. OBLIGATIONS: builder receives what it must prove
obl_env = builder.get_obligations(contract_id)
assert obl_env["state"]["obligations"]
# 4. TRANSITION: builder advances lifecycle
builder.transition(contract_id, "approved")
builder.transition(contract_id, "building")
# 5. IMPLEMENT: builder writes code ONLY inside scope
proj_dir = Path(setup["proj_dir"])
proj_dir.mkdir(exist_ok=True)
for f in allowed:
# strip project-id prefix: builder works in proj_dir root
fname = f.split("/")[-1]
fpath = proj_dir / fname
fpath.parent.mkdir(parents=True, exist_ok=True)
if fname == "scheduler.js":
fpath.write_text(
"const items = [];\n"
"function create(text) { items.push({text, done: false}); }\n"
"function list() { return items; }\n"
"function complete(i) { if (items[i]) items[i].done = true; }\n"
"module.exports = { create, list, complete };\n")
elif fname == "test_scheduler.js":
fpath.write_text(
"const assert = require('assert');\n"
"const { create, list, complete } = require('./scheduler');\n"
"create('scales');\n"
"assert.strictEqual(list().length, 1);\n"
"complete(0);\n"
"assert.strictEqual(list()[0].done, true);\n"
"console.log('PASS');\n")
# 6. REPORT: builder reports observations through the interface
builder.report_observation(contract_id, [
{"entity": "scheduler.js", "detail": "implemented"},
{"entity": "test_scheduler.js", "detail": "test created"},
])
# 7. VERIFY: builder transitions to verifying + runs tests
builder.transition(contract_id, "verifying")
for f in allowed:
fname = f.split("/")[-1]
fp = proj_dir / fname
if fp.exists():
print(f"DEBUG {fname}: {repr(fp.read_text()[:100])}")
vr = subprocess.run(["node", "test_scheduler.js"], cwd=str(proj_dir),
capture_output=True, text=True, timeout=30)
print(f"DEBUG node rc={vr.returncode} stdout={repr(vr.stdout[:80])} stderr={repr(vr.stderr[:80])}")
assert vr.returncode == 0 and "PASS" in vr.stdout
builder.report_observation(contract_id, [
{"entity": "test:scheduler", "detail": f"result: {vr.stdout.strip()}"}])
# 8. CLOSE: builder transitions through superseding, Scaffolding closes
builder.transition(contract_id, "superseding")
close_env = builder.close(contract_id, observed_evidence=[
{"entity": "scheduler.js", "detail": "implemented in scope"},
{"entity": "test:scheduler", "detail": "PASSED"}],
scope_files=[str(proj_dir / f) for f in allowed])
assert close_env["success"]
assert close_env["state"]["contract_quality"]["scope_check"]["verdict"] == "green"
# 9. FRESH RESUME: a completely fresh builder recovers state
fresh = ci.ConstructionInterface(
builder="fresh-builder",
project_store=setup["store"],
project_id="practice-scheduler-js")
resumed = fresh.resume(contract_id)
assert resumed["success"]
st = resumed["state"]
assert st["need"]["objective"] == (
"create a practice scheduler with create/list/complete")
assert st["phase"] == "closed"
assert st["scope"] == allowed
assert st["obligations"]
def test_builder_cannot_self_certify(setup):
iface = ci.ConstructionInterface(
builder="rogue", project_store=setup["store"],
project_id="practice-scheduler-js")
iface.scaffold.init(purpose="test")
doc = iface.scaffold.load()
iface.scaffold.new_contract(doc, contract_id="ROGUE", need="test",
scope=["src/x"])
iface.scaffold.save(doc)
env = iface.close("ROGUE", observed_evidence=[
{"entity": "x", "detail": "done"}])
assert not env["success"]
assert env["errors"][0]["error"] == ci.Errors.VERIFICATION_FAILED
def test_builder_cannot_broaden_scope(setup):
iface = ci.ConstructionInterface(
builder="sneaky", project_store=setup["store"],
project_id="practice-scheduler-js")
iface.scaffold.init(purpose="test")
doc = iface.scaffold.load()
iface.scaffold.new_contract(doc, contract_id="SC", need="test",
scope=["src/allowed.py"])
iface.scaffold.save(doc)
env = iface.amend_scope("SC", ["src/unauthorized.py"],
reason="need more", authority="builder")
assert not env["success"]
assert env["errors"][0]["error"] == ci.Errors.APPROVAL_REQUIRED
class TestSelfHostingEquivalence:
"""§13G: same construction machinery for Rune-internal and external
projects. Core semantics must match."""
def _make_external(self, tmp_path):
iface = ci.ConstructionInterface(
builder="b", project_store=str(tmp_path / "ext" / "scaffold"),
project_id="ext-proj")
iface.scaffold.init(purpose="external")
return iface
def _make_internal(self, tmp_path):
iface = ci.ConstructionInterface(
builder="b", project_store=str(tmp_path / "int" / "scaffold"),
project_id="rune")
iface.scaffold.init(purpose="internal")
return iface
def test_same_lifecycle_both_modes(self, tmp_path):
"""Same lifecycle steps produce same structural semantics."""
for iface in (self._make_external(tmp_path),
self._make_internal(tmp_path)):
doc = iface.scaffold.load()
iface.scaffold.new_contract(doc, contract_id="EQ", need="test",
scope=["src/x"])
iface.scaffold.transition_contract(doc, "EQ", "approved")
iface.scaffold.transition_contract(doc, "EQ", "building")
iface.scaffold.commit(doc, what="lifecycle")
doc2 = iface.scaffold.load()
c = iface.scaffold.contract(doc2, "EQ")
assert c["phase"] == "building"
assert c["identities"]["need"] == "test"
class TestInterruptionResume:
"""§13F: real fresh-process resume at each lifecycle phase."""
@pytest.mark.parametrize("phase", ["approved", "building", "verifying",
"closed"])
def test_fresh_builder_resume(self, tmp_path, phase):
"""A fresh builder process with NO prior context recovers the
construction state from the durable store alone."""
store = str(tmp_path / "scaffold")
s = ci.ConstructionInterface(builder="original",
project_store=store, project_id="p")
s.scaffold.init(purpose="resume test")
doc = s.scaffold.load()
s.scaffold.new_contract(doc, contract_id="R1", need="bounded work",
scope=["src/x"],
verification_obligations=[
"verify change works"])
s.scaffold.transition_contract(doc, "R1", "approved")
s.scaffold.commit(doc, what="approved")
if phase in ("building", "verifying", "closed"):
s.scaffold.transition_contract(doc, "R1", "building")
s.scaffold.commit(doc, what="building")
if phase in ("verifying", "closed"):
s.scaffold.transition_contract(doc, "R1", "verifying")
s.scaffold.commit(doc, what="verifying")
if phase == "closed":
s.scaffold.close_contract(doc, "R1", observed_evidence=[
{"entity": "module:x", "source": "test"}])
s.scaffold.commit(doc, what="closed")
# FRESH builder: no prior context, only contract id + store path
script = textwrap.dedent(f"""\
import sys, json
sys.path.insert(0, {os.path.join(ROOT, "src")!r})
from rune.construction_interface import ConstructionInterface
iface = ConstructionInterface(
builder="fresh-builder",
project_store={store!r},
project_id="p")
env = iface.resume("R1")
st = env["state"]
print(json.dumps({{
"phase": st["phase"],
"need": st["need"].get("objective", ""),
"scope": st["scope"],
"obligations": len(st["obligations"]),
"next_action": st["next_action"],
}}))
""")
r = subprocess.run([sys.executable, "-c", script],
capture_output=True, text=True, timeout=60)
assert r.returncode == 0, r.stderr
recovered = json.loads(r.stdout)
assert recovered["phase"] == phase
assert recovered["need"] == "bounded work"
assert recovered["scope"] == ["src/x"]
assert recovered["obligations"] > 0
assert recovered["next_action"]
def test_resume_does_not_duplicate(self, tmp_path):
store = str(tmp_path / "scaffold")
s = ci.ConstructionInterface(builder="original",
project_store=store, project_id="p")
s.scaffold.init(purpose="test")
doc = s.scaffold.load()
s.scaffold.new_contract(doc, contract_id="R2", need="work",
scope=["src/x"])
s.scaffold.commit(doc, what="created")
# fresh builder loads — no re-creation
s2 = ci.ConstructionInterface(builder="fresh",
project_store=store, project_id="p")
doc2 = s2.scaffold.load()
before = len([e for e in doc2["entities"]
if e["kind"] == "construction_contract"])
s2.scaffold.transition_contract(doc2, "R2", "approved")
s2.scaffold.commit(doc2, what="resumed transition")
after = len([e for e in s2.scaffold.load()["entities"]
if e["kind"] == "construction_contract"])
assert before == after