Files
rune/tests/test_cognitive_wiring.py
T
Nick 169cb63084
local-review.yml (push) Invalid workflow file
Auto-version and Release / release (push) Successful in 12s
CI Pipeline / lint (push) Failing after 31s
CI Pipeline / go-surface (push) Failing after 19s
CI Pipeline / rust-surface (push) Successful in 4m21s
CI Pipeline / unit-tests (push) Failing after 5m28s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / canary (push) Failing after 1m3s
CI Pipeline / type-check (push) Failing after 36s
CI Pipeline / gitea-offline (push) Successful in 18s
deploy / deploy (push) Failing after 15m11s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 8s
feat(tui): the cognitive loops are actually INVOKED — #2 (contract BUILDER-2026-10-08T13:15:49Z-03dc90)
- The defect: RuneApp instantiated CognitiveLoop/DreamCycle/AutonomyLoop and
  told the user '◆ Cognitive loop active — Rune thinks between turns' — but
  never called think()/dream()/evaluate() anywhere. A dead mechanism wearing
  a live-status costume.
- The fix: an idle-cognition ticker (set_interval 240s) on RuneApp — think()
  every tick (consolidate, probe dead weight, drift check) with the most
  recent user context, dream() every third tick (full consolidation), on a
  worker thread; only dim status lines touch the UI thread; failures degrade
  silently. Input submit captures _cognition_ctx.
- Bonus fix: module-level logger in rune_tui.py — 61 latent F821 NameErrors
  inside exception handlers (any suppressed-exception path that reached
  logger.debug would have raised instead of degrading). Now 0.
- tests/test_cognitive_wiring.py: 5/5 — think() invoked with context, dream()
  gated to the third tick, actions surface as status, silence stays silent,
  the wiring pinned at source level.
2026-10-08 14:20:41 +01:00

126 lines
4.2 KiB
Python

"""test_cognitive_wiring.py — #2: the cognitive loop is actually INVOKED.
The defect: RuneApp instantiated CognitiveLoop/DreamCycle but never called
think()/dream() — the "[dim]◆ Cognitive loop active[/dim]" message was a
dead mechanism. The fix: an idle-cognition ticker that invokes think() every
tick and dream() every third, off the render thread.
These tests pin the INVOCATION SEMANTICS with stub loops (no live TUI, no
live memory DBs): the tick must call think() with the captured context,
call dream() only on the third tick, and post status only for real actions.
"""
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
import importlib.util
spec = importlib.util.spec_from_file_location("rune_tui", ROOT / "src/rune/rune_tui.py")
rune_tui = importlib.util.module_from_spec(spec)
spec.loader.exec_module(rune_tui)
class _StubLoop:
def __init__(self):
self.calls = []
def think(self, context: str = "") -> dict:
self.calls.append(("think", context))
return {"actions": [{"type": "curiosity_probe", "probed": 5, "found": 2}]}
class _StubDream:
def __init__(self):
self.calls = 0
def dream(self) -> dict:
self.calls += 1
return {"actions": [{"type": "consolidation"}]}
class _StubApp:
"""The tick method's dependency surface, and nothing else."""
def __init__(self):
self.cognitive_loop = _StubLoop()
self.dream_cycle = _StubDream()
self.autonomy_loop = None
self._cognition_ctx = "hello world"
self._dream_count = 0
self.messages = []
def call_from_thread(fn, *args):
self.messages.append((fn, args))
self.call_from_thread = call_from_thread
class _Chan:
def __init__(self, sink):
self._sink = sink
def add_message(self, *args):
self._sink(("msg", args))
self.chat_channel = _Chan(self.messages.append)
_idle_cognition_tick = rune_tui.RuneApp._idle_cognition_tick
class TestCognitiveWiring(unittest.TestCase):
def _drain(self, app):
"""The tick spawns a worker thread; join it deterministically."""
import time
for _ in range(50):
time.sleep(0.05)
if not any(t.is_alive() for t in __import__("threading").enumerate() if t.name.startswith("Thread")):
break
time.sleep(0.2)
def test_tick_invokes_think_with_context(self):
app = _StubApp()
app._idle_cognition_tick()
self._drain(app)
self.assertEqual(app.cognitive_loop.calls, [("think", "hello world")])
def test_dream_only_on_third_tick(self):
app = _StubApp()
app._idle_cognition_tick()
self._drain(app)
self.assertEqual(app.dream_cycle.calls, 0, "dream must NOT fire on tick 1")
app._idle_cognition_tick()
self._drain(app)
self.assertEqual(app.dream_cycle.calls, 0)
app._idle_cognition_tick()
self._drain(app)
self.assertEqual(app.dream_cycle.calls, 1, "dream fires on the third tick")
def test_think_actions_surface_as_status(self):
app = _StubApp()
app._idle_cognition_tick()
self._drain(app)
texts = [args[1] for (_fn, args) in app.messages if isinstance(args, tuple) and len(args) >= 2]
self.assertTrue(any("curiosity_probe" in t for t in texts), f"real think actions must surface: {app.messages}")
def test_silent_loops_post_nothing(self):
app = _StubApp()
app.cognitive_loop = _StubLoop()
app.cognitive_loop.think = lambda context="": {"actions": []}
app._idle_cognition_tick()
self._drain(app)
texts = [args[1] for (_fn, args) in app.messages if isinstance(args, tuple) and len(args) >= 2]
self.assertEqual([t for t in texts if "think:" in t], [], "no actions -> no status spam")
def test_source_wires_the_ticker(self):
src = (ROOT / "src/rune/rune_tui.py").read_text()
self.assertIn("set_interval(240, self._idle_cognition_tick)", src)
self.assertIn("self._cognition_ctx = text", src, "input submit must capture context for the ticker")
if __name__ == "__main__":
unittest.main()