Self-review against the CI gates surfaced two genuine defects in the first commit. Changed-lines coverage is now 85% (floor 70): 1. The bridge projection read the WRONG STORE. `_active_construction_lines` built `ConstructionInterface` without `project_store`, so it fell back to the interface default instead of the configured construction store — the `RUNE_CONSTRUCTION_STORE` override was ignored, so a redirected deployment would have projected another project's construction state. Fixed: pass `_construction_store()` + the project id. 2. LOST UPDATE between the intent-layer note and the main activation path. The degrade path called `_construction_note()`, which does its own load/save; the normal path then saved its own stale `doc`, silently clobbering the note. Fixed: append the note to the SAME in-memory doc (one doc, one save), so the failure is recorded and survives. New coverage: active-contract projection + capsule rendering + read-only guarantee; store-resolution override; intent-layer-unavailable degrade path. Note: the stale TUI banner (`Rune v0.87.6` vs `__version__` 0.88.0) that this commit also fixed on the old base was fixed independently on master by the 0.88.2 release bump, so the rebase takes master's banner — nothing to carry here.
307 lines
14 KiB
Python
307 lines
14 KiB
Python
"""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"]
|