2026-11-03 13:43:15 +01:00
|
|
|
"""Validate the kvb-dream reminder, its GNOME notification, timer and settings registration."""
|
2026-08-02 19:34:05 +02:00
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
2026-11-03 13:43:15 +01:00
|
|
|
DREAM = REPO_ROOT / "skills" / "kvb-dream"
|
|
|
|
|
SKILL = DREAM / "SKILL.md"
|
|
|
|
|
HOOK = DREAM / "hooks" / "reminder.sh"
|
|
|
|
|
NOTIFY = DREAM / "hooks" / "notify.sh"
|
|
|
|
|
TIMER = DREAM / "systemd" / "kvb-dream-notify.timer"
|
|
|
|
|
SERVICE = DREAM / "systemd" / "kvb-dream-notify.service"
|
2026-08-02 19:34:05 +02:00
|
|
|
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestDreamSkill(unittest.TestCase):
|
2026-11-03 13:43:15 +01:00
|
|
|
def test_skill_runs_optimize_and_the_cleanups(self):
|
2026-08-02 19:34:05 +02:00
|
|
|
text = SKILL.read_text(encoding="utf-8")
|
2026-11-03 13:43:15 +01:00
|
|
|
self.assertIn("name: kvb-dream", text)
|
2026-08-02 19:34:05 +02:00
|
|
|
self.assertIn("multiSelect: false", text)
|
2026-11-03 13:43:15 +01:00
|
|
|
for skill in ("`kvb-optimize`", "`kvb-cleanup`", "`kvb-git-branch-gc`"):
|
|
|
|
|
self.assertIn(skill, text)
|
|
|
|
|
|
|
|
|
|
def test_optimize_covers_every_optimization_skill(self):
|
|
|
|
|
text = (REPO_ROOT / "skills" / "kvb-optimize" / "SKILL.md").read_text(encoding="utf-8")
|
|
|
|
|
for skill in (
|
|
|
|
|
"kvb-skill-cleanup",
|
|
|
|
|
"kvb-transcend",
|
|
|
|
|
"kvb-skill-auto",
|
|
|
|
|
"kvb-skill-tune",
|
|
|
|
|
"kvb-skill-compile",
|
|
|
|
|
"kvb-skill-essence",
|
|
|
|
|
"kvb-code-refactor",
|
|
|
|
|
"kvb-code-tune",
|
2026-11-03 21:07:50 +01:00
|
|
|
"kvb-agent-optimize",
|
2026-11-03 13:43:15 +01:00
|
|
|
"kvb-think-gaps",
|
|
|
|
|
):
|
|
|
|
|
self.assertIn(f"`{skill}`", text)
|
2026-08-02 19:34:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestDreamHook(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
|
|
|
self.addCleanup(self.tmp.cleanup)
|
2026-11-03 13:43:15 +01:00
|
|
|
self.stamps = Path(self.tmp.name) / "stamps"
|
2026-08-02 19:34:05 +02:00
|
|
|
|
2026-11-03 13:43:15 +01:00
|
|
|
def _run(self, payload: str, interval: str = "43200") -> str:
|
|
|
|
|
return subprocess.run(
|
|
|
|
|
["bash", str(HOOK)],
|
|
|
|
|
input=payload,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
check=True,
|
|
|
|
|
env={
|
|
|
|
|
"PATH": "/usr/bin:/bin",
|
|
|
|
|
"HOME": self.tmp.name,
|
|
|
|
|
"DREAM_STAMP_DIR": str(self.stamps),
|
|
|
|
|
"DREAM_INTERVAL_SECONDS": interval,
|
|
|
|
|
},
|
|
|
|
|
).stdout
|
2026-08-02 19:34:05 +02:00
|
|
|
|
2026-11-03 13:43:15 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def _session(sid: str) -> str:
|
|
|
|
|
return json.dumps({"session_id": sid, "prompt": "hi"})
|
|
|
|
|
|
|
|
|
|
def test_first_run_hints_and_stamps_the_session(self):
|
|
|
|
|
self.assertIn("/kvb-dream", self._run(self._session("a1")))
|
|
|
|
|
self.assertTrue((self.stamps / "a1").is_file())
|
|
|
|
|
|
|
|
|
|
def test_second_run_of_the_same_agent_is_silent(self):
|
|
|
|
|
self._run(self._session("a1"))
|
|
|
|
|
self.assertEqual(self._run(self._session("a1")), "")
|
|
|
|
|
|
|
|
|
|
def test_every_agent_gets_its_own_hint(self):
|
|
|
|
|
self._run(self._session("a1"))
|
|
|
|
|
self.assertIn("/kvb-dream", self._run(self._session("b2")))
|
2026-08-02 19:34:05 +02:00
|
|
|
|
|
|
|
|
def test_hint_returns_after_the_interval(self):
|
2026-11-03 13:43:15 +01:00
|
|
|
self._run(self._session("a1"))
|
|
|
|
|
self.assertIn("/kvb-dream", self._run(self._session("a1"), interval="0"))
|
2026-08-02 19:34:05 +02:00
|
|
|
|
|
|
|
|
def test_corrupt_stamp_does_not_crash(self):
|
2026-11-03 13:43:15 +01:00
|
|
|
self.stamps.mkdir()
|
|
|
|
|
(self.stamps / "a1").write_text("not-a-timestamp\n", encoding="utf-8")
|
|
|
|
|
self.assertIn("/kvb-dream", self._run(self._session("a1")))
|
2026-08-02 19:34:05 +02:00
|
|
|
|
2026-11-03 13:43:15 +01:00
|
|
|
def test_without_a_valid_session_it_stays_silent(self):
|
|
|
|
|
for payload in ("", "not json", self._session(""), self._session("../escape")):
|
|
|
|
|
with self.subTest(payload=payload):
|
|
|
|
|
self.assertEqual(self._run(payload), "")
|
|
|
|
|
self.assertFalse(self.stamps.exists())
|
2026-08-02 19:34:05 +02:00
|
|
|
|
|
|
|
|
|
2026-11-03 13:43:15 +01:00
|
|
|
class TestDreamNotification(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
|
|
|
self.addCleanup(self.tmp.cleanup)
|
|
|
|
|
self.bin = Path(self.tmp.name) / "bin"
|
|
|
|
|
self.bin.mkdir()
|
|
|
|
|
self.calls = Path(self.tmp.name) / "calls"
|
|
|
|
|
fake = self.bin / "notify-send"
|
|
|
|
|
fake.write_text(f'#!/bin/sh\nprintf "%s\\n" "$@" >> "{self.calls}"\n', encoding="utf-8")
|
|
|
|
|
fake.chmod(0o755)
|
|
|
|
|
|
|
|
|
|
def _run(self, desktop: str) -> None:
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["bash", str(NOTIFY)],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
check=True,
|
|
|
|
|
env={"PATH": f"{self.bin}:/usr/bin:/bin", "XDG_CURRENT_DESKTOP": desktop},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_gnome_session_gets_the_popup(self):
|
|
|
|
|
self._run("ubuntu:GNOME")
|
|
|
|
|
self.assertIn("/kvb-dream", self.calls.read_text(encoding="utf-8"))
|
|
|
|
|
|
|
|
|
|
def test_other_desktops_stay_silent(self):
|
|
|
|
|
self._run("KDE")
|
|
|
|
|
self.assertFalse(self.calls.exists())
|
|
|
|
|
|
|
|
|
|
def test_timer_fires_daily_at_five_pm_and_catches_up(self):
|
|
|
|
|
timer = TIMER.read_text(encoding="utf-8")
|
|
|
|
|
self.assertIn("OnCalendar=*-*-* 17:00:00", timer)
|
|
|
|
|
self.assertIn("Persistent=true", timer)
|
|
|
|
|
self.assertIn("kvb-dream/hooks/notify.sh", SERVICE.read_text(encoding="utf-8"))
|
2026-10-30 15:17:45 +01:00
|
|
|
|
2026-08-02 19:34:05 +02:00
|
|
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("node"), "node not installed")
|
|
|
|
|
class TestSettingsRegistration(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
|
|
|
self.addCleanup(self.tmp.cleanup)
|
|
|
|
|
self.settings = Path(self.tmp.name) / "settings.json"
|
|
|
|
|
|
|
|
|
|
def _patch(self) -> dict:
|
2026-08-10 17:13:29 +02:00
|
|
|
subprocess.run(
|
|
|
|
|
["node", str(PATCHER), str(self.settings)], check=True, capture_output=True
|
|
|
|
|
)
|
2026-08-02 19:34:05 +02:00
|
|
|
return json.loads(self.settings.read_text(encoding="utf-8"))
|
|
|
|
|
|
|
|
|
|
def test_hook_registered_once(self):
|
|
|
|
|
self._patch()
|
|
|
|
|
data = self._patch()
|
|
|
|
|
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
|
2026-11-03 13:43:15 +01:00
|
|
|
self.assertEqual(entries.count("kvb-dream/hooks/reminder.sh"), 1)
|
2026-11-03 15:45:57 +01:00
|
|
|
self.assertEqual(entries.count("kvb-coach/hooks/reminder.sh"), 1)
|
2026-11-03 13:43:15 +01:00
|
|
|
self.assertNotIn("tune/hooks/reminder.sh", entries)
|
2026-10-30 15:23:32 +01:00
|
|
|
gate = json.dumps(data["hooks"]["PreToolUse"])
|
2026-10-31 23:46:28 +01:00
|
|
|
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
|
|
|
|
|
self.assertEqual(gate.count("kvb-org-organize/scripts/watch.py"), 1)
|
|
|
|
|
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
|
2026-10-30 17:37:47 +01:00
|
|
|
post = json.dumps(data["hooks"]["PostToolUse"])
|
2026-10-31 23:46:28 +01:00
|
|
|
self.assertEqual(post.count("kvb-org-organize/scripts/watch.py"), 1)
|
|
|
|
|
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
|
|
|
|
|
self.assertEqual(post.count("kvb-org-perform/scripts/log.py"), 1)
|
|
|
|
|
|
2026-11-03 13:43:15 +01:00
|
|
|
def test_existing_hooks_preserved(self):
|
|
|
|
|
self.settings.write_text(
|
|
|
|
|
json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "true"}]}]}}),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
data = self._patch()
|
|
|
|
|
self.assertIn('"true"', json.dumps(data["hooks"]["UserPromptSubmit"]))
|
|
|
|
|
|
|
|
|
|
def test_tune_reminders_retire_and_dream_reminders_are_repointed(self):
|
2026-10-31 23:46:28 +01:00
|
|
|
base = Path(self.tmp.name) / "skills"
|
|
|
|
|
|
|
|
|
|
def hook(command):
|
|
|
|
|
return {"hooks": [{"type": "command", "command": command}]}
|
|
|
|
|
|
|
|
|
|
self.settings.write_text(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"hooks": {
|
|
|
|
|
"UserPromptSubmit": [
|
|
|
|
|
hook(f'bash "{base}/autotune/hooks/reminder.sh"'),
|
2026-11-03 13:43:15 +01:00
|
|
|
hook(f'bash "{base}/kvb-skill-tune/hooks/reminder.sh"'),
|
|
|
|
|
hook(f'bash "{base}/kvb-skill-dream/hooks/reminder.sh"'),
|
2026-10-31 23:46:28 +01:00
|
|
|
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
|
|
|
|
|
hook("echo keep"),
|
|
|
|
|
],
|
|
|
|
|
"PreToolUse": [
|
|
|
|
|
hook(f'python3 "{base}/democracy/scripts/council.py" gate'),
|
|
|
|
|
hook(f'python3 "{base}/democracy/scripts/watch.py" block'),
|
|
|
|
|
hook(f'python3 "{base}/subagent-model/scripts/log.py" spawn'),
|
|
|
|
|
],
|
|
|
|
|
"PostToolUse": [
|
|
|
|
|
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
|
|
|
|
|
hook(f'python3 "{base}/subagent-model/scripts/log.py" persist'),
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
data = self._patch()
|
|
|
|
|
text = json.dumps(data["hooks"])
|
2026-11-03 13:43:15 +01:00
|
|
|
for old in ("/autotune/", "/kvb-skill-tune/", "/kvb-skill-dream/", "/democracy/", "/subagent-model/"):
|
2026-10-31 23:46:28 +01:00
|
|
|
self.assertNotIn(f"skills{old}", text)
|
|
|
|
|
self.assertIn("echo keep", text)
|
|
|
|
|
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
|
2026-11-03 13:43:15 +01:00
|
|
|
self.assertEqual(entries.count("kvb-dream/hooks/reminder.sh"), 1)
|
2026-10-31 23:46:28 +01:00
|
|
|
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
|
|
|
|
|
gate = json.dumps(data["hooks"]["PreToolUse"])
|
|
|
|
|
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
|
|
|
|
|
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
|
2026-08-02 19:34:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|