"""test_native_default.py — T15 §15J/§15K: Scaffolding as the NATIVE DEFAULT construction route + bypass prevention. 15J: ordinary send_task input (the real TUI submission path per the 12A.C trace: TUI HistoryInput → submit → RuneOperator.send_task → construction hook) automatically produces canonical construction state: A. PROJECT_BUILD — durable Project Scaffold, no /build command B. EXISTING — attaches Rune authority, no fake project C. LOCAL_CHANGE — lightweight, still governed D. NOT_CONSTRUCTION — no construction state E. UNKNOWN — probes, no invention F. manual override through the SAME path (/scaffold-mode marker) 15K: a native client construction command (/build start) cannot mint construction outside the authority — NOT_CONSTRUCTION is refused; BuildManager state is created only AFTER canonical recognition. """ import json import os import sys from pathlib import Path import pytest REPO = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO / "src")) from rune import scaffold as sc # noqa: E402 from rune import structural_store as ss # noqa: E402 @pytest.fixture() def op(tmp_path, monkeypatch): monkeypatch.setenv("RUNE_CONSTRUCTION_STORE", str(tmp_path / ".rune" / "scaffold")) from rune.rune_operator import RuneOperator # §15X process repair: construction-hook tests exercise the hook, not # the provider. send_task invokes a REAL model downstream of the hook — # unstubbed, these unit tests burn provider quota and starve other # suites' model calls (observed: stage-telemetry GATE_FAILED after # quota exhaustion). The hook runs BEFORE model selection, so a stub # changes nothing these tests assert. from rune.tool_protocol import OperatorResponse monkeypatch.setattr( "rune.rune_operator.RuneOperator._execute_on_model", lambda self, *a, **k: OperatorResponse(success=True, user_text="stubbed")) return RuneOperator(str(tmp_path / "operator.json")), tmp_path def _last_activation(store: Path): doc = json.loads((store / ".rune" / "scaffold" / "current.json").read_text()) recs = [o for o in doc.get("observations", []) if o.get("kind") == "construction_activated"] return recs[-1] if recs else None # ── 15J ───────────────────────────────────────────────────────────────────── def test_15j_native_project_build_without_any_command(op): op, tmp = op op.send_task("Build me a standalone practice scheduler app") rec = _last_activation(tmp) assert rec is not None, "native input must produce construction state" assert rec["mode"] == "PROJECT_BUILD" assert rec["activated"] is True # durable provisional Project Scaffold exists projects = (tmp / ".rune" / "scaffold" / "projects") assert any(projects.glob("*/current.json")), "identity before repository" def test_15j_native_existing_project_change_attaches_rune(op): op, tmp = op op.send_task("Add CSV export to the rune project") rec = _last_activation(tmp) assert rec["mode"] == "EXISTING_PROJECT_CHANGE" assert rec["activated"] is True def test_15j_native_local_change_is_light(op): op, tmp = op op.send_task("Fix the typo in the README") rec = _last_activation(tmp) assert rec["mode"] == "LOCAL_CHANGE" assert rec["activated"] is True def test_15j_native_not_construction_touches_nothing(op): op, tmp = op op.send_task("Explain modal interchange theory") rec = _last_activation(tmp) assert rec["mode"] == "NOT_CONSTRUCTION" assert rec["activated"] is False # no project invention projects = (tmp / ".rune" / "scaffold" / "projects") assert not projects.exists() or not any(projects.glob("*/current.json")) def test_15j_intent_layer_unavailable_preserves_classifier_path(op, monkeypatch): """If the meaning layer is unavailable the PRE-EXISTING behaviour is preserved exactly (classify + activate) and the reason is recorded — governance is never silently changed in an unproven direction.""" import rune.construction_intent as cint def _boom(*a, **k): raise RuntimeError("intent layer offline") monkeypatch.setattr(cint, "resolve_construction_intent", _boom) op, tmp = op op.send_task("fix the broken retry handling in this project") rec = _last_activation(tmp) assert rec["mode"] == "EXISTING_PROJECT_CHANGE", rec assert rec["activated"] is True assert rec.get("intent_layer") is None doc = json.loads((tmp / ".rune" / "scaffold" / "current.json").read_text()) notes = [o for o in doc.get("observations", []) if o.get("kind") == "construction_note"] assert any("intent layer unavailable" in str(n.get("text", "")) for n in notes), notes def test_15j_native_question_with_target_is_not_construction(op): """§17J criterion 1 on the NORMAL path (T22 wiring repair). A pure question that happens to carry a target reference ("this module") must NOT record construction state. Before the intent-layer gate this classified EXISTING_PROJECT_CHANGE (a target reference was read as scope evidence) and recorded an activation for a discussion turn. The canonical meaning layer resolves it as DISCUSSION; the classifier is never reached. """ op, tmp = op op.send_task("explain how this module works") rec = _last_activation(tmp) assert rec is not None, "every input records its classification outcome" assert rec["mode"] == "NOT_CONSTRUCTION", rec assert rec["activated"] is False assert rec.get("intent_layer") == "DISCUSSION", rec # no project invention projects = (tmp / ".rune" / "scaffold" / "projects") assert not projects.exists() or not any(projects.glob("*/current.json")) def test_15j_native_real_construction_still_activates(op): """The meaning gate must not suppress genuine construction demands — the guard is a question/discussion gate, not an activation blocker.""" op, tmp = op op.send_task("fix the broken retry handling in this project") rec = _last_activation(tmp) assert rec["mode"] == "EXISTING_PROJECT_CHANGE", rec assert rec["activated"] is True assert rec.get("intent_layer") is None, rec def test_15j_native_unknown_probes_without_invention(op): op, tmp = op op.send_task("Build something useful for this") rec = _last_activation(tmp) assert rec["mode"] == "UNKNOWN" assert rec["activated"] is False assert not (tmp / ".rune" / "scaffold" / "projects").exists() or \ not any((tmp / ".rune" / "scaffold" / "projects").glob("*/current.json")) def test_15j_manual_override_same_path(op): """Case F: /scaffold-mode marker feeds explicit_override through the SAME classify_mode call — provenance carries 'user'.""" op, tmp = op op.send_task("/scaffold-mode NOT_CONSTRUCTION build me a scheduler app") rec = _last_activation(tmp) assert rec["mode"] == "NOT_CONSTRUCTION", \ "override must win through the SAME path" assert "user" in " ".join(rec.get("classification_evidence", [])), \ "override provenance must be recorded" # and a non-overridden version of the same text classifies normally op.send_task("Build me a standalone practice scheduler app") rec2 = _last_activation(tmp) assert rec2["mode"] == "PROJECT_BUILD" # ── 15K ───────────────────────────────────────────────────────────────────── def test_15k_build_manager_has_no_parallel_construction_state(tmp_path, monkeypatch): """§15K inventory: BuildManager (the /build vehicle) creates its own phase state but must NOT create project scaffolds/contracts. The canonical construction record comes only from the send_task hook. BuildManager.start_build shells out to `git worktree add` against a HARDCODED production repo path (/mnt/workdrive/rune) — that is real VCS mutation and has no place in a unit test (it fails on CI with dubious-ownership, and would pollute the real repo locally). This test therefore inspects the manager's state machine WITHOUT invoking the worktree-creating path: construction-state separation is a property of where BuildManager writes, not of the git operation.""" from rune import build_mode from rune.build_mode import BuildManager monkeypatch.chdir(tmp_path) # NOT bare os.chdir — must not leak monkeypatch.setattr(build_mode, "BUILDS_DIR", str(tmp_path / ".rune" / "work" / "builds")) monkeypatch.setattr(build_mode, "WORKTREE_DIR", str(tmp_path / ".rune" / "worktrees")) bm = BuildManager() # start_build's worktree step is a real VCS mutation — stub it; the # property under test (no canonical construction state) is orthogonal monkeypatch.setattr( BuildManager, "create_worktree", lambda self: setattr(self.state, "worktree_path", "stubbed") or "stubbed") build_id = bm.start_build("build a thing") # BuildManager's own state exists... assert bm.state.description == "build a thing" # ...but it created NO canonical construction authority assert not (tmp_path / "store" / "scaffold").exists() or \ not any((tmp_path / "store" / "scaffold").rglob("current.json")) def test_15k_tui_build_start_refuses_not_construction(monkeypatch, tmp_path): """/build start with a non-construction description must be refused BEFORE BuildManager mints build state — no silent bypass.""" monkeypatch.setenv("RUNE_CONSTRUCTION_STORE", str(tmp_path / ".rune" / "scaffold")) from rune.rune_operator import RuneOperator op = RuneOperator(str(tmp_path / "operator.json")) desc = "explain modal interchange theory" # the same classification the hardened /build start handler performs from rune.construction_mode import classify_mode d = classify_mode(desc, target_project=None, project_exists=None, affected_surface=[]) assert d.mode == "NOT_CONSTRUCTION" # the handler's contract: refuse (never reach BuildManager.start_build) # — proven here at the decision level plus the hook's canonical record op.send_task(f"/build start: {desc}") # via the hook path, the canonical store records NOT_CONSTRUCTION and # no build artifact exists doc = json.loads((tmp_path / ".rune" / "scaffold" / "current.json").read_text()) recs = [o for o in doc.get("observations", []) if o.get("kind") == "construction_activated"] assert recs and recs[-1]["mode"] in ("NOT_CONSTRUCTION", "UNKNOWN"), \ "prefixed build command must not mint construction for a chat intent" assert recs[-1]["activated"] is False assert not (tmp_path / ".rune" / "work" / "builds").exists() or \ not list((tmp_path / ".rune" / "work" / "builds").iterdir()) def test_15l_research_requests_never_become_project_builds(): """15L negative: research/investigation intents must not PROJECT_BUILD merely because they contain 'build'/'construct' words. Ownership: research competence = Research Core; continuity = future Substrate. The classifier may probe (UNKNOWN) but must never invent a project.""" from rune.construction_mode import classify_mode research_intents = [ "investigate why the build fails", "build understanding of the migration path", "construct an explanation of modal interchange", "research the best approach for CSV export", ] for t in research_intents: d = classify_mode(t, target_project=None, project_exists=None, affected_surface=[]) assert d.mode != "PROJECT_BUILD", \ f"research intent '{t}' must not PROJECT_BUILD" assert d.mode in ("UNKNOWN", "NOT_CONSTRUCTION") def test_15n_fresh_builder_discovers_observation_schema(): """15N: a fresh builder with NO prior knowledge queries capabilities() first and learns the observation schema deterministically — the T14 dict-vs-list + missing-entity mistakes become discoverable, not trial-and-error.""" import tempfile from rune import construction_interface as ci tmp = Path(tempfile.mkdtemp()) iface = ci.ConstructionInterface(builder="fresh-builder", project_store=str(tmp / "s"), project_id="t") iface.scaffold.init(purpose="15N probe") caps = iface.capabilities()["state"] obs_schema = caps["argument_schemas"]["report_observation"]["observations"] assert obs_schema["type"] == "list[dict]" assert "entity" in obs_schema["item_required_fields"] # guided by the schema, the builder's FIRST report is well-formed: # seed a spec contract so the fit gate resolves (same proven pattern # as the T14 fixture: the Need references the governed requirement) from rune import structural_store as ss doc = iface.scaffold.load() ss.add_entity(doc, id="contract:spec-15n", kind="contract", identities={"objective": "spec"}, evidence_refs=["e"]) ss.add_entity(doc, id="capability:planned-15n", kind="capability", maturity="DECLARED", evidence_refs=["e"]) ss.add_relationship(doc, type="implements", from_id="capability:planned-15n", to_id="contract:spec-15n", evidence_refs=["e"]) iface.scaffold.commit(doc, what="seed 15n spec") sub = iface.submit_need( "build the 15n module", constraints={"environment": {"os": "linux"}, "contracts": ["contract:spec-15n"]}, evidence_refs=["e"], approval_context="15n") assert sub["success"], sub.get("errors") cid = sub["ids"]["contract_id"] r = iface.report_observation(cid, [ {"entity": "m.js", "detail": "created"}]) assert r["success"], r.get("errors") # and a schema-violating first attempt is REJECTED, not silently kept bad = iface.report_observation(cid, {"entity": "x", "detail": "dict"}) assert not bad["success"]