feat(hooks): tiered prompt injection — Tier 0 kernel + Tier 1 navmap by cadence (FPLAN-0284 P2-P4, DPLAN-0214)
Replaces the single 8k always-injected global prompt with cadence-tiered injection: - Tier 0 (.aipass/tier0_kernel.md, ~2k) injects EVERY turn — identity grounding, the drone --help reflex, disaster-preventer rules. Folds DPLAN-0213 C1 (faithful-reporting) + C2 (no-gold-plating sub-agent brief). - Tier 1 (.aipass/tier1_navmap.md, ~7.7k) injects every 5th turn + session-start + post-compaction — full agent roster, framework, conventions, plus a new Terminology section migrated from the S211 backup. - Old global_prompt loader retired (disabled in hooks.json, removed from cadence wiring); aipass_global_prompt.md kept as a reference snapshot. Engine (built by @hooks): per-loader period in cadence.py should_fire() (back-compatible: unset period falls back to global); new tier0_kernel + navmap handlers; bypass.json extended to cover the two new dynamically-dispatched handlers. 614/614 tests pass, seedgo @hooks 100%. Live-verified: tier0 fires every turn, navmap on turn 0/5/10, turn counter advances once per turn (not per loader), no double-injection. Machine-local wiring (cadence_config.json, ~/.claude/settings.json bridge) updated on this host; fresh-clone seeding of those is a tracked follow-up. PROMPT_STYLE.md reference updated to point at the tier files as canonical examples.
This commit is contained in:
@@ -86,6 +86,36 @@
|
||||
"standard": "json_structure",
|
||||
"reason": "No JSON operations \u2014 reads markdown file and outputs text."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/prompt/tier0_kernel.py",
|
||||
"standard": "dead_code",
|
||||
"reason": "Invoked dynamically by engine via importlib from hooks.json handler path 'aipass.hooks.apps.handlers.prompt.tier0_kernel.handle' - not statically imported by design. Wired in UserPromptSubmit.tier0_kernel (FPLAN-0284). Same pattern as global_loader.py."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/prompt/tier0_kernel.py",
|
||||
"standard": "unused_function",
|
||||
"reason": "handle() called dynamically by engine._run_handler via importlib.import_module + getattr from hooks.json. Wired in UserPromptSubmit.tier0_kernel (FPLAN-0284)."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/prompt/tier0_kernel.py",
|
||||
"standard": "json_structure",
|
||||
"reason": "No JSON operations - reads markdown file (.aipass/tier0_kernel.md) and outputs text."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/prompt/navmap.py",
|
||||
"standard": "dead_code",
|
||||
"reason": "Invoked dynamically by engine via importlib from hooks.json handler path 'aipass.hooks.apps.handlers.prompt.navmap.handle' - not statically imported by design. Wired in UserPromptSubmit.navmap (FPLAN-0284). Same pattern as global_loader.py."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/prompt/navmap.py",
|
||||
"standard": "unused_function",
|
||||
"reason": "handle() called dynamically by engine._run_handler via importlib.import_module + getattr from hooks.json. Wired in UserPromptSubmit.navmap (FPLAN-0284)."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/prompt/navmap.py",
|
||||
"standard": "json_structure",
|
||||
"reason": "No JSON operations - reads markdown file (.aipass/tier1_navmap.md) and outputs text."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/security/edit_gate.py",
|
||||
"standard": "dead_code",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: navmap.py
|
||||
# Version: 1.0.0
|
||||
# Description: Tier 1 navigation map — periodic prompt injection (UserPromptSubmit)
|
||||
# Branch: hooks
|
||||
# Layer: apps/handlers/prompt
|
||||
# Created: 2026-06-18
|
||||
# Modified: 2026-06-18
|
||||
# =============================================
|
||||
|
||||
"""Loads .aipass/tier1_navmap.md — richer navigation map injected periodically."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
def _find_project_file(filename: str) -> Path | None:
|
||||
"""Walk up from CWD to find the nearest .aipass/<filename>."""
|
||||
cwd = Path.cwd()
|
||||
for parent in [cwd, *cwd.parents]:
|
||||
candidate = parent / ".aipass" / filename
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
if parent == parent.parent:
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def handle(hook_data: dict) -> dict:
|
||||
"""Load tier1 navmap — periodic (cadence period 5) + turn 0 + post-compaction."""
|
||||
try:
|
||||
import importlib
|
||||
|
||||
cadence = importlib.import_module("aipass.hooks.apps.modules.cadence")
|
||||
if not cadence.should_fire("navmap", hook_data):
|
||||
return {"stdout": "", "exit_code": 0}
|
||||
except Exception as exc:
|
||||
logger.info("[HOOKS] navmap: cadence check failed, firing anyway: %s", exc)
|
||||
|
||||
try:
|
||||
aipass_home = os.environ.get("AIPASS_HOME", "")
|
||||
cwd = str(Path.cwd())
|
||||
|
||||
if aipass_home and cwd.startswith(aipass_home):
|
||||
prompt_file = Path(aipass_home) / ".aipass" / "tier1_navmap.md"
|
||||
else:
|
||||
prompt_file = _find_project_file("tier1_navmap.md")
|
||||
|
||||
if not prompt_file or not prompt_file.exists():
|
||||
return {"stdout": "", "exit_code": 0}
|
||||
|
||||
content = prompt_file.read_text(encoding="utf-8")
|
||||
return {"stdout": content, "exit_code": 0, "sound": "navmap"}
|
||||
|
||||
except Exception as exc:
|
||||
logger.info("[HOOKS] navmap: unexpected error: %s", exc)
|
||||
return {"stdout": "", "exit_code": 0}
|
||||
@@ -0,0 +1,59 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: tier0_kernel.py
|
||||
# Version: 1.0.0
|
||||
# Description: Tier 0 kernel — always-on minimal prompt injection (UserPromptSubmit)
|
||||
# Branch: hooks
|
||||
# Layer: apps/handlers/prompt
|
||||
# Created: 2026-06-18
|
||||
# Modified: 2026-06-18
|
||||
# =============================================
|
||||
|
||||
"""Loads .aipass/tier0_kernel.md — tiny always-on identity + reflex block."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
def _find_project_file(filename: str) -> Path | None:
|
||||
"""Walk up from CWD to find the nearest .aipass/<filename>."""
|
||||
cwd = Path.cwd()
|
||||
for parent in [cwd, *cwd.parents]:
|
||||
candidate = parent / ".aipass" / filename
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
if parent == parent.parent:
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def handle(hook_data: dict) -> dict:
|
||||
"""Load tier0 kernel — every turn (cadence period 1)."""
|
||||
try:
|
||||
import importlib
|
||||
|
||||
cadence = importlib.import_module("aipass.hooks.apps.modules.cadence")
|
||||
if not cadence.should_fire("tier0", hook_data):
|
||||
return {"stdout": "", "exit_code": 0}
|
||||
except Exception as exc:
|
||||
logger.info("[HOOKS] tier0_kernel: cadence check failed, firing anyway: %s", exc)
|
||||
|
||||
try:
|
||||
aipass_home = os.environ.get("AIPASS_HOME", "")
|
||||
cwd = str(Path.cwd())
|
||||
|
||||
if aipass_home and cwd.startswith(aipass_home):
|
||||
prompt_file = Path(aipass_home) / ".aipass" / "tier0_kernel.md"
|
||||
else:
|
||||
prompt_file = _find_project_file("tier0_kernel.md")
|
||||
|
||||
if not prompt_file or not prompt_file.exists():
|
||||
return {"stdout": "", "exit_code": 0}
|
||||
|
||||
content = prompt_file.read_text(encoding="utf-8")
|
||||
return {"stdout": content, "exit_code": 0, "sound": "tier0 kernel"}
|
||||
|
||||
except Exception as exc:
|
||||
logger.info("[HOOKS] tier0_kernel: unexpected error: %s", exc)
|
||||
return {"stdout": "", "exit_code": 0}
|
||||
@@ -196,13 +196,15 @@ def should_fire(loader_name: str, hook_data: dict | None = None) -> bool:
|
||||
if not config.get("enabled", True):
|
||||
return True
|
||||
|
||||
period = config.get("period", 5)
|
||||
if period <= 0:
|
||||
return True
|
||||
global_period = config.get("period", 5)
|
||||
|
||||
loader_config = config.get("loaders", {}).get(loader_name, {})
|
||||
period = loader_config.get("period", global_period)
|
||||
offset = loader_config.get("offset", 0)
|
||||
|
||||
if period <= 0:
|
||||
return True
|
||||
|
||||
turn = _load_and_increment(hook_data or {})
|
||||
|
||||
fired = turn == 0 or (turn % period) == offset
|
||||
@@ -256,10 +258,12 @@ def print_introspection() -> None:
|
||||
config = _load_config()
|
||||
CONSOLE.print("[bold cyan]cadence[/bold cyan] Module")
|
||||
CONSOLE.print(f" Enabled: {config.get('enabled', True)}")
|
||||
CONSOLE.print(f" Period: {config.get('period', 5)} turns")
|
||||
global_period = config.get("period", 5)
|
||||
CONSOLE.print(f" Period: {global_period} turns (global default)")
|
||||
loaders = config.get("loaders", {})
|
||||
for name, lcfg in loaders.items():
|
||||
CONSOLE.print(f" Loader '{name}': offset={lcfg.get('offset', 0)}")
|
||||
lp = lcfg.get("period", global_period)
|
||||
CONSOLE.print(f" Loader '{name}': period={lp} offset={lcfg.get('offset', 0)}")
|
||||
path = _state_path()
|
||||
if path and path.exists():
|
||||
try:
|
||||
|
||||
@@ -489,3 +489,181 @@ class TestLoaderCadenceGuard:
|
||||
assert result["stdout"] == ""
|
||||
assert result["exit_code"] == 0
|
||||
assert "sound" not in result
|
||||
|
||||
|
||||
class TestPerLoaderPeriod:
|
||||
def setup_method(self):
|
||||
_reset_module_globals()
|
||||
|
||||
def test_loader_period_overrides_global(self, tmp_path):
|
||||
"""A loader with period:1 fires every turn, even when global period is 5."""
|
||||
from aipass.hooks.apps.modules.cadence import should_fire
|
||||
|
||||
config = tmp_path / "cadence.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"enabled": True,
|
||||
"period": 5,
|
||||
"loaders": {"tier0": {"period": 1}, "global": {"offset": 0}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
_write_state(tmp_path, turn=2)
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
assert should_fire("tier0") is True
|
||||
_reset_module_globals()
|
||||
assert should_fire("global") is False
|
||||
|
||||
def test_loader_without_period_uses_global(self, tmp_path):
|
||||
from aipass.hooks.apps.modules.cadence import should_fire
|
||||
|
||||
config = tmp_path / "cadence.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"enabled": True,
|
||||
"period": 5,
|
||||
"loaders": {"global": {"offset": 0}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
_write_state(tmp_path, turn=2)
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
assert should_fire("global") is False
|
||||
|
||||
def test_tier0_period_1_fires_every_turn(self, tmp_path):
|
||||
from aipass.hooks.apps.modules.cadence import should_fire
|
||||
|
||||
config = tmp_path / "cadence.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"enabled": True,
|
||||
"period": 5,
|
||||
"loaders": {"tier0": {"period": 1}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for turn_val in range(1, 8):
|
||||
_reset_module_globals()
|
||||
_write_state(tmp_path, turn=turn_val - 1)
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
assert should_fire("tier0") is True, f"tier0 should fire on turn {turn_val}"
|
||||
|
||||
def test_navmap_period_5_skips_non_fire_turns(self, tmp_path):
|
||||
from aipass.hooks.apps.modules.cadence import should_fire
|
||||
|
||||
config = tmp_path / "cadence.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"enabled": True,
|
||||
"period": 5,
|
||||
"loaders": {"navmap": {"period": 5, "offset": 0}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
_write_state(tmp_path, turn=2)
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
assert should_fire("navmap") is False
|
||||
|
||||
def test_navmap_fires_on_turn_0(self, tmp_path):
|
||||
from aipass.hooks.apps.modules.cadence import should_fire
|
||||
|
||||
config = tmp_path / "cadence.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"enabled": True,
|
||||
"period": 5,
|
||||
"loaders": {"navmap": {"period": 5, "offset": 0}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
assert should_fire("navmap") is True
|
||||
|
||||
def test_navmap_fires_after_reset_counter(self, tmp_path):
|
||||
from aipass.hooks.apps.modules.cadence import should_fire, reset_counter
|
||||
|
||||
config = tmp_path / "cadence.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"enabled": True,
|
||||
"period": 5,
|
||||
"loaders": {"navmap": {"period": 5, "offset": 0}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
_write_state(tmp_path, turn=7)
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
reset_counter()
|
||||
|
||||
_reset_module_globals()
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
assert should_fire("navmap") is True
|
||||
|
||||
def test_per_loader_period_zero_always_fires(self, tmp_path):
|
||||
from aipass.hooks.apps.modules.cadence import should_fire
|
||||
|
||||
config = tmp_path / "cadence.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"enabled": True,
|
||||
"period": 5,
|
||||
"loaders": {"always": {"period": 0}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
_write_state(tmp_path, turn=2)
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}._GUARD_DIR", tmp_path),
|
||||
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
|
||||
patch(f"{MODULE}._CONFIG_PATH", config),
|
||||
):
|
||||
assert should_fire("always") is True
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_navmap.py
|
||||
# Version: 1.0.0
|
||||
# Description: Tests for navmap prompt handler
|
||||
# Branch: hooks
|
||||
# Created: 2026-06-18
|
||||
# Modified: 2026-06-18
|
||||
# =============================================
|
||||
|
||||
"""Tests for handlers/prompt/navmap.py."""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
def _mock_cadence(fires: bool = True):
|
||||
"""Return a mock cadence module with configurable should_fire."""
|
||||
mock = MagicMock()
|
||||
mock.should_fire.return_value = fires
|
||||
return mock
|
||||
|
||||
|
||||
class TestNavmapHandler:
|
||||
def test_loads_navmap(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.navmap import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
prompt = aipass_dir / "tier1_navmap.md"
|
||||
prompt.write_text("# Navigation Map\nAgents and services", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert "Navigation Map" in result["stdout"]
|
||||
assert "Agents and services" in result["stdout"]
|
||||
assert result["sound"] == "navmap"
|
||||
|
||||
def test_returns_empty_when_file_missing(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.navmap import handle
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert result["stdout"] == ""
|
||||
assert "sound" not in result
|
||||
|
||||
def test_empty_hook_data(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.navmap import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier1_navmap.md").write_text("navmap content", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert result["stdout"] == "navmap content"
|
||||
|
||||
def test_skips_on_cadence_skip(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.navmap import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier1_navmap.md").write_text("content", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(False)):
|
||||
result = handle({})
|
||||
|
||||
assert result["stdout"] == ""
|
||||
assert result["exit_code"] == 0
|
||||
assert "sound" not in result
|
||||
|
||||
def test_fires_anyway_on_cadence_error(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.navmap import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier1_navmap.md").write_text("navmap content", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", side_effect=ImportError("no cadence")):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert "navmap content" in result["stdout"]
|
||||
|
||||
def test_external_project_gets_own_file(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.navmap import handle
|
||||
|
||||
project = tmp_path / "my-project"
|
||||
project.mkdir()
|
||||
aipass_dir = project / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier1_navmap.md").write_text("# Project Navmap", encoding="utf-8")
|
||||
monkeypatch.chdir(project)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": "/some/other/path"}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert "Project Navmap" in result["stdout"]
|
||||
assert result["sound"] == "navmap"
|
||||
|
||||
def test_cadence_called_with_navmap_name(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.navmap import handle
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
mock = _mock_cadence(False)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=mock):
|
||||
handle({"some": "data"})
|
||||
|
||||
mock.should_fire.assert_called_once_with("navmap", {"some": "data"})
|
||||
@@ -0,0 +1,127 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_tier0_kernel.py
|
||||
# Version: 1.0.0
|
||||
# Description: Tests for tier0_kernel prompt handler
|
||||
# Branch: hooks
|
||||
# Created: 2026-06-18
|
||||
# Modified: 2026-06-18
|
||||
# =============================================
|
||||
|
||||
"""Tests for handlers/prompt/tier0_kernel.py."""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
def _mock_cadence(fires: bool = True):
|
||||
"""Return a mock cadence module with configurable should_fire."""
|
||||
mock = MagicMock()
|
||||
mock.should_fire.return_value = fires
|
||||
return mock
|
||||
|
||||
|
||||
class TestTier0KernelHandler:
|
||||
def test_loads_tier0_kernel(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
prompt = aipass_dir / "tier0_kernel.md"
|
||||
prompt.write_text("# Tier 0 Kernel\nAlways on", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert "Tier 0 Kernel" in result["stdout"]
|
||||
assert "Always on" in result["stdout"]
|
||||
assert result["sound"] == "tier0 kernel"
|
||||
|
||||
def test_returns_empty_when_file_missing(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert result["stdout"] == ""
|
||||
assert "sound" not in result
|
||||
|
||||
def test_empty_hook_data(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier0_kernel.md").write_text("content", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert result["stdout"] == "content"
|
||||
|
||||
def test_skips_on_cadence_skip(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier0_kernel.md").write_text("content", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(False)):
|
||||
result = handle({})
|
||||
|
||||
assert result["stdout"] == ""
|
||||
assert result["exit_code"] == 0
|
||||
assert "sound" not in result
|
||||
|
||||
def test_fires_anyway_on_cadence_error(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
|
||||
|
||||
aipass_dir = tmp_path / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier0_kernel.md").write_text("kernel content", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", side_effect=ImportError("no cadence")):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert "kernel content" in result["stdout"]
|
||||
|
||||
def test_external_project_gets_own_file(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
|
||||
|
||||
project = tmp_path / "my-project"
|
||||
project.mkdir()
|
||||
aipass_dir = project / ".aipass"
|
||||
aipass_dir.mkdir()
|
||||
(aipass_dir / "tier0_kernel.md").write_text("# Project Kernel", encoding="utf-8")
|
||||
monkeypatch.chdir(project)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": "/some/other/path"}):
|
||||
with patch("importlib.import_module", return_value=_mock_cadence(True)):
|
||||
result = handle({})
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert "Project Kernel" in result["stdout"]
|
||||
assert result["sound"] == "tier0 kernel"
|
||||
|
||||
def test_cadence_called_with_tier0_name(self, tmp_path, monkeypatch):
|
||||
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
mock = _mock_cadence(False)
|
||||
|
||||
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
|
||||
with patch("importlib.import_module", return_value=mock):
|
||||
handle({"some": "data"})
|
||||
|
||||
mock.should_fire.assert_called_once_with("tier0", {"some": "data"})
|
||||
Reference in New Issue
Block a user