147 lines
5.7 KiB
Python
147 lines
5.7 KiB
Python
"""Tests for hooks/inject.py. Run: python3 tests/hook_test.py"""
|
|
|
|
import json
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
|
|
PLUGIN_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(PLUGIN_ROOT / "hooks"))
|
|
|
|
import inject # noqa: E402
|
|
|
|
|
|
def write_prompts(directory, files):
|
|
"""files: dict of filename -> content, written under directory."""
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
for name, content in files.items():
|
|
(directory / name).write_text(content)
|
|
|
|
|
|
class ComposeOrderTest(unittest.TestCase):
|
|
def test_concatenates_in_filename_sort_order(self):
|
|
with TemporaryDirectory() as tmp:
|
|
d = Path(tmp)
|
|
write_prompts(d, {"20-second.md": "second\n", "10-first.md": "first\n"})
|
|
paths = inject.prompt_files(d)
|
|
self.assertEqual(["10-first.md", "20-second.md"], [Path(p).name for p in paths])
|
|
self.assertEqual("first\nsecond\n", inject.compose(paths))
|
|
|
|
def test_no_separator_added_between_files(self):
|
|
with TemporaryDirectory() as tmp:
|
|
d = Path(tmp)
|
|
write_prompts(d, {"10-a.md": "a", "20-b.md": "b"})
|
|
paths = inject.prompt_files(d)
|
|
self.assertEqual("ab", inject.compose(paths))
|
|
|
|
|
|
class ByteIdentityTest(unittest.TestCase):
|
|
"""With only the real 10-orchestration.md present, the composer's output
|
|
must be byte-identical to what the old single-file inject.py emitted
|
|
directly from ORCHESTRATION.md's contents — no added separators or
|
|
trailing newlines."""
|
|
|
|
def test_single_file_matches_its_raw_contents_exactly(self):
|
|
real_file = PLUGIN_ROOT / "prompts" / "session-start" / "10-orchestration.md"
|
|
with TemporaryDirectory() as tmp:
|
|
d = Path(tmp)
|
|
d.mkdir(exist_ok=True)
|
|
shadow = d / "10-orchestration.md"
|
|
shadow.write_bytes(real_file.read_bytes())
|
|
paths = inject.prompt_files(d)
|
|
composed = inject.compose(paths)
|
|
self.assertEqual(real_file.read_text(), composed)
|
|
|
|
def test_real_prompts_dir_composes_all_files_byte_identical_concatenation(self):
|
|
paths = inject.prompt_files()
|
|
self.assertGreaterEqual(len(paths), 1)
|
|
expected = "".join(Path(p).read_text() for p in paths)
|
|
self.assertEqual(expected, inject.compose(paths))
|
|
|
|
|
|
class BudgetTest(unittest.TestCase):
|
|
def make_dir_with_lines(self, tmp, n_lines):
|
|
d = Path(tmp)
|
|
d.mkdir(exist_ok=True)
|
|
content = "\n".join(f"line {i}" for i in range(n_lines)) + "\n"
|
|
(d / "10-big.md").write_text(content)
|
|
return d
|
|
|
|
def test_under_warn_budget_injects_silently(self):
|
|
with TemporaryDirectory() as tmp:
|
|
d = self.make_dir_with_lines(tmp, inject.WARN_LINE_BUDGET - 1)
|
|
paths = inject.prompt_files(d)
|
|
context = inject.compose(paths)
|
|
self.assertLessEqual(len(context.splitlines()), inject.WARN_LINE_BUDGET)
|
|
|
|
def test_over_warn_under_refuse_still_injects(self):
|
|
with TemporaryDirectory() as tmp:
|
|
d = self.make_dir_with_lines(
|
|
tmp, inject.WARN_LINE_BUDGET + 1
|
|
)
|
|
paths = inject.prompt_files(d)
|
|
context = inject.compose(paths)
|
|
line_count = len(context.splitlines())
|
|
self.assertGreater(line_count, inject.WARN_LINE_BUDGET)
|
|
self.assertLessEqual(line_count, inject.REFUSE_LINE_BUDGET)
|
|
# Injection still happens: compose() returns the full content.
|
|
self.assertTrue(context)
|
|
|
|
def test_over_refuse_budget_line_count_detected(self):
|
|
with TemporaryDirectory() as tmp:
|
|
d = self.make_dir_with_lines(tmp, inject.REFUSE_LINE_BUDGET + 1)
|
|
paths = inject.prompt_files(d)
|
|
context = inject.compose(paths)
|
|
self.assertGreater(len(context.splitlines()), inject.REFUSE_LINE_BUDGET)
|
|
|
|
def test_main_warns_on_stderr_between_warn_and_refuse(self, ):
|
|
import io
|
|
from unittest import mock
|
|
|
|
with TemporaryDirectory() as tmp:
|
|
d = self.make_dir_with_lines(tmp, inject.WARN_LINE_BUDGET + 1)
|
|
stderr = io.StringIO()
|
|
stdout = io.StringIO()
|
|
with mock.patch.object(inject, "SESSION_START_DIR", str(d)), \
|
|
mock.patch("sys.stderr", stderr), \
|
|
mock.patch("sys.stdout", stdout):
|
|
inject.main()
|
|
self.assertIn("injecting anyway", stderr.getvalue())
|
|
payload = json.loads(stdout.getvalue())
|
|
self.assertIn("additionalContext", payload["hookSpecificOutput"])
|
|
|
|
def test_main_refuses_and_emits_nothing_over_refuse_budget(self):
|
|
import io
|
|
from unittest import mock
|
|
|
|
with TemporaryDirectory() as tmp:
|
|
d = self.make_dir_with_lines(tmp, inject.REFUSE_LINE_BUDGET + 1)
|
|
stderr = io.StringIO()
|
|
stdout = io.StringIO()
|
|
with mock.patch.object(inject, "SESSION_START_DIR", str(d)), \
|
|
mock.patch("sys.stderr", stderr), \
|
|
mock.patch("sys.stdout", stdout):
|
|
inject.main()
|
|
self.assertIn("refusing to inject", stderr.getvalue())
|
|
self.assertEqual("", stdout.getvalue())
|
|
|
|
|
|
class NoPromptsTest(unittest.TestCase):
|
|
def test_main_emits_nothing_when_dir_empty(self):
|
|
import io
|
|
from unittest import mock
|
|
|
|
with TemporaryDirectory() as tmp:
|
|
d = Path(tmp) / "empty"
|
|
d.mkdir()
|
|
stdout = io.StringIO()
|
|
with mock.patch.object(inject, "SESSION_START_DIR", str(d)), \
|
|
mock.patch("sys.stdout", stdout):
|
|
inject.main()
|
|
self.assertEqual("", stdout.getvalue())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|