test_lt_security.py python
136 lines 4.7 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Security tests for LT loop tightening (§LT.10)."""
2
3 from __future__ import annotations
4
5 import json
6 import os
7 import stat
8 import subprocess
9 from pathlib import Path
10
11 from tests.support import FIXTURES, KIT_ROOT, git_status_runner, load_fixture_config, run_cli
12 from tools.footprint_coverage import check_footprint_coverage
13 from tools.handover_compact import compact_handover_change_log
14
15
16 def test_coverage_missing_paths_are_repo_relative(tmp_path: Path) -> None:
17 config = load_fixture_config(tmp_path, "config-git-only.yaml")
18 from cli.footprint import resolve_footprint
19 from cli.version_lock import ORIGIN_KIT, FootprintEntry, build_version_lock_from_entries
20
21 files = resolve_footprint(config, kit=KIT_ROOT)
22 lock = build_version_lock_from_entries(
23 kit_version="0.1.0",
24 config_version=1,
25 entries=[
26 FootprintEntry(
27 path=files[0].destination,
28 source=files[0].source,
29 sha256="0" * 64,
30 origin=ORIGIN_KIT,
31 )
32 ],
33 installed_at="2026-01-01T00:00:00Z",
34 )
35 report = check_footprint_coverage(tmp_path, config, lock=lock, rendered=files, kit=KIT_ROOT)
36 for path in report.missing:
37 assert not Path(path).is_absolute()
38 assert ".." not in Path(path).parts
39
40
41 def test_compact_archive_path_repo_relative(tmp_path: Path) -> None:
42 config = load_fixture_config(tmp_path, "config-git-only.yaml")
43 handover = tmp_path / "docs" / "OVERSEER-HANDOVER.md"
44 handover.parent.mkdir(parents=True, exist_ok=True)
45 bullets = "\n\n".join(f"- **2026-01-{d:02d}** — e" for d in range(1, 21))
46 handover.write_text(
47 f"<!-- overseer:anchor:change-log -->\n{bullets}\n"
48 "<!-- /overseer:anchor:change-log -->\n",
49 encoding="utf-8",
50 )
51 report = compact_handover_change_log(config, tmp_path, keep=15, write=True)
52 assert not Path(report.archive).is_absolute()
53 assert ".." not in Path(report.archive).parts
54 living = handover.read_text(encoding="utf-8")
55 assert str(tmp_path) not in living
56
57
58 def test_session_start_hook_fail_open_without_ok(tmp_path: Path) -> None:
59 script = KIT_ROOT / "cursor" / "hooks" / "session-start-next.sh"
60 env = os.environ.copy()
61 env.pop("OVERSEER_OK", None)
62 env["PATH"] = "/usr/bin:/bin"
63 completed = subprocess.run(
64 ["sh", str(script)],
65 cwd=tmp_path,
66 env=env,
67 capture_output=True,
68 text=True,
69 check=False,
70 )
71 assert completed.returncode == 0
72 payload = json.loads(completed.stdout.strip())
73 assert "additional_context" in payload
74 assert "followup_message" in payload
75
76
77 def test_git_only_handover_compact_zero_muse_argv(tmp_path: Path) -> None:
78 runner = git_status_runner()
79 assert (
80 run_cli(
81 ["init", "--from-config", str(FIXTURES / "config-git-only.yaml"), "--non-interactive"],
82 cwd=tmp_path,
83 runner=runner,
84 )
85 == 0
86 )
87 handover = tmp_path / "docs" / "OVERSEER-HANDOVER.md"
88 bullets = "\n\n".join(f"- **2026-01-{d:02d}** — e" for d in range(1, 21))
89 text = handover.read_text(encoding="utf-8")
90 if "<!-- overseer:anchor:change-log -->" in text:
91 start = text.index("<!-- overseer:anchor:change-log -->")
92 end = text.index("<!-- /overseer:anchor:change-log -->")
93 handover.write_text(
94 text[: start + len("<!-- overseer:anchor:change-log -->")]
95 + "\n"
96 + bullets
97 + "\n"
98 + text[end:],
99 encoding="utf-8",
100 )
101 else:
102 handover.write_text(
103 text
104 + "\n<!-- overseer:anchor:change-log -->\n"
105 + bullets
106 + "\n<!-- /overseer:anchor:change-log -->\n",
107 encoding="utf-8",
108 )
109 run_cli(["handover-compact", "--write"], cwd=tmp_path, runner=runner)
110 muse_calls = [c for c in runner.calls if c[0].startswith("muse")]
111 assert muse_calls == []
112
113
114 def test_session_start_hook_fail_open_without_python3(tmp_path: Path) -> None:
115 script = KIT_ROOT / "cursor" / "hooks" / "session-start-next.sh"
116 env = os.environ.copy()
117 env.pop("OVERSEER_OK", None)
118 env["PATH"] = "/usr/bin:/bin"
119 completed = subprocess.run(
120 ["sh", str(script)],
121 cwd=tmp_path,
122 env=env,
123 capture_output=True,
124 text=True,
125 check=False,
126 )
127 assert completed.returncode == 0
128 payload = json.loads(completed.stdout.strip().splitlines()[-1])
129 assert "additional_context" in payload
130
131
132 def test_hook_scripts_are_executable_in_kit_source() -> None:
133 for name in ("session-start-next.sh", "session-end-closeout.sh"):
134 path = KIT_ROOT / "cursor" / "hooks" / name
135 mode = path.stat().st_mode
136 assert mode & stat.S_IXUSR
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago