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
- 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.
126 lines
4.2 KiB
Python
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()
|