Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb5018413 | ||
|
|
c8f0a8e353 | ||
|
|
fb7fb34ca5 | ||
|
|
abe24da81d | ||
|
|
15c54539b5 | ||
|
|
ad35a2f7f5 | ||
|
|
e2f536f71a | ||
|
|
5b2712f5e6 | ||
|
|
124b77e95b | ||
|
|
1b7995e093 | ||
|
|
fcd835883e | ||
|
|
37ce68eff8 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.67.0**
|
||||
**Current release: v0.70.0**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-16T00:01:04Z",
|
||||
"generated_at": "2026-09-16T03:45:21Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -255,6 +255,7 @@
|
||||
"testing_mode",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"wait_guard",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.67.0"
|
||||
version = "0.70.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.67.0"
|
||||
__version__ = "0.70.0"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.67.0"' in rune_version,
|
||||
'__version__ = "0.70.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -185,3 +185,171 @@ class CapabilityLedger:
|
||||
"generation": rec["generation"],
|
||||
"limitations": rec["limitations"],
|
||||
}
|
||||
|
||||
|
||||
# ── T21 §21C/21D: LIVING CAPABILITY SPECIFICATIONS + STRICT SEMVER ──────
|
||||
#
|
||||
# A Living Capability Spec is the WHY+WHAT-SHOULD-EXIST layer; the
|
||||
# capability record remains the WHAT-EXISTS+WHERE layer. Both live in the
|
||||
# SAME ledger document (rule C: no second authority) — the spec is a
|
||||
# versioned field-set on the capability, not a separate subsystem.
|
||||
#
|
||||
# STRICT SEMVER (§21D): only meaningful units get versions.
|
||||
# PATCH: bug fix / internal change, contract compatible
|
||||
# MINOR: backwards-compatible new capability/behaviour
|
||||
# MAJOR: breaking contract/interface/state-semantics change
|
||||
# Baselines for existing capabilities: explicit
|
||||
# governance_version_started_at + baseline_reason; no fake history.
|
||||
|
||||
SPEC_SCHEMA_VERSION = 1
|
||||
|
||||
import re as _re
|
||||
|
||||
SEMVER_RE = _re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
||||
|
||||
|
||||
class SpecError(ValueError):
|
||||
"""Invalid spec operation (material change without bump, unjustified
|
||||
bump, bad version...)."""
|
||||
|
||||
|
||||
def classify_version_bump(*, contract_changed: bool,
|
||||
behaviour_added: bool,
|
||||
consumer_break: bool,
|
||||
implementation_only: bool) -> str:
|
||||
"""§21D-3: derive PATCH/MINOR/MAJOR from the change shape. The model
|
||||
may propose; the machinery verifies."""
|
||||
if consumer_break:
|
||||
return "MAJOR"
|
||||
if contract_changed:
|
||||
# contract changed but no consumer break: interface tightened —
|
||||
# still breaking for consumers relying on the old shape
|
||||
return "MAJOR"
|
||||
if behaviour_added:
|
||||
return "MINOR"
|
||||
if implementation_only:
|
||||
return "PATCH"
|
||||
raise SpecError("no version-worthy change: metadata churn does not bump")
|
||||
|
||||
|
||||
def bump_version(version: str, kind: str) -> str:
|
||||
m = SEMVER_RE.match(version)
|
||||
if not m:
|
||||
raise SpecError(f"invalid semver {version!r}")
|
||||
major, minor, patch = (int(x) for x in m.groups())
|
||||
if kind == "MAJOR":
|
||||
return f"{major + 1}.0.0"
|
||||
if kind == "MINOR":
|
||||
return f"{major}.{minor + 1}.0"
|
||||
if kind == "PATCH":
|
||||
return f"{major}.{minor}.{patch + 1}"
|
||||
raise SpecError(f"unknown bump kind {kind!r}")
|
||||
|
||||
|
||||
def attach_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
requirements: list[str],
|
||||
expected_behaviours: list[str],
|
||||
non_goals: list[str],
|
||||
acceptance_criteria: list[str],
|
||||
baseline_version: str = "0.1.0",
|
||||
baseline_reason: str = "initial governed baseline",
|
||||
governance_version_started_at: str | None = None) -> dict:
|
||||
"""Attach a Living Capability Specification to an existing capability
|
||||
record (rule 21C: extend, don't duplicate). Baseline policy (§21D-1):
|
||||
the starting version is explicit — 1.0.0 only for stable contracts
|
||||
with sufficient evidence; otherwise 0.x with a reason."""
|
||||
from rune.capability_ledger import LEDGER_SCHEMA_VERSION # noqa: F401
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
if not SEMVER_RE.match(baseline_version):
|
||||
raise SpecError(f"invalid baseline semver {baseline_version!r}")
|
||||
spec = {
|
||||
"schema_version": SPEC_SCHEMA_VERSION,
|
||||
"version": baseline_version,
|
||||
"governance_version_started_at": governance_version_started_at
|
||||
or _now(),
|
||||
"baseline_reason": baseline_reason,
|
||||
"requirements": requirements,
|
||||
"expected_behaviours": expected_behaviours,
|
||||
"non_goals": non_goals,
|
||||
"acceptance_criteria": acceptance_criteria,
|
||||
"version_history": [{
|
||||
"at": _now(), "version": baseline_version, "kind": "BASELINE",
|
||||
"reason": baseline_reason, "changed_sections": [],
|
||||
"affected_consumers": [], "validation_consequences": [],
|
||||
}],
|
||||
}
|
||||
rec["spec"] = spec
|
||||
rec["updated_at"] = _now()
|
||||
return spec
|
||||
|
||||
|
||||
def revise_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
contract_changed: bool = False,
|
||||
behaviour_added: bool = False,
|
||||
consumer_break: bool = False,
|
||||
implementation_only: bool = False,
|
||||
reason: str,
|
||||
changed_sections: list[str] | None = None,
|
||||
affected_consumers: list[str] | None = None,
|
||||
validation_consequences: list[str] | None = None,
|
||||
proposed_kind: str | None = None) -> dict:
|
||||
"""§21D-3/21E: revise a spec with an AUTOMATIC version decision.
|
||||
The model proposes; the machinery classifies and records. Material
|
||||
change without a bump is impossible (every revise computes one);
|
||||
unjustified bumps are impossible (no-change revise raises)."""
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
spec = rec.get("spec")
|
||||
if spec is None:
|
||||
raise SpecError(f"{capability_id} has no spec — attach one first")
|
||||
kind = (proposed_kind or classify_version_bump(
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only))
|
||||
# verify a PROPOSED kind against the change shape (model cannot
|
||||
# upgrade PATCH to MINOR without the corresponding change shape)
|
||||
actual = classify_version_bump(
|
||||
contract_changed=contract_changed,
|
||||
behaviour_added=behaviour_added,
|
||||
consumer_break=consumer_break,
|
||||
implementation_only=implementation_only)
|
||||
order = {"PATCH": 0, "MINOR": 1, "MAJOR": 2}
|
||||
if proposed_kind and order[proposed_kind] > order[actual]:
|
||||
raise SpecError(
|
||||
f"proposed {proposed_kind} exceeds change shape {actual} — "
|
||||
f"unjustified bump")
|
||||
old = spec["version"]
|
||||
new = bump_version(old, kind)
|
||||
spec["version"] = new
|
||||
spec["version_history"].append({
|
||||
"at": _now(), "version": new, "kind": kind,
|
||||
"reason": reason[:200],
|
||||
"changed_sections": list(changed_sections or []),
|
||||
"affected_consumers": list(affected_consumers or []),
|
||||
"validation_consequences": list(validation_consequences or []),
|
||||
})
|
||||
rec["updated_at"] = _now()
|
||||
return {"old": old, "new": new, "kind": kind, "reason": reason}
|
||||
|
||||
|
||||
def spec_context(rec: dict) -> dict:
|
||||
"""The spec half of a surgical context packet (§21M): WHY + WHAT
|
||||
SHOULD EXIST, compact."""
|
||||
s = rec.get("spec") or {}
|
||||
return {
|
||||
"capability_id": rec["capability_id"],
|
||||
"version": s.get("version"),
|
||||
"purpose": rec.get("purpose"),
|
||||
"requirements": s.get("requirements", []),
|
||||
"expected_behaviours": s.get("expected_behaviours", []),
|
||||
"non_goals": s.get("non_goals", []),
|
||||
"acceptance_criteria": s.get("acceptance_criteria", []),
|
||||
"contract": rec.get("contract"),
|
||||
"invariants": rec.get("invariants", []),
|
||||
"behaviour_specs": rec.get("behaviour_specs", []),
|
||||
"last_bump": (s.get("version_history") or [{}])[-1],
|
||||
}
|
||||
@@ -1585,8 +1585,13 @@ class RuneOperator:
|
||||
task.status = "failed"
|
||||
task.result = result.error or "Unknown error"
|
||||
self.state.current_task = None
|
||||
self._save()
|
||||
# Mirror the terminal move into the canonical ledger (#18).
|
||||
# T21 §21I CANONICAL-FIRST ORDERING (crash-window fix): the
|
||||
# canonical transition commits BEFORE the legacy cache save. The
|
||||
# previous order (save → transition) left a crash window where
|
||||
# operator.json claimed completed/failed while the canonical
|
||||
# repo still said running — the cache outran truth. Reversed:
|
||||
# a crash after transition but before _save() leaves the cache
|
||||
# STALE (backfillable via _ensure_canonical) instead of AHEAD.
|
||||
self._ensure_canonical(task)
|
||||
if result.success and result.pending_approval is not None:
|
||||
self._task_state.transition(task.task_id, "awaiting_approval")
|
||||
@@ -1605,6 +1610,7 @@ class RuneOperator:
|
||||
self._task_state.transition(task.task_id, "failed")
|
||||
# P0-04 (#20): meaningful-event seed (see send_task failure path).
|
||||
self._emit_task_failed(task, channel=self._channel_for_task(task.task_id))
|
||||
self._save()
|
||||
|
||||
def _ensure_canonical(self, task: RuneTask) -> None:
|
||||
"""Backfill a task seeded through the legacy path into the ledger (#18).
|
||||
|
||||
+33
-4
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.67.0
|
||||
Version: v0.70.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1697,9 +1697,37 @@ class RuneApp(App):
|
||||
except Exception as e:
|
||||
return ("registry_error", str(e))
|
||||
|
||||
def _task_repo_for_loader():
|
||||
"""Access the canonical TaskStateRepository for the task
|
||||
loader (T21 21G migration) — closure over the operator
|
||||
getter already in scope. Degrades to the legacy ledger view
|
||||
when the operator is unreachable (startup ordering), which
|
||||
the load_tasks fallback then handles."""
|
||||
try:
|
||||
return getattr(_operator(), "_task_state", None)
|
||||
except Exception as exc: # noqa: BLE001 — deliberate degradation: startup ordering; the fallback path handles it and the degradation is logged
|
||||
print(f"[task-loader] canonical repo unavailable ({exc}); "
|
||||
f"falling back to export ledger", flush=True)
|
||||
return None
|
||||
|
||||
def load_tasks():
|
||||
try:
|
||||
if BACKEND_AVAILABLE:
|
||||
# T21 21G: the TUI task loader reads the CANONICAL
|
||||
# TaskStateRepository (17F authority), not the legacy
|
||||
# task_persist export ledger. task_persist remains a
|
||||
# session-end export view (its only remaining role).
|
||||
repo = _task_repo_for_loader()
|
||||
if repo is not None:
|
||||
records = repo.list_all()
|
||||
active = [
|
||||
{"task_id": r.task_id, "title": r.title,
|
||||
"status": r.status}
|
||||
for r in records
|
||||
if r.status in ("created", "running")]
|
||||
all_pins = pins.list_pins()
|
||||
open_pins = [p for p in all_pins if p.get("status") == "open"]
|
||||
return ("tasks", active, open_pins)
|
||||
data = task_persist.load()
|
||||
tasks = data.get("tasks", [])
|
||||
active = [t for t in tasks if t.get("status") in ("open", "in_progress")]
|
||||
@@ -1856,7 +1884,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.67.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.70.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -3117,6 +3145,7 @@ class RuneApp(App):
|
||||
/testing → aggregate across registered projects
|
||||
/testing <project> → one project's outstanding items."""
|
||||
try:
|
||||
from pathlib import Path as _Path
|
||||
from rune import testing_mode as tm
|
||||
from rune.behaviour_ledger import BehaviourLedger
|
||||
from rune.project_registry import ProjectRegistry
|
||||
@@ -3131,7 +3160,7 @@ class RuneApp(App):
|
||||
f"{arg.strip()!r}[/yellow]")
|
||||
return
|
||||
loc = entry["scaffold_location"]
|
||||
if not (Path(loc) / "behaviour_ledger.json").is_file():
|
||||
if not (_Path(loc) / "behaviour_ledger.json").is_file():
|
||||
ch.add_message("system",
|
||||
f"[yellow]{arg.strip()} has no behaviour ledger "
|
||||
f"yet[/yellow]")
|
||||
@@ -3142,7 +3171,7 @@ class RuneApp(App):
|
||||
f"[bold]{arg.strip()}: {len(items)} outstanding "
|
||||
f"validation(s)[/bold]")
|
||||
else:
|
||||
rows = tm.aggregate(registry, Path("store/scaffold"))
|
||||
rows = tm.aggregate(registry, _Path("store/scaffold"))
|
||||
ch.add_message("system", "[bold]Testing aggregate "
|
||||
"(registered scaffolded projects)[/bold]")
|
||||
for r in rows:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""wait_guard.py — T21 §21B: the sleep→poll regression detector.
|
||||
|
||||
Mirror of the MiMo Code controller guard (~/.mimocode/hooks/
|
||||
event-wait-guard.ts) in Python, so the CLASSIFICATION LOGIC is
|
||||
regression-tested in the Rune suite (rule 21B-4: the fix must survive
|
||||
model changes, compaction, provider swaps).
|
||||
|
||||
Detection: a command SEGMENT (split on ; | newline && ||) that combines
|
||||
a long sleep (>= 30s) with a status query of CI/deploy/release state.
|
||||
Heredoc bodies are stripped first (v2 precision fix — documentation
|
||||
about the pattern must not trip the guard).
|
||||
|
||||
Classification vocabulary (21B-1):
|
||||
USER_REQUESTED_DELAY bare sleep, user's explicit choice
|
||||
RATE_LIMIT_BACKOFF short bounded backoff (<30s)
|
||||
RETRY_BACKOFF bounded retry loop inside a script
|
||||
WATCHDOG deadline-based failure detection
|
||||
EVENT_FALLBACK_WITH_REASON declared no-event-source fallback
|
||||
SCHEDULED_TIMER cron/scheduled work
|
||||
UNJUSTIFIED_SLEEP DEFECT: sleep+poll with an event source available
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
LONG_SLEEP_RE = re.compile(r"\bsleep\s+(\d{2,4})\b")
|
||||
STATUS_POLL_RE = re.compile(
|
||||
"|".join(
|
||||
(
|
||||
r"actions/runs",
|
||||
r"commits/.*/status",
|
||||
r"required.checks",
|
||||
r"deploy_qualify",
|
||||
r"releases/tags",
|
||||
r"readlink.*rune-code/current",
|
||||
)
|
||||
),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
THRESHOLD_SECS = 30
|
||||
|
||||
|
||||
def classify_wait(command: str) -> dict:
|
||||
"""Classify one shell command's wait behaviour. Returns
|
||||
{"classification": ..., "evidence": ...}. UNJUSTIFIED_SLEEP is a
|
||||
defect: the controller guard rejects it and redirects to the
|
||||
canonical event primitive."""
|
||||
stripped = re.sub(
|
||||
r"<<-?\s*['\"]?(\w+)['\"]?\n[\s\S]*?\n\1", " [heredoc] ", command
|
||||
)
|
||||
segments = [s.strip() for s in
|
||||
re.split(r"[;|\n]|&&|\|\|", stripped) if s.strip()]
|
||||
for i, segment in enumerate(segments):
|
||||
m = LONG_SLEEP_RE.search(segment)
|
||||
if not m:
|
||||
continue
|
||||
secs = int(m.group(1))
|
||||
if secs < THRESHOLD_SECS:
|
||||
continue
|
||||
# the regression pattern is sleep THEN check: the status poll may
|
||||
# be in the same segment or the immediately following ones (the
|
||||
# "sleep; curl status" / "sleep && fetch && readlink" shapes)
|
||||
lookahead = " ".join(segments[i:i + 3])
|
||||
if STATUS_POLL_RE.search(lookahead):
|
||||
return {
|
||||
"classification": "UNJUSTIFIED_SLEEP",
|
||||
"evidence": f"sleep {secs}s then status poll: "
|
||||
f"{' / '.join(segments[i:i + 2])[:140]}",
|
||||
"redirect": "scripts/ci_event_wait.py --sha <sha>",
|
||||
}
|
||||
if re.match(r"^\s*sleep\s+\d+\s*$", stripped.strip()):
|
||||
return {"classification": "USER_REQUESTED_DELAY", "evidence": stripped.strip()}
|
||||
return {"classification": "NO_WAIT", "evidence": ""}
|
||||
@@ -0,0 +1,21 @@
|
||||
# 21B-1 — Rune sleep/poll audit (2026-09-16)
|
||||
|
||||
| Site | Classification | Rationale |
|
||||
|---|---|---|
|
||||
| ci_event_wait.py:82 `time.sleep(gap_secs)` | RETRY_BACKOFF | bounded authority_after_wake ack-window re-read (4s×3) |
|
||||
| auto_bump_pr.py:169/235 | POLLING (legacy path) | bump-PR CI wait — auto_bump PRs retired (direct bumps since 2026-09-08) |
|
||||
| merge_robot.py:120/217 | RETRY_BACKOFF | bounded retry on merge API attempts |
|
||||
| notify_event_bridge.py:51 | RETRY_BACKOFF | single 2s retry before best-effort declaration |
|
||||
| research_event_relay.py:152 | EVENT_FALLBACK_WITH_REASON | local file-read poll (not Gitea) |
|
||||
| t14/t15_builder_session.py | WATCHDOG-adjacent | notifier stderr drain interval |
|
||||
| event_wait.py:93 | EVENT_FALLBACK | notifier POST retry (3× best-effort) |
|
||||
| auto-update-gitea.yml:45 | SCHEDULED_TIMER | gitea binary update ordering |
|
||||
|
||||
**T20's own sleeps (`sleep 540/480/600`) were in the AGENT SESSION, not
|
||||
Rune code** — the model invented sleep→poll continuation instead of
|
||||
using ci_event_wait.py. That is a CONTROLLER-level defect: the MiMo
|
||||
Code execution machinery accepted `sleep N; check` when a supported
|
||||
event source existed.
|
||||
|
||||
MiMo Code controller located: /home/nick/.local/share/npm/lib/node_modules/@mimo-ai/cli
|
||||
(runs as same package for both nick and gitea-desktop-runner).
|
||||
@@ -0,0 +1,31 @@
|
||||
# 21B — MiMo Code controller sleep/poll audit + guard (2026-09-16)
|
||||
|
||||
## Controller identity (verified, not assumed)
|
||||
- Package: @mimo-ai/cli (npm), launcher at
|
||||
/home/nick/.local/share/npm/lib/node_modules/@mimo-ai/cli/bin/mimo
|
||||
- Runtime: compiled native binary (ELF x86-64, Bun-compiled, not stripped)
|
||||
at .../@mimo-ai/mimocode-linux-x64/bin/mimo — NOT source-patchable.
|
||||
- Extension surface: ~/.mimocode/hooks/*.ts via @mimo-ai/plugin Hooks API
|
||||
(tool.execute.before with cancel). Hot-reloads per turn.
|
||||
Precedent: no-blocking-wait.ts (2026-09-07), production-proven.
|
||||
|
||||
## Controller sleep/poll audit
|
||||
- The controller binary has no extension-surface polling loops. The T20
|
||||
regression (`sleep 540; <CI status probe>`) was MODEL-GENERATED Bash
|
||||
passed through the controller's shell execution — the correct
|
||||
interception point is tool.execute.before. Guard = event-wait-guard.ts.
|
||||
|
||||
## LIVE PROOF (LIVE_PROVEN_THIS_SESSION)
|
||||
- v1 guard written mid-session; hot-reloaded IMMEDIATELY (same turn):
|
||||
it intercepted the very next Bash tool call — the guard's own evidence
|
||||
file write — because the heredoc contained the literal text "sleep 540".
|
||||
- False positive acknowledged and fixed in v2: pattern detection now
|
||||
requires sleep + status-poll in the SAME command segment, with heredoc
|
||||
bodies stripped before scanning.
|
||||
- The interception itself is the live proof: the harness accepted the
|
||||
hook, matched the pattern, cancelled the tool call, and delivered the
|
||||
redirect message — without any controller restart.
|
||||
|
||||
## Rune-side classification (21B-1)
|
||||
See 21B1_rune_sleep_audit.md — no UNJUSTIFIED_SLEEP in active paths;
|
||||
auto_bump_pr.py polling loops classified legacy (21G candidate).
|
||||
@@ -24,8 +24,39 @@
|
||||
"scripts/deploy_stage.py",
|
||||
"scripts/deploy_promotion.py"
|
||||
],
|
||||
"spec": {
|
||||
"acceptance_criteria": [
|
||||
"deployment identity re-read from authority"
|
||||
],
|
||||
"baseline_reason": "monotonic + fail-closed proven across T16-T20; contract stable",
|
||||
"expected_behaviours": [
|
||||
"red SHA refused",
|
||||
"stale superseded truthfully",
|
||||
"current/last-good ordered"
|
||||
],
|
||||
"governance_version_started_at": "2026-09-16T03:59:49Z",
|
||||
"non_goals": [
|
||||
"no live-tree mutation"
|
||||
],
|
||||
"requirements": [
|
||||
"monotonic promotion, fail-closed"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "0.4.0",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-16T03:59:49Z",
|
||||
"changed_sections": [],
|
||||
"kind": "BASELINE",
|
||||
"reason": "monotonic + fail-closed proven across T16-T20; contract stable",
|
||||
"validation_consequences": [],
|
||||
"version": "0.4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_touched": "deploy root + current/last-good symlinks",
|
||||
"updated_at": "2026-09-16T01:26:33Z"
|
||||
"updated_at": "2026-09-16T03:59:49Z"
|
||||
},
|
||||
"capability:event-wait-resume": {
|
||||
"behaviour_specs": [
|
||||
@@ -54,8 +85,39 @@
|
||||
"source_locations": [
|
||||
"src/rune/event_wait.py"
|
||||
],
|
||||
"spec": {
|
||||
"acceptance_criteria": [
|
||||
"watchdog only fires when events are lost"
|
||||
],
|
||||
"baseline_reason": "cross-user + replay proven in T17C/T19; contract stable",
|
||||
"expected_behaviours": [
|
||||
"wake mid-wait",
|
||||
"durable-record recovery",
|
||||
"replay before registration"
|
||||
],
|
||||
"governance_version_started_at": "2026-09-16T03:59:49Z",
|
||||
"non_goals": [
|
||||
"no polling loops"
|
||||
],
|
||||
"requirements": [
|
||||
"long operations resume on event, not timeout"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "0.3.0",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-16T03:59:49Z",
|
||||
"changed_sections": [],
|
||||
"kind": "BASELINE",
|
||||
"reason": "cross-user + replay proven in T17C/T19; contract stable",
|
||||
"validation_consequences": [],
|
||||
"version": "0.3.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_touched": "W/R bridge waits (shared root)",
|
||||
"updated_at": "2026-09-16T01:26:33Z"
|
||||
"updated_at": "2026-09-16T03:59:49Z"
|
||||
},
|
||||
"capability:operator-send-task": {
|
||||
"behaviour_specs": [
|
||||
@@ -91,8 +153,39 @@
|
||||
"source_locations": [
|
||||
"src/rune/rune_operator.py"
|
||||
],
|
||||
"spec": {
|
||||
"acceptance_criteria": [
|
||||
"route exists in route.jsonl for every task"
|
||||
],
|
||||
"baseline_reason": "canonical authority proven; contract stable since T17F",
|
||||
"expected_behaviours": [
|
||||
"task record created",
|
||||
"route decided before execution",
|
||||
"construction activation recorded"
|
||||
],
|
||||
"governance_version_started_at": "2026-09-16T03:59:49Z",
|
||||
"non_goals": [
|
||||
"no provider selection"
|
||||
],
|
||||
"requirements": [
|
||||
"description routes through classification and construction activation"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "0.2.0",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-16T03:59:49Z",
|
||||
"changed_sections": [],
|
||||
"kind": "BASELINE",
|
||||
"reason": "canonical authority proven; contract stable since T17F",
|
||||
"validation_consequences": [],
|
||||
"version": "0.2.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_touched": "TaskStateRepository (canonical), operator.json (legacy cache)",
|
||||
"updated_at": "2026-09-16T01:55:10Z"
|
||||
"updated_at": "2026-09-16T03:59:49Z"
|
||||
},
|
||||
"capability:tui-testing-command": {
|
||||
"behaviour_specs": [
|
||||
@@ -119,8 +212,37 @@
|
||||
"source_locations": [
|
||||
"src/rune/rune_tui.py"
|
||||
],
|
||||
"spec": {
|
||||
"acceptance_criteria": [
|
||||
"backend is authority"
|
||||
],
|
||||
"baseline_reason": "new capability T19; young contract",
|
||||
"expected_behaviours": [
|
||||
"counts match ledger outstanding()"
|
||||
],
|
||||
"governance_version_started_at": "2026-09-16T03:59:49Z",
|
||||
"non_goals": [
|
||||
"no TUI polish"
|
||||
],
|
||||
"requirements": [
|
||||
"aggregate + per-project outstanding validations"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "0.1.0",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-16T03:59:49Z",
|
||||
"changed_sections": [],
|
||||
"kind": "BASELINE",
|
||||
"reason": "new capability T19; young contract",
|
||||
"validation_consequences": [],
|
||||
"version": "0.1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_touched": "",
|
||||
"updated_at": "2026-09-16T01:26:33Z"
|
||||
"updated_at": "2026-09-16T03:59:49Z"
|
||||
}
|
||||
},
|
||||
"history": [
|
||||
@@ -134,5 +256,5 @@
|
||||
}
|
||||
],
|
||||
"schema_version": 1,
|
||||
"updated_at": "2026-09-16T01:55:10Z"
|
||||
"updated_at": "2026-09-16T03:59:49Z"
|
||||
}
|
||||
+290
-150
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,199 @@
|
||||
"""test_living_specs.py — T21 §21C/21D/21E/21N: living capability specs,
|
||||
strict internal SemVer, automatic maintenance, version-aware quicksand."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import behaviour_ledger as bl # noqa: E402
|
||||
from rune import capability_ledger as cl # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def world(tmp_path):
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
l.define(capability_id="event-core", name="Event core",
|
||||
purpose="event lifecycle", owning_project="rune",
|
||||
component="component:event_bridge",
|
||||
source_locations=["src/events/core.py"],
|
||||
contract="in→out, exactly-once completion")
|
||||
return l
|
||||
|
||||
|
||||
def test_21c_attach_spec_baseline(world):
|
||||
spec = cl.attach_spec(world, "event-core",
|
||||
requirements=["events complete exactly once"],
|
||||
expected_behaviours=["completion observable"],
|
||||
non_goals=["no at-least-once yet"],
|
||||
acceptance_criteria=["VAL proves completion"],
|
||||
baseline_version="0.1.0",
|
||||
baseline_reason="governance starts; contract young")
|
||||
assert spec["version"] == "0.1.0"
|
||||
assert spec["governance_version_started_at"]
|
||||
rec = world.get("event-core")
|
||||
assert rec["spec"]["version_history"][0]["kind"] == "BASELINE"
|
||||
|
||||
|
||||
def test_21d_patch_implementation_only(world):
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
r = cl.revise_spec(world, "event-core", implementation_only=True,
|
||||
reason="fixed inner loop bug; contract unchanged")
|
||||
assert (r["old"], r["new"], r["kind"]) == ("0.1.0", "0.1.1", "PATCH")
|
||||
|
||||
|
||||
def test_21d_minor_backwards_compatible_addition(world):
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
r = cl.revise_spec(world, "event-core", behaviour_added=True,
|
||||
reason="added drain() behaviour, contract-compatible")
|
||||
assert r["kind"] == "MINOR" and r["new"] == "0.2.0"
|
||||
|
||||
|
||||
def test_21d_major_break(world):
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
r = cl.revise_spec(world, "event-core", consumer_break=True,
|
||||
reason="completion payload shape changed",
|
||||
affected_consumers=["component:consumer_b"])
|
||||
assert r["kind"] == "MAJOR" and r["new"] == "1.0.0"
|
||||
|
||||
|
||||
def test_21d_no_change_no_bump(world):
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
with pytest.raises(cl.SpecError):
|
||||
cl.revise_spec(world, "event-core", reason="metadata churn")
|
||||
|
||||
|
||||
def test_21d_unjustified_proposed_bump_refused(world):
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
with pytest.raises(cl.SpecError):
|
||||
cl.revise_spec(world, "event-core", implementation_only=True,
|
||||
proposed_kind="MAJOR", reason="I feel like it")
|
||||
|
||||
|
||||
def test_21e_new_capability_full_chain(world, tmp_path):
|
||||
"""21E-1: new capability → spec + ledger + behaviour link."""
|
||||
world.define(capability_id="drain", name="Drain",
|
||||
purpose="drain pending events", owning_project="rune",
|
||||
component="component:event_bridge",
|
||||
source_locations=["src/events/drain.py"],
|
||||
behaviour_specs=["behaviour:VAL-DRAIN-1"])
|
||||
cl.attach_spec(world, "drain", requirements=["drains all"],
|
||||
expected_behaviours=["drain returns count"],
|
||||
non_goals=[], acceptance_criteria=["count correct"],
|
||||
baseline_version="0.1.0", baseline_reason="new capability")
|
||||
beh = bl.BehaviourLedger(tmp_path)
|
||||
beh.define(behaviour_id="VAL-DRAIN-1", owning_project="rune",
|
||||
intended_behaviour="drain returns pending count",
|
||||
owner_component="component:event_bridge",
|
||||
dependencies=["component:event_bridge"])
|
||||
beh.record_auto_verification("VAL-DRAIN-1", passed=True)
|
||||
assert world.get("drain")["spec"]["version"] == "0.1.0"
|
||||
assert beh.get("VAL-DRAIN-1")["state"] == bl.AUTO_VERIFIED
|
||||
|
||||
|
||||
def test_21e_move_preserves_identity_version_history(world):
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
cl.revise_spec(world, "event-core", implementation_only=True,
|
||||
reason="pre-move fix")
|
||||
world.move("event-core", ["src/events/moved/core.py"])
|
||||
rec = world.get("event-core")
|
||||
assert rec["capability_id"] == "capability:event-core"
|
||||
assert rec["spec"]["version"] == "0.1.1"
|
||||
assert len(rec["spec"]["version_history"]) == 2
|
||||
assert any(h["event"] == "moved" for h in world.doc["history"])
|
||||
|
||||
|
||||
def test_21n_version_aware_invalidation(world, tmp_path):
|
||||
"""MAJOR break on the capability → behaviours proven against the old
|
||||
version reopen with a version-referenced reason."""
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
beh = bl.BehaviourLedger(tmp_path)
|
||||
beh.define(behaviour_id="VAL-EV-1", owning_project="rune",
|
||||
intended_behaviour="exactly-once completion",
|
||||
owner_component="component:event_bridge",
|
||||
dependencies=["component:event_bridge"])
|
||||
beh.record_auto_verification("VAL-EV-1", passed=True)
|
||||
beh.record_real_world("VAL-EV-1", passed=True, generation="cap-0.1.0")
|
||||
r = cl.revise_spec(world, "event-core", consumer_break=True,
|
||||
reason="payload shape v2",
|
||||
affected_consumers=["component:consumer_b"])
|
||||
assert r["kind"] == "MAJOR"
|
||||
inv = beh.invalidate_for_change(
|
||||
changed_entities={"component:event_bridge"}, generation="cap-1.0.0",
|
||||
reasons={"behaviour:VAL-EV-1":
|
||||
f"capability event-core {r['old']}→{r['new']} MAJOR: "
|
||||
f"payload shape changed; proven against {r['old']}"})
|
||||
assert [x["behaviour_id"] for x in inv] == ["behaviour:VAL-EV-1"]
|
||||
rec = beh.get("VAL-EV-1")
|
||||
assert rec["state"] == bl.RETEST_REQUIRED
|
||||
assert "0.1.0" in rec["invalidations"][-1]["because"]
|
||||
# history: proven against old version remains queryable
|
||||
assert any(h.get("generation") == "cap-0.1.0"
|
||||
for h in beh.retest_history("VAL-EV-1"))
|
||||
|
||||
|
||||
def test_21n_unrelated_capability_version_stable(world):
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
world.define(capability_id="other", name="Other", purpose="o",
|
||||
owning_project="rune", component="component:other")
|
||||
cl.attach_spec(world, "other", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
cl.revise_spec(world, "event-core", implementation_only=True,
|
||||
reason="unrelated fix")
|
||||
assert world.get("other")["spec"]["version"] == "0.1.0" # untouched
|
||||
|
||||
|
||||
def test_21o_spec_drift_detection(world, tmp_path):
|
||||
"""21O: source/contract changes without a spec revision must be
|
||||
detectable — generation marker on the spec vs the capability."""
|
||||
cl.attach_spec(world, "event-core", requirements=["r"],
|
||||
expected_behaviours=["b"], non_goals=[],
|
||||
acceptance_criteria=["a"])
|
||||
rec = world.get("event-core")
|
||||
# someone changes the source WITHOUT revising the spec
|
||||
world.material_change("event-core", note="rewrote completion path",
|
||||
generation="gen-after-spec")
|
||||
# same-second timestamps make wall-clock ordering racey (all events
|
||||
# within one second share the stamp); compare against the material
|
||||
# change itself: material_change bumps updated_at and appends
|
||||
# history, but the spec's LAST BUMP stays at the baseline time.
|
||||
rec = world.get("event-core")
|
||||
n_bumps_before = len(rec["spec"]["version_history"])
|
||||
world.material_change("event-core", note="rewrote path",
|
||||
generation="gen-x")
|
||||
rec = world.get("event-core")
|
||||
# material change without a revise: NO new bump recorded
|
||||
assert len(rec["spec"]["version_history"]) == n_bumps_before, (
|
||||
"material change must not fabricate a version bump")
|
||||
# the drift signal, deterministically: the capability's material
|
||||
# generation is NEWER than the spec's last governed bump — captured
|
||||
# on the record, not inferred from a raced clock
|
||||
assert rec["updated_at"] >= rec["spec"]["version_history"][-1]["at"]
|
||||
# and the control: a revise records a new bump and closes the gap
|
||||
r = cl.revise_spec(world, "event-core", implementation_only=True,
|
||||
reason="catch up")
|
||||
rec2 = world.get("event-core")
|
||||
assert rec2["spec"]["version"] == r["new"]
|
||||
assert len(rec2["spec"]["version_history"]) == n_bumps_before + 1
|
||||
@@ -0,0 +1,102 @@
|
||||
"""test_task_state_ordering.py — T21 §21I: canonical-first ordering with
|
||||
bounded fault injection. Canonical truth must survive every crash point."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune.orchestration.atomic import AtomicStore # noqa: E402
|
||||
from rune.orchestration.conversation_state import TaskStateRepository # noqa: E402
|
||||
from rune.orchestration.event_store import DurableEventStore # noqa: E402
|
||||
from rune.orchestration.records import TaskStateRecord # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def repo(tmp_path):
|
||||
return TaskStateRepository(
|
||||
DurableEventStore(str(tmp_path / "e.jsonl")),
|
||||
AtomicStore(tmp_path / "hot"))
|
||||
|
||||
|
||||
def _seed(repo, tid="t-1", channel="ch"):
|
||||
repo.import_record(TaskStateRecord(task_id=tid, title=tid,
|
||||
status="running", channel_id=channel))
|
||||
return tid
|
||||
|
||||
|
||||
# ── crash-point semantics (the invariant, modeled) ───────────────────────
|
||||
def test_21i_crash_after_transition_before_cache_save(repo):
|
||||
"""Canonical-first: transition commits, cache save never runs.
|
||||
Canonical says completed; the cache (absent/stale) is backfillable.
|
||||
TRUTH = canonical. This is the SAFE failure direction."""
|
||||
tid = _seed(repo)
|
||||
repo.transition(tid, "completed") # commits durably
|
||||
# simulate crash before cache save: canonical reads completed
|
||||
assert repo.get(tid).status == "completed"
|
||||
# the cache may claim running — canonical still wins on read
|
||||
|
||||
|
||||
def test_21i_crash_before_transition_with_old_order(repo):
|
||||
"""The OLD (pre-T21) order: cache saved 'completed' to disk, crash
|
||||
before canonical transition. Canonical still says running — the
|
||||
cache was AHEAD of truth. The fix eliminates this direction: the
|
||||
canonical transition now precedes any cache save in the operator
|
||||
terminal path (rune_operator _apply_runtime_result ordering)."""
|
||||
tid = _seed(repo)
|
||||
# canonical unchanged (transition never ran)
|
||||
assert repo.get(tid).status == "running"
|
||||
# under the new ordering this state is impossible for a COMPLETED
|
||||
# task: the cache cannot outrun the durable repo
|
||||
|
||||
|
||||
def test_21i_projection_write_failure_leaves_canonical(repo, tmp_path):
|
||||
tid = _seed(repo)
|
||||
repo.transition(tid, "completed")
|
||||
# a projection-read failure must not change canonical truth
|
||||
assert repo.get(tid).status == "completed"
|
||||
assert [r.task_id for r in repo.list_all() if r.task_id == tid]
|
||||
|
||||
|
||||
def test_21i_restart_after_either_case(repo, tmp_path):
|
||||
"""Restart resilience: the durable event log + hot checkpoint
|
||||
reconstruct canonical state after a fresh repo instance."""
|
||||
tid = _seed(repo)
|
||||
repo.transition(tid, "completed")
|
||||
# fresh instance over the same stores (simulated restart)
|
||||
repo2 = TaskStateRepository(
|
||||
DurableEventStore(str(tmp_path / "e.jsonl")),
|
||||
AtomicStore(tmp_path / "hot"))
|
||||
assert repo2.get(tid).status == "completed"
|
||||
|
||||
|
||||
def test_21i_counter_fallback_cannot_mask_truth(repo):
|
||||
"""_task_counters derives from the repo when records exist — the
|
||||
legacy-cache fallback fires only for a genuinely empty repo (T17F)."""
|
||||
tid = _seed(repo)
|
||||
repo.transition(tid, "completed")
|
||||
recs = repo.list_all()
|
||||
assert any(r.task_id == tid and r.status == "completed" for r in recs)
|
||||
|
||||
|
||||
def test_21i_operator_source_has_canonical_first_order():
|
||||
"""STRUCTURAL GUARD: the operator's terminal path must transition
|
||||
BEFORE saving the cache (T21 21I fix). Greps the real source."""
|
||||
src = (REPO / "src/rune/rune_operator.py").read_text()
|
||||
# find the _apply_runtime_result block
|
||||
idx = src.find("def _apply_runtime_result")
|
||||
assert idx > 0
|
||||
block = src[idx:idx + 6000]
|
||||
save_at = block.find("self._save()")
|
||||
trans_at = block.find('self._task_state.transition(task.task_id, "completed")')
|
||||
assert trans_at > 0, "terminal transition missing"
|
||||
if save_at > 0:
|
||||
assert trans_at < save_at, (
|
||||
"canonical transition must precede cache save in the terminal "
|
||||
"path (21I crash-window fix regressed)")
|
||||
@@ -0,0 +1,65 @@
|
||||
"""test_wait_guard.py — T21 §21B-4: sleep→poll regression protection.
|
||||
Held-outs reproduce the EXACT T20 patterns (raw execution evidence)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import wait_guard as wg # noqa: E402
|
||||
|
||||
|
||||
def test_21b_t20_pattern_540_blocked():
|
||||
"""The EXACT T20 pattern: sleep 540 then CI status probe."""
|
||||
cmd = "sleep 540; curl -s $GITEA/api/v1/repos/conductor/rune/commits/abc/status"
|
||||
r = wg.classify_wait(cmd)
|
||||
assert r["classification"] == "UNJUSTIFIED_SLEEP"
|
||||
assert r["redirect"].startswith("scripts/ci_event_wait.py")
|
||||
|
||||
|
||||
def test_21b_t20_pattern_deploy_probe_blocked():
|
||||
cmd = "sleep 300 && git fetch && readlink /mnt/workdrive/rune-code/current"
|
||||
assert wg.classify_wait(cmd)["classification"] == "UNJUSTIFIED_SLEEP"
|
||||
|
||||
|
||||
def test_21b_t20_pattern_release_probe_blocked():
|
||||
cmd = "sleep 600; curl -s https://gitea.example/api/v1/repos/x/y/releases/tags/v1"
|
||||
assert wg.classify_wait(cmd)["classification"] == "UNJUSTIFIED_SLEEP"
|
||||
|
||||
|
||||
def test_21b_heredoc_documentation_not_blocked():
|
||||
"""v2 precision fix (live finding): writing documentation containing
|
||||
the literal pattern text must NOT trip the guard."""
|
||||
cmd = (
|
||||
"cat > evidence.md <<'EOF'\n"
|
||||
"T20 raw execution contained: sleep 540 then actions/runs probe\n"
|
||||
"EOF\n"
|
||||
"echo saved"
|
||||
)
|
||||
assert wg.classify_wait(cmd)["classification"] == "NO_WAIT"
|
||||
|
||||
|
||||
def test_21b_bare_sleep_is_user_delay():
|
||||
assert wg.classify_wait("sleep 45")["classification"] == "USER_REQUESTED_DELAY"
|
||||
|
||||
|
||||
def test_21b_short_backoff_permitted():
|
||||
cmd = "curl -s $API/status; sleep 4; curl -s $API/status"
|
||||
r = wg.classify_wait(cmd)
|
||||
assert r["classification"] == "NO_WAIT" # sleeps below threshold
|
||||
|
||||
|
||||
def test_21b_non_polling_long_sleep_permitted():
|
||||
"""Long sleep with NO status poll in the segment: not our defect."""
|
||||
cmd = "sleep 120; ./render_frames.sh"
|
||||
assert wg.classify_wait(cmd)["classification"] == "NO_WAIT"
|
||||
|
||||
|
||||
def test_21b_segment_isolation():
|
||||
"""sleep+poll must co-occur in the SAME segment: a long sleep in one
|
||||
segment and an unrelated status read in another is not the pattern."""
|
||||
cmd = "sleep 900\nmake build"
|
||||
assert wg.classify_wait(cmd)["classification"] == "NO_WAIT"
|
||||
Reference in new issue
Block a user