othmanadi--planning-with-files
121 行
4.8 KiB
Python
121 行
4.8 KiB
Python
"""Behavioral contract tests for the Pi planning-with-files extension source.
|
|
|
|
These tests validate that the extension source covers Claude-parity hooks,
|
|
DeepSeek cache-safe mode, and loop guard safety constraints.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
EXT_ROOT = REPO_ROOT / ".pi" / "skills" / "planning-with-files" / "extensions" / "planning-with-files"
|
|
INDEX_TS = EXT_ROOT / "index.ts"
|
|
RUNTIME_TS = EXT_ROOT / "runtime.ts"
|
|
PLAN_TS = EXT_ROOT / "plan.ts"
|
|
CONSTANTS_TS = EXT_ROOT / "constants.ts"
|
|
|
|
|
|
class PiExtensionCapabilitiesTests(unittest.TestCase):
|
|
def _read(self, path: Path) -> str:
|
|
self.assertTrue(path.exists(), f"missing required source file: {path}")
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
def test_required_source_files_exist(self) -> None:
|
|
for path in (INDEX_TS, RUNTIME_TS, PLAN_TS, CONSTANTS_TS):
|
|
self.assertTrue(path.exists(), f"missing required source file: {path}")
|
|
|
|
def test_mode_union_contains_auto_parity_cache_safe_notify(self) -> None:
|
|
text = self._read(RUNTIME_TS)
|
|
self.assertRegex(
|
|
text,
|
|
r'type\s+HookMode\s*=\s*"auto"\s*\|\s*"parity"\s*\|\s*"cache-safe"\s*\|\s*"notify"',
|
|
)
|
|
|
|
def test_registers_required_hook_events(self) -> None:
|
|
text = self._read(RUNTIME_TS)
|
|
required_events = [
|
|
"session_start",
|
|
"before_agent_start",
|
|
"tool_call",
|
|
"tool_result",
|
|
"agent_end",
|
|
"session_before_compact",
|
|
"input",
|
|
]
|
|
for event_name in required_events:
|
|
self.assertIn(f'pi.on("{event_name}"', text)
|
|
|
|
def test_auto_continue_limit_is_three(self) -> None:
|
|
text = self._read(CONSTANTS_TS)
|
|
self.assertRegex(text, r"AUTO_CONTINUE_LIMIT\s*=\s*3")
|
|
|
|
def test_plan_execute_command_is_registered(self) -> None:
|
|
text = self._read(RUNTIME_TS)
|
|
self.assertIn('pi.registerCommand("plan-execute"', text)
|
|
|
|
def test_tampered_blocking_message_exists(self) -> None:
|
|
text = self._read(CONSTANTS_TS)
|
|
self.assertIn("PLAN TAMPERED", text)
|
|
|
|
def test_plan_resolution_keeps_plan_id_active_plan_and_newest_fallback(self) -> None:
|
|
text = self._read(PLAN_TS)
|
|
self.assertIn("process.env.PLAN_ID", text)
|
|
self.assertIn(".active_plan", text)
|
|
self.assertTrue(
|
|
re.search(r"resolveNewestPlanDir", text),
|
|
"plan resolver must include newest plan directory fallback",
|
|
)
|
|
|
|
def test_dangerous_bash_uses_word_boundary_regex(self) -> None:
|
|
# v2.40: substring matching produced false positives on every benign
|
|
# `git push origin <branch>`. The runtime must now use a regex array
|
|
# so only destructive variants (--force, --mirror, rm -rf, sudo,
|
|
# chmod 777, git reset --hard, git clean -fd, fork bomb, dd to disk)
|
|
# trigger the warning.
|
|
text = self._read(RUNTIME_TS)
|
|
self.assertIn(
|
|
"DANGEROUS_BASH_PATTERNS",
|
|
text,
|
|
"runtime must define DANGEROUS_BASH_PATTERNS regex array",
|
|
)
|
|
# Substring-match shape must NOT be present. The v2.39.0 dangerPatterns
|
|
# list with a raw `"git push"` would fire on every benign push.
|
|
self.assertNotIn(
|
|
'dangerPatterns =',
|
|
text,
|
|
"v2.39.0 dangerPatterns substring list still present; replace with "
|
|
"DANGEROUS_BASH_PATTERNS regex array",
|
|
)
|
|
self.assertNotIn(
|
|
'"git push",',
|
|
text,
|
|
"raw substring 'git push' inside the danger list would false-positive "
|
|
"on benign pushes; use the --force/--mirror regex variant instead",
|
|
)
|
|
# Each required pattern, as literal substring of the TS source. These
|
|
# are stable to YAML/JS escaping: they appear verbatim in the file.
|
|
required_substrings = [
|
|
r"\brm\s+-[a-z]*r", # rm -rf, rm -fr, rm -Rf
|
|
r"\bsudo\b", # sudo
|
|
r"\bchmod\s+(0?777", # chmod 777 / chmod 0777
|
|
r"a\+rwx", # chmod a+rwx
|
|
r"\bgit\s+push\s+.*(--force", # forced push
|
|
r"--mirror", # mirror push
|
|
r"\bgit\s+reset\s+--hard\b", # git reset --hard
|
|
r"\bgit\s+clean\s+-", # git clean -fd family
|
|
r"\bdd\s+.*of=", # dd to a raw device
|
|
]
|
|
missing = [s for s in required_substrings if s not in text]
|
|
self.assertFalse(
|
|
missing,
|
|
"runtime missing required dangerous-command patterns: "
|
|
+ ", ".join(missing),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|